본문으로 바로가기
반응형

fultter에서 dll 파일을 로드하려면 두가지 방법이 존재합니다.

이전 포스팅에서 사용했던 method channel을 사용하는 방법, ffi plugin을 사용하는 방법인데, 전자의 경우 구현하기가 까다롭습니다.

mac os에서는 method channel을 통하여 xcFramework의 함수를 호출하기 위해서 swift 코드에 호출할 함수를 맵핑시켜 주는 코드를 아주 조금만 수정하면 가능했으나, windows dll의 경우 C++ 언어로 이 작업을 해야 합니다.

kmp 는 kotlin으로

Mac용 flutter에 로드하기 위해서 swift

Windows용 flutter에 로드하기 위해서 C++

이 두 OS에서 맵핑된 함수를 dart로 뽑아내기 위해서는 dart로..

따라서 사용해야 하는 언어가 너무 많아지고 복잡해 지므로 여기서는 flutter plugin중 하나의 ffi를 사용합니다.

FFI 설정

flutter plugin의 pubspec.yaml에 아래와 같이 ffi 를 추가합니다.

...

dependencies:
  flutter:
    sdk: flutter
  ffi: ^2.1.3
  ...

...

생성했던 dll 파일을 plugin 프로젝트의 windows 폴더 아래 복사합니다. 단 주의할 점은 plugin 프로젝트에서 이를 실행시키이 위한 example/windows 폴더도 존재합니다. 여기에 복사하지 않됩니다.

"프로젝트명/windows/MyLib.dll" 의 경로에 복사되어야 합니다.

Library Load

라이브러리의 로드는 open 함수 바로 수행할 수 있습니다.

    DynamicLibrary dyLib = DynamicLibrary.open('../windows/MyLib.dll');

 

다만 library를 로드할때 singleton으로 만들어 한번만 로드하여 사용하도록 아래와 같이 만듭니다.

//load_libaray.dart

import 'dart:ffi';
import 'dart:io';

/// Library load를 처리
class LoadLibrary {
  LoadLibrary._privateConstructor();

  static final LoadLibrary _instance = LoadLibrary._privateConstructor();

  factory LoadLibrary() {
    return _instance;
  }

  DynamicLibrary? loadLib() {
    print("current Directory: ${Directory.current.path}");

    DynamicLibrary dyLib;

    try {
      if (Platform.isWindows) {
        dyLib = DynamicLibrary.open('../windows/MyLib.dll');
      } else {
        print("Not supported OS");
        return null;
      }
      print("load lib success");
    } catch (e) {
      print('F11ailed to load DLL: $e');
      return null;
    }

    return dyLib;
  }
}

dll을 파일을 정확한 위치에 잘 복사했다면 load 성공 메시지를 확인할 수 있습니다.

Libarary 함수 호출

함수 호출시 호출할 라이브러리 함수의 signature와 이를 maping할 dart 함수의 signature를 선언합니다.

@OptIn(ExperimentalNativeApi::class)
@CName("inputTest")
fun inputTest(name: String): Int {
    println("inputTest() - name:$name")
    return 100
}

 이 함수가 kmp로 선언했던 함수입니다. 이 함수가 dll로 컴파일되면 String같은 경우 char*로 변경되면서 pointer로 접근해야 합니다. (참고로 kmp 빌드시 dll과 함께 header파일이 같이 제공 됩니다. 실제로는 dll 파일만사용하지만 header 파일을 열어 extern c로 구성된 함수의 함수 signature를 확인할 수 있습니다.)

typedef InputTestNative = Int32 Function(Pointer<Utf8> name); //dll 함수 타입
typedef InputTestDart = int Function(Pointer<Utf8> name);     //dart 함수 타입

함수를 호출할때는 lookup 함수를  사용합니다.

// library에서 inputTest() 함수 검색
final InputTestDart inputTest = dyLib
         .lookup<NativeFunction<InputTestNative>>("inputTest")
         .asFunction();

// 입력값 pointer로 변환
final paramPointer = "홍길동".toNativeUtf8();

// 함수 호출
final int result = inputTest(paramPointer);

// param으로 만들었던 pointer 해제
calloc.free(paramPointer);

print('result: $result');

param으로 넘겨야 하는 값이 char* 이기 때문에 pointer를 만들어 담아서 dll 함수를호출하고, 사용 이후에 반드시 free로 해제해야 합니다.

실제 전체 함수를 보면 아래와 같습니다.

import 'dart:ffi';
import 'package:ffi/ffi.dart';
import '../load_library.dart';
import 'dart:io' show Directory, Platform;

typedef InputTestNative = Int32 Function(Pointer<Utf8> name); //dll 함수 타입
typedef InputTestDart = int Function(Pointer<Utf8> name);     //dart 함수 타입

class TestCall {
  static final TAG = 'TestCall';
  final loadedLibrary = LoadLibrary();
  final nativeFunctionName = 'inputTest';

  Future<String?> invoke() {
    print('current Directory: ${Directory.current.path}');
    print('requestTest: $requestTest');

    int? result;
    Pointer<Utf8>? paramPointer;
    try {
      // library 로드
      final dyLib = loadedLibrary.loadLib();
      if (dyLib != null) {
          // 함수 호출
          InputTestDart inputTest = dyLib
            .lookup<NativeFunction<InputTestNative>>(nativeFunctionName)
            .asFunction();

          // param 준비
          final paramPointer = "내 이름은 홍길동".toNativeUtf8();
          result = inputTest(paramPointer);
          print('result: $result');
      }      
    } catch (e) {
      print('Failed to load DLL: $e');
    } finally {
      //dart에서 선언한 pointer는 dart에서 해제해야 한다.
      if (paramPointer != null) {
        calloc.free(paramPointer);
      }
    }
    return Future.value(result ?? "Failed to call inputTest() Native function");
  }
}

 

Pointer를 사용하는 추가 예제

@OptIn(ExperimentalNativeApi::class)
@CName("returnParam")
fun returnParam(name: String): String {
    println("inputTest() - name:$name")
    return name
}

만약 위와 같이 String을 param으로 받고 String을 반환하는 함수라면 아래와 같이 선언하고 호출해야 합니다.

호출 코드

typedef ReturnParamNative = Pointer<Utf8> Function(Pointer<Utf8> name); //dll 함수 타입
typedef ReturnParamDart = Pointer<Utf8> Function(Pointer<Utf8> name);   //dart 함수 타입

// library에서 inputTest() 함수 검색
final ReturnParamDart inputTest = dyLib
         .lookup<NativeFunction<ReturnParamNative>>("returnParam")
         .asFunction();

// 입력값 pointer로 변환
final paramPointer = "임꺽정".toNativeUtf8();

// 함수 호출
Pointer<Utf8>? name = inputTest(paramPointer);
String result = name.toDartString(); // dart string으로 변환

// param으로 만들었던 pointer 해제
calloc.free(paramPointer);

print('result: $result');

여기서 주위할 점은 함수의 return값이 pointer 이므로 이를 재변환하는 toDartString() 함수를 사용한다는점 입니다.

 

반응형