jnigen & swiftgen: 네이티브 인터롭, 이제 두렵지 않다!
jnigen과 swiftgen을 사용한 네이티브 인터롭(Native Interop) 경험을 공유하며, Android 및 iOS 바인딩 생성에 대한 노하우를 제공
jni@1.0.0으로의 마이그레이션 과정에서 겪은 API 변경 사항과 바인딩 설정 방식 변화를 설명
콜백(Callback) 처리, 메모리 관리, 플랫폼 간 API 통합 등 네이티브 인터롭 구현 시 필요한 기술적 고려 사항 제시
jnigen과 swiftgen의 제너레이터 스크립트(Generator Script) 예시를 제공하여, 실제 구현에 필요한 코드 구조를 제시
swiftgen의 ObjC 호환 Swift 코드 작성 및 FFI(Foreign Function Interface) 설정에 대한 가이드라인 제시
jnigen과 swiftgen: 바인딩 생성 과정
본문에서는 jnigen과 swiftgen을 사용하여 Dart 바인딩(Dart Bindings)을 생성하는 과정을 설명한다. jnigen은 Java/Kotlin 코드를 기반으로 Dart 바인딩을 생성하며, swiftgen은 Swift 코드를 Objective-C 호환 형태로 변환하여 Dart 바인딩을 생성한다.
jnigen: Android 프로젝트 빌드(Android Project Build) 후, jnigen.dart 스크립트를 실행하여 바인딩 생성
swiftgen: Objective-C 호환 Swift 코드를 작성하고, swiftgen.dart 스크립트를 통해 FFI(Foreign Function Interface) 설정을 구성
핵심: 각 플랫폼에 맞는 제너레이터 스크립트(Generator Script)를 사용하여 바인딩 생성 과정을 자동화하고, 코드 생성 전후에 추가 로직을 삽입하여 유연성을 확보
jni@1.0.0 마이그레이션 시 주의사항
jni@0.14/0.15에서 jni@1.0.0으로의 마이그레이션 과정에서 API 변경(API Changes)으로 인한 추가 작업이 필요하다. 특히, 메서드명 변경(toDartString() -> toString())과 생성자 오버라이드(Constructor Override) 관련 변경 사항에 유의해야 한다.
바인딩 재 생성: 마이그레이션 시, 네이티브 통합 코드(Native Integration Code)를 주석 처리 후 Android 프로젝트를 다시 빌드하고 바인딩을 재생성하는 것이 효율적
Dart 스크립트 기반 설정: yaml 파일 대신 Dart 스크립트를 사용하여 바인딩 설정을 정의하는 새로운 방식은 코드 생성 과정(Code Generation Process)을 숨기고, 전처리/후처리 로직을 추가할 수 있는 유연성을 제공
팁: Gradle 캐시(Gradle Cache) 문제로 인해 바인딩 생성 오류 발생 시, 캐시를 비우고 다시 빌드
콜백(Callback) 처리 및 비동기 호출
네이티브 코드에서 Dart로 데이터를 전달하기 위해 콜백 인터페이스(Callback Interface)를 정의하고, Dart 측에서 StreamController를 사용하여 사용자 친화적인 API를 구현한다. jnigen은 type-safe implement() 메서드를, swiftgen은 implementAsListener() 메서드를 제공한다.
jnigen: Kotlin 인터페이스를 정의하고, $Mixin을 통해 implement() 메서드 생성
swiftgen: @objc 프로토콜(Protocol)을 정의하고, implementAsListener()를 사용하여 비동기 콜백(Asynchronous Callback) 구현
비동기 콜백: observer/notification 패턴의 경우, implementAsListener()를 사용하여 ObjC 호출자가 즉시 반환되도록 구현
메모리 관리 및 객체 해제
jni 바인딩 사용 시, 네이티브 Java 객체(Native Java Objects)의 메모리 관리에 주의해야 한다. 객체에 대한 모든 참조가 사라지면 Java GC(Garbage Collector)가 해당 객체를 회수한다. Dart 측에서 JNI global reference를 수동으로 해제하려면 .release()를 호출해야 한다.
jnigen: JNI global reference를 수동으로 해제하기 위해 .release() 메서드 사용
swiftgen: ARC(Automatic Reference Counting)를 통해 메모리 자동 관리
중요: Dart 측에서 콜백 객체에 대한 참조를 유지하여 Dart GC에 의해 콜백이 수집되는 것을 방지
플랫폼 간 API 통합 전략
플랫폼 간 API의 일관성을 유지하기 위해 추상 클래스(Abstract Class)와 팩토리 생성자(Factory Constructor)를 활용한다. 각 플랫폼별로 생성된 바인딩을 임포트하고, Platform.isAndroid 또는 Platform.isIOS를 사용하여 런타임에 적절한 구현체를 반환한다.
추상 클래스: 공통 API(Common API)를 정의하고, 플랫폼별 구현체를 캡슐화
팩토리 생성자: 런타임에 플랫폼에 맞는 구현체(Platform-Specific Implementation)를 반환
장점: 플랫폼별 코드를 분리하여 관리하고, API 일관성(API Consistency)을 유지하며, 코드 중복을 줄임
swiftgen 사용 시 Objective-C 호환성
swiftgen을 사용하여 iOS 바인딩을 생성할 때, Swift 코드는 Objective-C와 호환되어야 한다. 모든 Dart에 노출되는 타입은 @objc 어노테이션(Annotation)을 사용하고, NSObject를 상속해야 한다.
@objc 어노테이션: Dart에 노출되는 모든 타입(Types)에 적용
NSObject 상속: 클래스는 NSObject를 상속하고, init() 메서드를 override하여 super.init()을 호출
제약 사항: Swift struct, enum with associated values, generics는 지원하지 않으며, Objective-C 호환 타입(Compatible Types)만 사용 가능
ffigen include filters: 불필요한 바인딩 생성을 막기 위해 include filters를 사용하여 필요한 타입만 선택