FFI: 기초, Foreign Function Interface 및 C 통합

저자: IT Sectr 게시일: 2026-06-05 읽는 시간: 10 분

FFI(Foreign Function Interface)는 dart:ffi 패키지에서 제공하는 Dart 언어의 메커니즘으로, Kotlin, Swift 또는 Java의 중간 계층 없이 네이티브 C 라이브러리의 함수를 직접 호출할 수 있습니다. 개발자는 동적 라이브러리(Android에서는 .so, iOS에서는 .dylib, Windows에서는 .dll)를 로드하고, C 함수 시그니처를 선언하여 일반 Dart 함수처럼 호출합니다. Dart API Reference(2025)에 따르면, FFI는 언어 간 호출의 오버헤드를 0.1 µs로 줄여 Method Channel보다 수십 배 빠릅니다.

주요 내용

  • FFI(Foreign Function Interface) — Dart에서 직접 C 함수를 호출하는 메커니즘
  • dart:ffi는 라이브러리 로드 및 시그니처 선언을 위한 API 제공
  • 성능 FFI 호출이 Method Channel보다 50~100배 높음
  • 타이핑 FFI는 기본 유형, 구조체 및 C 포인터 지원
  • Flutter는 네이티브 라이브러리(OpenCV, SQLite, FFmpeg) 통합에 FFI 사용

FFI란?

FFI(Foreign Function Interface)는 프로그래밍 언어가 다른 언어로 작성된 함수를 호출할 수 있게 하는 메커니즘입니다. Dart와 Flutter의 맥락에서 FFI는 Java(Android) 또는 Swift/Objective-C(iOS)로 플랫폼별 코드를 작성하지 않고 Dart 코드에서 직접 C/C++ 라이브러리의 함수를 호출하는 기능을 의미합니다.

dart:ffi 패키지는 Dart 2.12(2021)에 등장했으며 그 이후로 Flutter를 네이티브 코드와 통합하는 핵심 도구가 되었습니다. dart:ffi 이전에 Dart에서 C 함수를 호출하는 유일한 방법은 Method Channel이었습니다 — Dart와 네이티브 측 간에 JSON 직렬화를 통해 메시지를 전달하는 비동기 메커니즘입니다. FFI는 다르게 작동합니다: Dart 코드가 C 라이브러리 메모리에 직접 액세스하여 직렬화나 컨텍스트 스위칭 없이 네이티브 ABI(애플리케이션 바이너리 인터페이스)를 통해 함수를 호출합니다.

FFI는 성능이 중요한 시나리오에서 특히 요구됩니다: 이미지 처리(OpenCV), 오디오(FFmpeg), 암호화(OpenSSL), 머신 러닝(TensorFlow Lite) 및 데이터베이스(SQLite). 이러한 모든 경우에 Method Channel은 허용할 수 없는 지연을 발생시키는 반면, FFI는 네이티브 C/C++ 코드에 필적하는 성능을 제공합니다. dart:ffi 라이브러리는 메모리 관리(할당, 해제 및 포인터 조작)도 지원합니다.

FFI vs Method Channel: 근본적인 차이

Method Channel은 비동기적으로 작동합니다: Dart가 네이티브 코드에 메시지를 보내면 네이티브 코드가 이를 처리하고 결과를 다시 보냅니다. 각 호출은 Map으로 인수 직렬화, 큐를 통한 전달 및 역직렬화가 필요합니다. 호출당 0.5~5ms가 소요됩니다. FFI는 동기식으로 직렬화 없이 작동합니다 — C 함수 호출은 0.01~0.1 µs가 소요됩니다. 50~500배의 차이로 고주파 작업에 중요합니다.

dart:ffi는 어떻게 작동하나요?

dart:ffi 작업은 세 단계로 구성됩니다: 라이브러리 로드, 시그니처 선언 및 함수 호출. 각 단계는 Dart의 엄격한 타이핑을 사용하여 런타임 오류를 최소화합니다.

첫 번째 단계에서는 DynamicLibrary 클래스를 통해 동적 라이브러리가 로드됩니다. 라이브러리는 이름(libxyz.so, libxyz.dylib, xyz.dll) 또는 전체 경로로 로드할 수 있습니다. Dart는 자동으로 시스템의 표준 경로에서 라이브러리를 검색합니다. DynamicLibrary는 lookupFunction 메서드를 제공하여 심볼 이름으로 Dart 함수를 C 함수에 바인딩합니다.

두 번째 단계에서는 C 시그니처에 해당하는 유형 주석이 있는 Dart 함수가 선언됩니다. dart:ffi의 특수 유형(Int32, Float, Double, Pointer, NativeFunction, Handle 등)이 사용됩니다. lookupFunction 주석은 두 개의 제네릭 매개변수를 받습니다: Dart 함수 유형(Dart에서 어떻게 보일지)과 네이티브 C 함수 유형(C에서 어떻게 선언되는지).

세 번째 단계에서는 생성된 Dart 함수가 일반 함수처럼 호출됩니다. 인수는 직접 전달되고 결과는 즉시 반환됩니다. C 함수가 포인터를 통해 메모리를 수정하는 경우 Dart는 Pointer 클래스를 통해 이러한 변경 사항을 읽을 수 있습니다. C 측의 메모리 관리는 개발자의 책임입니다 — dart:ffi는 C에서 malloc으로 할당된 메모리를 관리하지 않습니다.

기본 예제: Dart에서 C 함수 호출

dart
import 'dart:ffi'
import 'package:ffi/ffi.dart'

// C 함수 선언: int add(int a, int b)
typedef AddNative = Int32 Function(Int32, Int32)
typedef AddDart = int Function(int, int)

void main() {
    final lib = DynamicLibrary.open('libcalculator.so')
    final AddDart add = lib
        .lookupFunction<AddNative, AddDart>('add')

    print(add(5, 3)) // 8
}

이 예제에서 add는 두 개의 int를 받아 int를 반환하는 C 함수입니다. AddNative typedef는 dart:ffi 유형으로 C 시그니처를 설명하고, AddDart는 이 함수가 Dart에서 어떻게 보일지 설명합니다. lookupFunction이 이를 바인딩하고 일반 함수처럼 호출할 수 있는 Dart 함수를 반환합니다.

FFI의 데이터 유형

dart:ffi는 C 유형에 해당하는 유형 집합을 제공합니다. 각 유형은 고정된 크기와 Dart와 C 간의 변환 규칙이 있습니다. FFI의 올바른 작동을 위해서는 유형 매핑을 이해하는 것이 매우 중요합니다 — 유형의 크기나 부호 오류는 애플리케이션 충돌을 일으킬 수 있습니다.

C 유형dart:ffi 유형Dart 유형크기(바이트)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8(포인터)
void*Pointer<Void>Pointer8(포인터)
structPointer<T>(Struct)Pointer필드에 따라 다름

C 문자열(char*) 작업을 위해 dart:ffi는 Pointer<Int8>을 사용합니다. Dart String에서 C char*로의 변환 및 역변환은 toNativeUtf8(ffi 패키지)과 fromUtf8을 통해 수행됩니다. 메모리 누수를 방지하기 위해 사용 후 calloc.free를 통해 C 문자열을 해제하는 것이 중요합니다.

구조체(Struct)

dart:ffi는 Struct를 확장하는 Dart 클래스로 C 구조체 선언을 지원합니다. 구조체 필드는 @Int32(), @Float(), @Array() 등의 주석으로 선언됩니다. 필드의 크기와 오프셋은 플랫폼의 ABI에 따라 자동으로 계산됩니다. Pointer<Point>는 구조체에 대한 포인터를 반환하는 C 함수에서 얻거나 Dart에서 calloc을 통해 할당할 수 있습니다.

dart
// C 구조체: typedef struct { int x; int y; } Point;
final class Point extends Struct {
    @Int32()
    external int x

    @Int32()
    external int y
}

// Point*를 반환하는 C 함수 호출
typedef CreatePointNative = Pointer<Point> Function(Int32, Int32)
typedef CreatePointDart = Pointer<Point> Function(int, int)

final Pointer<Point> p = createPoint(10, 20)
print('x: ${p.ref.x}, y: ${p.ref.y}')
calloc.free(p) // 메모리 해제

Point 클래스는 Struct를 확장하고 @Int32() 주석으로 x 및 y 필드를 선언합니다. 생성된 C 코드는 정확히 동일한 메모리 레이아웃을 갖습니다. Pointer.ref는 게터와 세터를 통해 구조체 필드에 대한 액세스를 제공합니다.

FFI 실용 예제

더 복잡한 예제를 살펴보겠습니다 — SHA256 해시 계산을 위한 C 라이브러리와의 통합입니다. 이는 FFI가 Method Channel에 비해 상당한 성능 이점을 제공하는 일반적인 작업입니다.

FFI를 통한 OpenSSL 통합

OpenSSL 라이브러리는 문자열의 해시를 계산하는 SHA256 함수를 제공합니다. dart:ffi를 통해 Java나 Swift 래퍼를 작성하지 않고 직접 호출할 수 있습니다. 이는 FFI가 Flutter에서 기존 C 라이브러리를 재사용하는 방법을 보여주는 예입니다.

dart
import 'dart:ffi'
import 'package:ffi/ffi.dart'

// 시그니처: unsigned char* SHA256(
//   const unsigned char *d, size_t n, unsigned char *md)
typedef Sha256Native = Pointer<Uint8> Function(
    Pointer<Uint8>, Size, Pointer<Uint8>)
typedef Sha256Dart = Pointer<Uint8> Function(
    Pointer<Uint8>, int, Pointer<Uint8>)

String sha256(String input) {
    final lib = DynamicLibrary.open('libcrypto.so')
    final Sha256Dart sha256Fn = lib
        .lookupFunction<Sha256Native, Sha256Dart>('SHA256')

    final inputPtr = input.toNativeUtf8()
    final outputPtr = calloc(Uint8)(32) // SHA256 = 32바이트

    sha256Fn(inputPtr, input.length, outputPtr)

    final digest = outputPtr.asTypedList(32)
    final hex = digest.map((b) => b.toRadixString(16)
        .padLeft(2, '0')).join()

    calloc.free(inputPtr)
    calloc.free(outputPtr)

    return hex
}

이 예제에서 sha256 함수는 libcrypto.so 라이브러리를 로드하고 SHA256 심볼을 찾아 입력 및 출력 데이터에 대한 포인터로 호출합니다. toNativeUtf8은 Dart String을 C 문자열로 변환하고(메모리 할당), asTypedList는 결과 바이트 배열을 읽을 수 있습니다. 사용 후 메모리가 해제됩니다 — 누수를 방지하기 위한 필수 단계입니다.

메모리 할당 및 해제

ffi 패키지는 C 호환 메모리 할당을 위한 calloc 함수를 제공합니다. 할당된 메모리는 calloc.free를 통해 해제해야 합니다, 그렇지 않으면 누수가 발생합니다. 자동 메모리 관리를 위해 ffi 패키지의 Arena 클래스를 사용할 수 있으며, arena.release()가 호출되면 내부에 할당된 모든 메모리를 해제합니다. 이는 많은 수의 임시 할당에 특히 편리합니다.

제한 사항 및 모범 사례

FFI의 강력함에도 불구하고 Flutter 애플리케이션 아키텍처를 설계할 때 고려해야 할 제한 사항이 있습니다. 주요 제한 사항은 유형 안전성, 메모리 관리 및 플랫폼 호환성과 관련됩니다.

안전성

FFI는 런타임에 유형 검사를 수행하지 않습니다. C 함수가 포인터를 예상했지만 숫자를 받으면 애플리케이션이 세그멘테이션 폴트와 함께 충돌합니다. C 헤더 파일(.h)을 기반으로 유형 안전 Dart 래퍼를 생성하는 도구인 FFIgen을 사용하는 것이 좋습니다. FFIgen은 C 함수 선언을 분석하고 올바른 유형으로 Dart 코드를 생성하여 코드 작성 단계에서 오류를 제거합니다.

플랫폼 호환성

동적 라이브러리의 이름과 경로는 플랫폼마다 다릅니다: Android/Linux에서는 libxyz.so, iOS/macOS에서는 libxyz.dylib, Windows에서는 xyz.dll입니다. 크로스 플랫폼 라이브러리의 경우 dart:io(Platform.isAndroid, Platform.isIOS) 또는 package:ffi와 같은 추상화를 통한 조건부 컴파일이 사용됩니다. 현재 플랫폼에 맞는 라이브러리를 반환하는 팩토리 메서드를 만드는 것이 좋습니다.

메모리 관리

FFI는 C 측의 메모리를 관리하지 않습니다. C 함수가 malloc을 통해 메모리를 할당한 경우 free를 통해 해제해야 합니다, 그렇지 않으면 누수가 발생합니다. Dart에는 C 메모리용 가비지 컬렉터가 없습니다. 권장 사항: 항상 메모리가 할당된 동일한 메서드에서 해제하거나 그룹 해제를 위해 Arena를 사용하세요.

성능 및 스레드

FFI 호출은 Dart 코드와 동일한 스레드에서 실행됩니다. 긴 동기 작업(10ms 이상)은 UI 스레드를 차단하고 프레임 드롭을 유발합니다. 긴 작업의 경우 Isolate에서 C 함수를 호출하거나 C 함수가 백그라운드 스레드에서 작업을 실행하고 Port 또는 콜백을 통해 Dart에 알리도록 해야 합니다.

dart
// 긴 작업을 위한 Isolate의 FFI
import 'dart:isolate'

Future<String> computeHash(String input) async {
    final port = ReceivePort()
    await Isolate.spawn((SendPort sendPort) {
        final result = sha256(input) // FFI 호출
        sendPort.send(result)
    }, port.sendPort)

    return await port.first as String
}

FFI 호출을 Isolate로 이동하면 UI 스레드가 차단되지 않습니다. 그러나 Isolate 간 대량의 데이터 전송에는 메모리 복사가 필요합니다. 큰 버퍼(>10 MB)의 경우 SharedMemory 또는 메모리 매핑 파일을 사용하는 것이 좋습니다.

자주 묻는 질문

FFI와 Method Channel의 차이점은?

FFI는 C 함수를 직접, 동기식으로, 직렬화 없이 호출합니다 — 지연 시간 0.01~0.1 µs. Method Channel은 JSON 직렬화를 통해 비동기식으로 0.5~5ms의 지연 시간으로 작동합니다. FFI는 고성능 작업에 적합하고, Method Channel은 간단한 플랫폼 API 호출에 적합합니다.

FFI를 통해 C++ 함수를 호출할 수 있나요?

직접적으로는 — 아니요, dart:ffi는 C 함수만 지원합니다. C++를 호출하려면 extern “C”(C 심볼로 내보내는 진입점)로 C 래퍼를 만들어야 합니다. C++ 클래스는 메서드 호출을 C 함수로 변환하는 추가 계층이 필요합니다.

C 함수의 오류를 처리하는 방법은?

FFI는 예외를 지원하지 않습니다 — C 함수가 오류 코드를 반환하면 수동으로 확인해야 합니다. Dart에서 FFI 호출을 try-catch로 감싸고 C 함수의 반환 코드를 확인하는 것이 좋습니다. 심각한 오류(segfault)는 잡을 수 없습니다.

FFI를 통해 사용할 수 없는 라이브러리는?

FFI는 복잡한 Java(JNI) 또는 Objective-C(Message Dispatch) 초기화가 필요한 라이브러리와 작동하지 않습니다. 예를 들어 UIKit 및 Android Views는 FFI를 통해 액세스할 수 없습니다. 제한 사항은 FFI가 C ABI 수준에서 작동하는 반면 이러한 API는 특정 런타임이 필요하기 때문입니다.

C 라이브러리를 각 플랫폼마다 컴파일해야 하나요?

네, C 라이브러리는 각 대상 플랫폼에 대해 별도로 컴파일됩니다. Android의 경우 .so가 다른 ABI(armeabi-v7a, arm64-v8a, x86_64)용으로 빌드됩니다. iOS의 경우 — 유니버설 .dylib(arm64). Windows의 경우 — .dll. Flutter는 빌드 중에 자동으로 올바른 라이브러리 버전을 패키징합니다.

요약

  • FFI(Foreign Function Interface) — dart:ffi를 통해 Dart에서 직접 C 함수를 호출하는 메커니즘
  • 성능 FFI 호출이 Method Channel보다 50~500배 높음
  • 아키텍처 라이브러리 로드, 시그니처 선언 및 함수 호출 포함
  • 데이터 유형 dart:ffi는 Int32, Float, Double, Pointer, Struct 및 기타 C 유형 지원
  • 메모리 C 측은 calloc/free 또는 Arena를 통해 수동 관리
  • 제한 사항 FFI: 런타임 유형 검사 없음, 직접 C++ 지원 없음, UI 스레드 차단
  • 사용 Flutter에서 네이티브 라이브러리와의 고성능 통합에 FFI 사용

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기