Flutter Mobile Development · 강의

dart:ffi로 C 라이브러리 호출하기

네이티브 공유 라이브러리에 연결하고 dart:ffi를 통해 구조체와 포인터를 마샬링합니다.

레슨 1/413개 단계

dart:ffi로 C 라이브러리 호출하기은(는) CoddyKit의 무료 Flutter Mobile Development 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Flutter Mobile Development 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why dart:ffi?

dart:ffi is Dart's Foreign Function Interface. It lets your Flutter app call functions in native C shared libraries (.so, .dylib, .dll, or the iOS process image) directly, with no platform-channel round-trip.

  • Synchronous by default and very low overhead, unlike MethodChannel which serializes messages across an async boundary.
  • Ideal for CPU-heavy code, existing C/C++/Rust libraries, and OS-level APIs (sqlite, libsodium, image codecs).
  • You bind a C signature to a Dart signature, then call it like an ordinary function.

The cost: you manage memory and types yourself. Get a pointer or a struct layout wrong and you crash the whole process.

Opening a DynamicLibrary

Everything starts with a DynamicLibrary. It is the handle to the loaded native code from which you look up symbols.

  • DynamicLibrary.open(path) loads a shared library by file name. On Android use 'libfoo.so'; on iOS/macOS code is usually statically linked, so use DynamicLibrary.process() or DynamicLibrary.executable().
  • Pick the right name per platform with Platform.isAndroid / Platform.isIOS.
import 'dart:ffi';
import 'dart:io' show Platform;

DynamicLibrary openNativeLib() {
  if (Platform.isAndroid) {
    return DynamicLibrary.open('libnative_math.so');
  }
  if (Platform.isIOS || Platform.isMacOS) {
    // Symbols are linked into the app process on iOS.
    return DynamicLibrary.process();
  }
  if (Platform.isWindows) {
    return DynamicLibrary.open('native_math.dll');
  }
  return DynamicLibrary.open('libnative_math.so');
}

Native types vs Dart types

FFI uses two type universes. The native type describes the C ABI; the Dart type is what your Dart code actually sees.

  • Int32, Int64, Uint8, Double, Float are native marker types — you never instantiate them, they map to Dart int/double.
  • Pointer<T> is a native address. Void marks no value.
  • The C function type is written with Function using native types; the Dart-facing type uses plain Dart types.

Example: C int32_t add(int32_t, int32_t) becomes native Int32 Function(Int32, Int32) and Dart int Function(int, int).

Looking up and calling a function

Use lookupFunction to bind a C symbol to a Dart function in one call. It takes two generic parameters: the native signature and the Dart signature.

  • The first type argument must use native types (Int32, Double, …).
  • The second is the callable Dart type returned to you.

Below, a pure-Dart simulation shows the call shape that FFI mirrors at runtime.

// Conceptually, FFI does this:
//   typedef NativeAdd = Int32 Function(Int32, Int32);
//   typedef DartAdd   = int Function(int, int);
//   final add = lib.lookupFunction<NativeAdd, DartAdd>('add');

// Pure-Dart stand-in so the call site is identical in shape:
int Function(int, int) bindAdd() {
  return (int a, int b) => a + b; // native impl returns a + b
}

void main() {
  final add = bindAdd();
  print('add(20, 22) = ${add(20, 22)}');
}

typedef for clean bindings

Real bindings declare the two signatures as typedefs. This keeps lookupFunction readable and lets you reuse signatures.

  • Native typedef uses native marker types and the suffix convention ...Native.
  • Dart typedef uses Dart types.
  • The string passed to lookupFunction is the exact exported C symbol name.
import 'dart:ffi';

// C: double native_pow(double base, int32_t exp);
typedef NativePowNative = Double Function(Double, Int32);
typedef NativePow = double Function(double, int);

class MathBindings {
  final DynamicLibrary lib;
  late final NativePow pow;

  MathBindings(this.lib) {
    pow = lib.lookupFunction<NativePowNative, NativePow>('native_pow');
  }
}

Allocating native memory

To pass pointers you must allocate native (off-heap) memory. The package:ffi library provides malloc (a calloc variant also exists) plus extensions for strings.

  • malloc<Int32>() returns a Pointer<Int32>; use .value to read/write.
  • malloc<Int32>(n) allocates an array of n elements; index with ptr[i] or ptr.elementAt(i).
  • You must free what you allocate with malloc.free(ptr) — the GC does not track native memory.
import 'dart:ffi';
import 'package:ffi/ffi.dart';

void usePointer() {
  final ptr = malloc<Int32>(3); // array of 3 int32
  try {
    ptr[0] = 10;
    ptr[1] = 20;
    ptr[2] = 12;
    var sum = 0;
    for (var i = 0; i < 3; i++) {
      sum += ptr[i];
    }
    print('sum = $sum');
  } finally {
    malloc.free(ptr); // always free
  }
}

Marshalling strings

C strings are null-terminated char*, represented as Pointer<Utf8> (from package:ffi). Conversion goes both ways:

  • Dart → C: myString.toNativeUtf8() allocates a native buffer (free it later).
  • C → Dart: ptr.toDartString() copies the bytes into a Dart String.

If the native function returns a pointer it allocated, you typically must call its matching free export — never malloc.free memory you did not allocate with malloc.

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

// C: int32_t count_chars(const char* text);
typedef CountNative = Int32 Function(Pointer<Utf8>);
typedef Count = int Function(Pointer<Utf8>);

int countChars(Count nativeCount, String text) {
  final cStr = text.toNativeUtf8();
  try {
    return nativeCount(cStr);
  } finally {
    malloc.free(cStr);
  }
}

Defining a Struct

To marshal C structs, declare a Dart class extending Struct. Each field is annotated with its native type so the FFI runtime computes the exact memory layout/offsets.

  • Scalar fields get annotations like @Int32(), @Double().
  • Field order and types must match the C struct exactly, including padding/alignment rules.
  • You never construct a Struct with new; you obtain one via a Pointer<T>.ref backed by native memory.
import 'dart:ffi';

// C:
// typedef struct { double x; double y; } Point;
final class Point extends Struct {
  @Double()
  external double x;

  @Double()
  external double y;
}

Passing structs by pointer

Most C APIs take a Point*. Allocate the struct, fill it through .ref, pass the pointer, then read results back.

  • malloc<Point>() gives a Pointer<Point> sized correctly for the layout.
  • ptr.ref is a view onto that native memory; writing ptr.ref.x = 3.0 mutates the C struct in place.
  • The native function reads/writes the same memory — this is how you get values out by reference.
import 'dart:ffi';
import 'package:ffi/ffi.dart';

// C: void translate(Point* p, double dx, double dy);
typedef TranslateNative = Void Function(Pointer<Point>, Double, Double);
typedef Translate = void Function(Pointer<Point>, double, double);

final class Point extends Struct {
  @Double()
  external double x;
  @Double()
  external double y;
}

void moveOrigin(Translate translate) {
  final p = malloc<Point>();
  try {
    p.ref.x = 0;
    p.ref.y = 0;
    translate(p, 4.0, 5.0);
    print('moved to (${p.ref.x}, ${p.ref.y})');
  } finally {
    malloc.free(p);
  }
}

Don't block the UI thread

FFI calls are synchronous: they run on the calling isolate's thread. A long native computation called from the main isolate freezes Flutter's UI.

  • For heavy work, run the FFI call inside an Isolate (e.g. Isolate.run on modern Dart) or a worker isolate.
  • Note: a DynamicLibrary handle and native pointers can be passed between isolates as addresses, but each isolate must re-open or share carefully — treat pointers as plain integers across boundaries.
  • Native code that calls back into Dart must use NativeCallable / send ports, not arbitrary threads.
import 'dart:isolate';

// Simulates offloading a heavy native FFI computation off the UI thread.
int _heavyNativeWork(int n) {
  var acc = 0;
  for (var i = 0; i < n; i++) {
    acc = (acc + i) % 1000003;
  }
  return acc;
}

Future<void> main() async {
  final result = await Isolate.run(() => _heavyNativeWork(5000000));
  print('result = $result');
}

Memory safety and ownership

FFI bugs are process crashes, not exceptions. Discipline matters:

  • Ownership: whoever allocates must free. Memory from malloc → malloc.free. Memory from a C library → that library's destructor export.
  • Wrap allocate/use/free in try/finally so you free even on error.
  • For long-lived native objects, attach a NativeFinalizer so the destructor runs when the Dart wrapper is GC'd.
  • Never read .ref/.value on a pointer after it is freed — that is a use-after-free.
import 'dart:ffi';
import 'package:ffi/ffi.dart';

class SafeBuffer {
  final Pointer<Uint8> ptr;
  final int length;
  SafeBuffer(this.length) : ptr = malloc<Uint8>(length);

  void dispose() => malloc.free(ptr);
}

void main() {
  final buf = SafeBuffer(16);
  try {
    buf.ptr[0] = 255;
    print('first byte = ${buf.ptr[0]}');
  } finally {
    buf.dispose();
  }
}

Quick Check

Answer based on dart:ffi struct and memory rules.

Recap

You can now bind to native C libraries from Flutter with dart:ffi:

  • Load code with DynamicLibrary.open / .process(), choosing the path per platform.
  • Bind symbols with lookupFunction<Native, Dart>, declaring native vs Dart typedefs.
  • Allocate off-heap memory with malloc, marshal strings via toNativeUtf8 / toDartString, and pass arrays as pointers.
  • Define structs by extending Struct with native-type annotations; pass them by pointer and read results through .ref.
  • Keep heavy calls off the UI isolate, and enforce strict ownership: free what you allocate, use try/finally and NativeFinalizer, and never touch freed pointers.

FFI trades safety for speed and reach — correct types, layout, and lifetimes are entirely your responsibility.

무료로 시작

AI 튜터와 함께 Dart을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
22
레슨
88

자주 묻는 질문

“dart:ffi로 C 라이브러리 호출하기” 강의는 무료인가요?

네 — “dart:ffi로 C 라이브러리 호출하기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Flutter Mobile Development 강의 전체를 잠금 해제할 수 있습니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.

“dart:ffi로 C 라이브러리 호출하기”에서 뭘 배우나요?

네이티브 공유 라이브러리에 연결하고 dart:ffi를 통해 구조체와 포인터를 마샬링합니다. 브라우저에서 직접 실행하는 실습 코드로 Flutter Mobile Development을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Flutter Mobile Development을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Flutter Mobile Development은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“dart:ffi로 C 라이브러리 호출하기” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Flutter Mobile Development 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Flutter Mobile Development 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. dart:ffi로 C 라이브러리 호출하기
  2. Pigeon을 활용한 타입 안전 플랫폼 채널
  3. iOS 및 Android용 사용자 지정 플랫폼 플러그인 작성
  4. 백그라운드 아이솔레이트 및 네이티브 메모리 관리
← Flutter Mobile Development(으)로 돌아가기