dart:ffi를 사용한 C 상호운용(C interop)

dart:ffi를 사용한 C 상호운용(C interop)

Dart 프로그램에서 C 코드를 사용하려면 dart:ffi 라이브러리를 사용해요.

출처: 원문

본문

Dart Native 플랫폼에서 실행되는 Dart 모바일, 명령줄, 서버 앱은 dart:ffi 라이브러리를 사용해 네이티브 C API를 호출하고, 네이티브 메모리를 읽고, 쓰고, 할당하고, 해제할 수 있어요. FFI는 외부 함수 인터페이스(foreign function interface)를 뜻해요. 비슷한 기능을 나타내는 다른 용어로는 네이티브 인터페이스(native interface)와 언어 바인딩(language bindings)이 있어요.

API 문서는 dart:ffi API 참조에서 볼 수 있어요.

예제 파일 다운로드

이 가이드의 예제를 다루려면 전체 ffi 샘플 디렉터리를 다운로드해 주세요. 여기에는 dart:ffi 라이브러리 사용법을 보여주는 다음 예제가 포함되어 있어요.

예제 설명
hello_world 인자도 반환 값도 없는 C 함수를 호출하는 방법
primitives 인자와 반환 값이 intpointer인 C 함수를 호출하는 방법
structs 구조체(struct)를 사용해 문자열을 C와 주고받고, 단순하고 복잡한 C 구조체를 다루는 방법
test_utils 이 모든 예제에 쓰이는 공통 테스트 유틸리티

hello_world 예제 살펴보기

hello_world 예제는 C 라이브러리를 호출하는 데 필요한 최소한의 코드를 담고 있어요. 이 예제는 앞서 다운로드한 samples/ffi에서 찾을 수 있어요.

파일들hello_world 예제는 다음 파일들을 포함해요.

소스 파일 설명
hello.dart C 라이브러리의 hello_world() 함수를 사용하는 Dart 파일
pubspec.yaml SDK 하한이 3.4인 Dart pubspec 파일
hello_library/hello.h hello_world() 함수를 선언
hello_library/hello.c hello.h를 import하고 hello_world() 함수를 정의하는 C 파일
hello_library/hello.def DLL 빌드 시 사용되는 정보를 지정하는 모듈 정의 파일
hello_library/CMakeLists.txt C 코드를 동적 라이브러리로 컴파일하는 CMake 빌드 파일

C 라이브러리를 빌드하면 libhello.dylib(macOS), libhello.dll(Windows), 또는 libhello.so(Linux)라는 동적 라이브러리 파일을 포함한 여러 파일이 만들어져요.

빌드 및 실행 – 동적 라이브러리를 빌드하고 Dart 앱을 실행하는 명령은 다음과 비슷해요.

$ cd hello_library
$ cmake .
...
$ make
...
$ cd ..
$ dart pub get
$ dart run hello.dart
Hello World

참고: macOS에서 Dart VM(dart)을 포함한 실행 파일은 서명된 라이브러리만 로드할 수 있어요. 라이브러리 서명에 대해 더 알아보려면 Apple의 코드 서명 가이드(Code Signing Guide)를 참고해 주세요.

dart:ffi 활용하기

dart:ffi 라이브러리를 사용해 C 함수를 호출하는 방법을 배우려면 hello.dart 파일을 살펴봐 주세요. 이 섹션에서는 이 파일의 내용을 설명해 드릴게요.

  1. dart:ffi를 import해 주세요.
    import 'dart:ffi' as ffi;
    
  2. 동적 라이브러리의 경로를 저장하는 데 사용할 path 라이브러리를 import해 주세요.
    import 'dart:io' show Platform, Directory;
    import 'package:path/path.dart' as path;
    
  3. C 함수의 FFI 타입 시그니처로 typedef를 만들어 주세요. dart:ffi 라이브러리에 따라 가장 많이 사용되는 타입에 대해 알아보려면 네이티브 타입과 상호작용(Interfacing with native types)을 참고해 주세요.
    typedef hello_world_func = ffi.Void Function();
    
  4. C 함수를 호출할 때 사용할 변수의 typedef를 만들어 주세요.
    typedef HelloWorld = void Function();
    
  5. 동적 라이브러리의 경로를 저장할 변수를 만들어 주세요.
    final String libraryPath;
    if (Platform.isMacOS) {
      libraryPath = path.join(
        Directory.current.path, 'hello_library', 'libhello.dylib',
      );
    } else if (Platform.isWindows) {
      libraryPath = path.join(
        Directory.current.path, 'hello_library', 'Debug', 'hello.dll',
      );
    } else {
      libraryPath = path.join(
        Directory.current.path, 'hello_library', 'libhello.so',
      );
    }
    
  6. C 함수가 들어 있는 동적 라이브러리를 열어 주세요.
    final dylib = ffi.DynamicLibrary.open(libraryPath);
    
  7. C 함수에 대한 참조를 얻어 변수에 넣어 주세요. 이 코드는 2, 3단계의 typedef와 4단계의 동적 라이브러리 변수를 사용해요.
    final HelloWorld hello = dylib
        .lookup<ffi.NativeFunction<hello_world_func>>('hello_world')
        .asFunction();
    
  8. C 함수를 호출해 주세요.
    hello();
    

hello_world 예제를 이해했다면 다른 dart:ffi 예제들도 확인해 주세요.

C 라이브러리 번들 및 로드

네이티브 C 라이브러리를 번들/패키징/배포한 다음 로드하는 방법은 플랫폼과 라이브러리 유형에 따라 달라요. 방법을 배우려면 다음 페이지와 예제를 참고해 주세요.

  • Flutter: Android 앱용 dart:ffi
  • Flutter: iOS 앱용 dart:ffi
  • Flutter: macOS 앱용 dart:ffi
  • dart:ffi 예제

네이티브 타입과 상호작용

dart:ffi 라이브러리는 NativeType을 구현하며 C의 네이티브 타입을 나타내는 여러 타입을 제공해요. 일부 네이티브 타입은 인스턴스화할 수 있어요. 다른 일부 네이티브 타입은 타입 시그니처에서 마커(marker)로만 사용할 수 있어요.

인스턴스화할 수 있는 타입 시그니처 마커 – 다음 네이티브 타입은 타입 시그니처에서 마커로 사용할 수 있어요. 이들 또는 그 서브타입은 Dart 코드에서 인스턴스화할 수 있어요.

Dart 타입 설명
Array 고정 크기의 항목 배열. 타입별 배열의 상위 타입
Pointer 네이티브 C 메모리를 가리키는 포인터
Struct 모든 FFI 구조체 타입의 상위 타입
Union 모든 FFI 공용체(union) 타입의 상위 타입

타입 시그니처 마커로만 제공되는 타입 – 다음 목록은 타입 시그니처에서 마커 역할을 하는 플랫폼에 구애받지 않는 네이티브 타입들이에요. 이들은 Dart 코드에서 인스턴스화할 수 없어요.

Dart 타입 설명
Bool C의 네이티브 bool을 나타냄
Double C의 네이티브 64비트 double을 나타냄
Float C의 네이티브 32비트 float을 나타냄
Int8 C의 네이티브 부호 있는 8비트 정수를 나타냄
Int16 C의 네이티브 부호 있는 16비트 정수를 나타냄
Int32 C의 네이티브 부호 있는 32비트 정수를 나타냄
Int64 C의 네이티브 부호 있는 64비트 정수를 나타냄
NativeFunction C의 함수 타입을 나타냄
Opaque C의 모든 불투명(opaque) 타입의 상위 타입
Uint8 C의 네이티브 부호 없는 8비트 정수를 나타냄
Uint16 C의 네이티브 부호 없는 16비트 정수를 나타냄
Uint32 C의 네이티브 부호 없는 32비트 정수를 나타냄
Uint64 C의 네이티브 부호 없는 64비트 정수를 나타냄
Void C의 void 타입을 나타냄

AbiSpecificInteger를 확장하는 ABI 특정 마커 네이티브 타입도 여럿 있어요. 이 타입들이 특정 플랫폼에서 어떻게 매핑되는지 알아보려면 아래 표에 연결된 API 문서를 참고해 주세요.

Dart 타입 설명
AbiSpecificInteger 모든 ABI 특정 정수 타입의 상위 타입
Int C의 int 타입을 나타냄
IntPtr C의 intptr_t 타입을 나타냄
Long C의 long int(long) 타입을 나타냄
LongLong C의 long long 타입을 나타냄
Short C의 short 타입을 나타냄
SignedChar C의 signed char 타입을 나타냄
Size C의 size_t 타입을 나타냄
UintPtr C의 uintptr_t 타입을 나타냄
UnsignedChar C의 unsigned char 타입을 나타냄
UnsignedInt C의 unsigned int 타입을 나타냄
UnsignedLong C의 unsigned long int 타입을 나타냄
UnsignedLongLong C의 unsigned long long 타입을 나타냄
UnsignedShort C의 unsigned short 타입을 나타냄
WChar C의 wchar_t 타입을 나타냄

package:ffigen으로 FFI 바인딩 생성

큰 API 표면에서는 C 코드와 통합되는 Dart 바인딩을 직접 작성하는 것이 시간이 많이 걸릴 수 있어요. C 헤더 파일에서 FFI 래퍼를 Dart가 만들게 하려면 package:ffigen 바인딩 생성기를 사용해 주세요.

Dart FFI에서 코드 에셋 지원에 대해 알아보려면 NativeDefaultAsset에 대한 dart:ffi API 참조를 확인해 주세요.

네이티브 코드 빌드 및 번들

Dart 빌드 훅(build hooks, 이전 명칭은 네이티브 에셋/native assets)은 패키지가 네이티브 코드 에셋을 포함하고, 이들이 투명하게 빌드되고 번들되어 런타임에 제공되도록 해요. 자세한 내용은 Hooks를 참고해 주세요.

더 알아보기