Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using @Native/@FfiNative in an FFI plugin on Android #923

Open
virtualzeta opened this issue Aug 12, 2023 · 12 comments
Open

Using @Native/@FfiNative in an FFI plugin on Android #923

virtualzeta opened this issue Aug 12, 2023 · 12 comments

Comments

@virtualzeta
Copy link

virtualzeta commented Aug 12, 2023

I'm struggling with declaring and loading native libraries but something is wrong on Android.

Library load function with DynamicLibrary.open with which there are no problems.

String libraryNameOrigin = "native";
String libraryName = "lib${libraryNameOrigin}.so";
String libraryPathTest1 = "";
String libraryPathCompleteTest1 = platformPath(libraryName, path: libraryPathTest1);

print(File(libraryPathCompleteTest1).existsSync()); // true
print(DynamicLibrary.process().providesSymbol("dlopen")); // true
print(DynamicLibrary.executable().providesSymbol("dlopen")); // true

DynamicLibrary library = DynamicLibrary.open(libraryPathCompleteTest1); // handler with correct address

Library load function with dlopen native function that there are problems with.

const RTLD_LAZY = 0x00001;
const RTLD_LOCAL = 0x00000;
const RTLD_GLOBAL = 0x00100;
String libraryNameOrigin = "native";
String libraryName = "lib${libraryNameOrigin}.so";
String libraryPathTest2 = "/data/user/0/com.example.<app_name>/lib";
String libraryPathCompleteTest2 = platformPath(libraryName, path: libraryPathTest2);

print(File(libraryPathCompleteTest2).existsSync()); // true
print(DynamicLibrary.process().providesSymbol("dlopen")); // true
print(DynamicLibrary.executable().providesSymbol("dlopen")); // true
     Pointer<Void> libraryHandle = using((arena) {
       final libraryHandle = dlopen(
           platformPath(name, path: path).toNativeUtf8(allocator: arena).cast(),
           RTLD_LAZY | RTLD_GLOBAL);
       return libraryHandle;
     }); // handler with address 0x0

No errors but the handle has an empty pointer and is not usable to use symbols of the library.

Types of declarations used in different tests without success (in addition to others).

@FfiNative<Pointer<Void> Function(Pointer<Char>, Int)>("dlopen")
external Pointer<Void> dlopen(Pointer<Char> filename, int flags);
final Pointer<Void> Function(Pointer<Char>, int) dlopen =
     DynamicLibrary.process()
         .lookup<NativeFunction<Pointer<Void> Function(Pointer<Char>, Int)>>(
             "dlope")
         .asFunction();

Same thing also for function release with dlclose native function.

     print(DynamicLibrary.process().providesSymbol("dlclose")); // true
     print(DynamicLibrary.executable().providesSymbol("dlclose")); // true
     final int Function(Pointer<Void>) dlclose = DynamicLibrary.process()
         .lookup<NativeFunction<Int32 Function(Pointer<Void>)>>("dlclose")
         .asFunction();
     int result = dlclose(library.handle);
     print(result); // 0 but library (successfully loaded with DynamicLibrary.open) is not released

Types of declarations used in different tests without success (in addition to others).

@FfiNative<Int Function(Pointer<Void>)>("dlclose")
external int dlclose(Pointer<Void> handle);
final int Function(Pointer<Void>) dlclose = DynamicLibrary.process()
     .lookup<NativeFunction<Int32 Function(Pointer<Void>)>>("dlclose")
     .asFunction();

I have to be able to use the @FfiNative and external declaration, but since I couldn't I also tried with lookup noting that it doesn't change anything.

If it serves as information and if it impacts the flow, programming in C dlopen works fine in my system (after fixing it because the dlfcn.c and dlfcn.h files were missing).

So, why?

SDK: Flutter 3.7.3 • stable channel • Dart 2.19.2
Workspace: Flutter
Target platform: Android
Environment OS: Windows 10

@virtualzeta
Copy link
Author

virtualzeta commented Aug 13, 2023

OK, I found that as far as dlopen is concerned everything is working fine and that's why I'm not getting any errors BUT sending the flag value RTLD_LAZY | RTLD_GLOBAL or only RTLD_GLOBAL returns the handle with zeroed address, but if I just use RTLD_LAZY (or RTLD_LOCAL or RTLD_LAZY | RTLD_LOCAL) then I get the address correctly.

Unfortunately though the reason I'm using dlopen directly as it works natively and not DynamicLibrary.open is because I really mean that the library symbols need to be global to be seen and used later.

If, for example, I have a native add function inside the library after running dlopen the result is this:

print(DynamicLibrary.process().providesSymbol("add")); // false
print(DynamicLibrary.executable().providesSymbol("add")); // false

So I have to figure out how to make that request without having this behavior and have the global symbols as per goal.

Indeed, I would like to be able to retrieve them by declaring native functions with @FfiNative and external like this:

@ffi.FfiNative<ffi.Int Function(ffi.Int, ffi.Int)>('add')
external int add(
    int to,
    int b,
);

But this only works if the symbols in the library are global.

Considering what I've read it would seem that Linux and Android (among other systems) differ on this loading aspect because Android would load dynamic libraries using dlopen with RTLD_LOCAL flag. While the main executable and the
LD_PRELOAD libraries are flagged with RTLD_GLOBAL.

And it is probably for this reason that in this comment a test made with Linux is proposed which works and after having used dlopen with RTLD_GLOBAL flag the symbols can be seen with DynamicLibrary.process.

I would also like to find a way to put the received handle from dlopen inside a variable library (as using DynamicLibrary.open) to be able to use it, if desired, with a lookup, but as far as I've seen the handle from a DynamicLibrary instance it can only be requested and not set.

It seems to me that in this comment this was proposed.

SDK: Flutter 3.7.3 • stable channel • Dart 2.19.2
Workspace: Flutter
Target platform: Android
Environment OS: Windows 10

@virtualzeta
Copy link
Author

Considering that in the same Andorid folder of the libraries, in addition to the custom native library lib<library_name>.so there is also libflutter.so and whose symbols seem to be all globally loaded (despite the problem described in the previous comment) I wonder what the loading difference of the two.

If I could initialize the custom library with flutter's one the problem would be skipped.

@dcharkes
Copy link
Collaborator

String libraryPathTest2 = "/data/user/0/com.example.<app_name>/lib";
String libraryPathCompleteTest2 = platformPath(libraryName, path: libraryPathTest2);

I believe dlopen just wants the dynamic library name on Android, not the path. The dynamic library should be bundled in the the final app in the folder where dlopen will look for it. If you have problems bundling, please try to use flutter create --template=plugin_ffi, it has a setup for bundling the dylib in the Flutter app.

@virtualzeta
Copy link
Author

virtualzeta commented Aug 16, 2023

I believe dlopen just wants the dynamic library name on Android, not the path. The dynamic library should be bundled in the the final app in the folder where dlopen will look for it.

I started the tests using the library name without path with dlopen but when I saw that the handle was returned empty (with RTLD_GLOBAL flag) I searched and used the full path as per your comment mentioned example.

I tried now again with no path but the result is the same. I only get the correct handle without using RTLD_GLOBAL which is the opposite of what I want to get.

The library is found both by dlopen and by DynamicLibrary.open, both with and without a path. In the case of loading with DynamicLibrary.open I see the symbols in the loaded library but not globally.

In the end the difference with and without path is reduced to the fact that in the first case existsSync of which exists and in the second no.

@virtualzeta
Copy link
Author

virtualzeta commented Aug 16, 2023

If you have problems bundling, please try to use flutter create --template=plugin_ffi, it has a setup for bundling the dylib in the Flutter app.

So...
I really appreciate having comments from Dart/Flutter contributors, but I think sometimes you read quickly what is written to give an answer as fast as possible.

Thanks again for your availability I believe I have indicated in a precise manner what I want to obtain and patiently all the operations carried out and explained reasoning about them but it may not be clear yet, maybe because of English which is not my language.
In this post there is more information about the initial process of understanding that led to the current post.

Creating a Dart project for using the ffi library using templates is certainly useful if you don't already have a basis to start from, which in this case I already have, and unless I'm mistaken there isn't much to it.

In particular:

  • I have a .cpp file with native functions;
  • I have a .h file with headers;
  • I have a Dart file with function main;
  • I have a Dart file with binding/declaration native function;
  • I have a dart file where I manage the library call where I also have with declaration dlopen;
  • I have a cMakeLists.txt file to create .so libraries for each Android ABI;
  • I can link the library either with DynamicLibrary.open or directly with dlopen;
  • I can use native functions using lookup on library loaded with DynamicLibrary.open;

I miss something? Yes.
What? My test goal: being able to use external native functions with @FfiNative declaration in Android (therefore with Flutter framework, because with Dart it already works since it follows my Windows operating system which provides everything globally).

Does it help me to use flutter create --template=plugin_ffi? No.
Why? Because the binding automatically generated by ffigen is managed with lookup and I can already use it effectively (and that's not what I want) and also because DynamicLibrary.open is used which I'm purposely not using in favor of dlopen to try to solve the fact that with the former you can't specify flags.

I've decided to paste all my files here so if anyone wants to use them as a test bed and figure out how to use dlopen with the RTLD_GLOBAL flag in Android without getting a 0x0 handle I'd really appreciate it.
As I said, using the absolute path or not using it at all gives the same result. I can only get a proper handle without pretending to use RTLD_GLOBAL flags.

lib/main.dart

import 'dart:io';
import 'dart:ffi';
import 'package:path_provider/path_provider.dart';
import 'dlopen.dart';
import 'native.dart';

Future<String> getLocalPath(String? typePath) async {
  dynamic folder;
  typePath ??= "";
  if (typePath.isNotEmpty) {
    try {
      switch (typePath) {
        case "actual":
          folder = Directory.current;
          break;
        case "system":
          folder = Directory.systemTemp;
          break;
        case "main":
          folder = Directory.systemTemp.parent;
          break;
        case "documents":
          folder = await getApplicationDocumentsDirectory();
          break;
        case "temp":
          folder = await getTemporaryDirectory();
          break;
        case "libraries":
          if (Platform.isAndroid) {
            folder = Directory(Directory.systemTemp.parent.path + "/lib");
          } else {
            folder = await getLibraryDirectory();
          }
          break;
        case "download":
          folder = await getDownloadsDirectory();
          break;
        case "appSupprt":
          folder = await getApplicationSupportDirectory();
          break;
        case "appCache":
          folder = await getApplicationCacheDirectory();
          break;
        case "externalStorage":
          folder = await getExternalStorageDirectory();
          break;
        case "externalCache":
          folder = await getExternalStorageDirectory();
          break;
        default:
          break;
      }
    } catch (errore) {}
  }
  String? folderPath;
  if (folder != null) {
    try {
      folderPath = folder.path;
    } catch (errore) {}
  }
  folderPath ??= "";
  return folderPath;
}

void main() async {
  String libraryName = "native";
  String folderPath = await getLocalPath("libraries");
  print(Directory(folderPath).existsSync());
  dlopenGlobalPlatformSpecific(libraryName, path: folderPath);
  print(DynamicLibrary.process().providesSymbol("add"));
  print(DynamicLibrary.executable().providesSymbol("add"));
  try {
    print(add(20, 4));
  } catch (error) {
    print(error.toString());
  }
}

lib/dlopen.dart

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

const RTLD_DEFAULT = 0xffffffff;
const RTLD_LOCAL = 0x00000;

/// On Linux and Android.
const RTLD_LAZY = 0x00001;

/// On Android Arm.
const RTLD_GLOBAL_android_arm32 = 0x00002;

/// On Linux and Android Arm64.
const RTLD_GLOBAL_rest = 0x00100;
final RTLD_GLOBAL = Abi.current() == Abi.androidArm
    ? RTLD_GLOBAL_android_arm32
    : RTLD_GLOBAL_rest;

@FfiNative<Pointer<Void> Function(Pointer<Char>, Int)>("dlopen")
external Pointer<Void> dlopen(Pointer<Char> filename, int flags);

@FfiNative<Pointer<Void> Function(Pointer<Void>, Pointer<Char>)>("dlsym")
external Pointer<Void> dlsym(Pointer<Void> filename, Pointer<Char> name);

Object? dlopenGlobalPlatformSpecific(String libraryName, {String? path}) {
  String libraryNameComplete = "lib${libraryName}.so";
  String libraryPathComplete = platformPath(libraryNameComplete, path: path);
  print(File(libraryPathComplete).existsSync());
  Pointer<Void> libraryHandle = using((arena) {
    final libraryHandle = dlopen(
        libraryPathComplete.toNativeUtf8(allocator: arena).cast(), RTLD_LAZY);
    return libraryHandle;
  });
  return null;
}

String platformPath(String name, {String? path}) {
  path ??= "";
  if (path.isNotEmpty) {
    if (path.substring(path.length - 1) != "/") {
      path = "$path/";
    }
  }
  String completePath = "$path$name";
  return completePath;
}

lib/native.dart

// Generated by `package:ffigen`.
import 'dart:ffi' as ffi;

@ffi.FfiNative<ffi.Int Function(ffi.Int, ffi.Int)>('add')
external int add(
  int a,
  int b,
);

@ffi.FfiNative<ffi.Int Function(ffi.Int, ffi.Int)>('subtract')
external int subtract(
  int a,
  int b,
);

lib/native/native.cpp

#define EXPORT extern "C" __attribute__((visibility("default")))__attribute__((used))

EXPORT
int add(int a, int b) {
    return a + b;
}

EXPORT
int subtract(int a, int b) {
    return a - b;
}

int main() {
    return 0;
}

lib/native/native.h

int add(int a, int b);

int subtract(int a, int b);

lib/native/CMakeLists.txt

cmake_minimum_required(VERSION 3.18)
project(native)
set(SOURCES ./${PROJECT_NAME}.cpp ./${PROJECT_NAME}.h)
include_directories(./)
add_library( 
    ${PROJECT_NAME}
    SHARED
    ${SOURCES}
)
set_target_properties(${PROJECT_NAME} PROPERTIES 
	PUBLIC_HEADER ./${PROJECT_NAME}.h
	OUTPUT_NAME ${PROJECT_NAME}
)
target_compile_definitions(${PROJECT_NAME} PUBLIC DART_SHARED_LIB)

android/app/build.gradle
Inside android {}

    externalNativeBuild {
        cmake {
            path "../../lib/native/CMakeLists.txt"
            version 3.18
        }
    }

That's all.

Thank you.

@dcharkes
Copy link
Collaborator

dcharkes commented Aug 16, 2023

Does it help me to use flutter create --template=plugin_ffi? No.
Why? Because the binding automatically generated by ffigen is managed with lookup and I can already use it effectively (and that's not what I want) and also because DynamicLibrary.open is used which I'm purposely not using in favor of dlopen to try to solve the fact that with the former you can't specify flags.

I didn't mean to use that to not use @FfiNatives. I meant it to get the bundling to work. If you use that as a starting point.

Using the @FfiNatives on Android works for me locally. Using dlopen with GLOBAL is really a workaround for missing support for flutter/flutter#129757, can you use DynamicLibrary.open in the mean-time?

My test goal: being able to use external native functions with @FfiNative declaration in Android

Is there a particular reason DynamicLibrary.open does not work for your use case?

Creating a Dart project for using the ffi library using templates is certainly useful if you don't already have a basis to start from, which in this case I already have, and unless I'm mistaken there isn't much to it.

This is problematic, because of the bundling. Your Flutter apps will not have the dynamic libraries inside the final app bundle unless you start modifying the build files manually (Gradle for Android). This is why we have the template. flutter/flutter#129757 should make it more feasible to just tell that you have a dynamic library and then Flutter will take care of bundling it.

@virtualzeta
Copy link
Author

virtualzeta commented Aug 16, 2023

I didn't mean to use that to not use @FfiNatives. I meant it to get the bundling to work. If you use that as a starting point.

OK, thanks anyway for that.

This is problematic, because of the bundling. Your Flutter apps will not have the dynamic libraries inside the final app bundle unless you start modifying the build files manually (Gradle for Android). This is why we have the template. flutter/flutter#129757 should make it more feasible to just tell that you have a dynamic library and then Flutter will take care of bundling it.

I understand and I agree that the template makes things easier, BUT, apart from that I always like to do the most complicated things, after having seen (days before your advice) that that solution didn't bring any advantage to what I wanted to do so I discarded it and as I said above, also pasting the piece of code, I have already modified the build.gradle file and the libraries are correctly in place for each ABI. I haven't done a release version and I'm in debug and unless that makes a difference the external library is there. Instead of why IF it was NOT present:

  • how could Dart tell me that the file exists?
  • how could DynamicLibrary and dlopen hook it using an absolute path?

I also paste a screenshot to show that the library exists.

image

image

In another post I also pointed out that it was together with libflutter.so that on the other hand of my library, in addition to having the symbols available in it, I also saw them in process as global. So this point to me just takes us away from the heart of the matter.

Using dlopen with GLOBAL is really a workaround for missing support for flutter/flutter#129757, can you use DynamicLibrary.open in the mean-time?

Let's recap.

  • I want to declare functions as external and with @FFiNative annotation;
  • If I open the library with DynamicLibrary.open I can call the function IF it is present as a global symbol and I see it in DynamicLibrary.process, therefore for example with Windows target platform.
    Do I need Windows? NO: Android.
  • How can I then give so that the symbols are present in process?
    With the beautiful workaround suggested by @dcharkes using dlopen directly with RTLD_GLOBAL flag.
    Did I like the tip? Much! :-D
    But it works? Nope. :-(
    Why? Because I can only get the handle if I call dlopen without RTLD_GLOBAL.
    So what's the advantage of using it instead of DynamicLibrary if I have even just one handle without being able to use it in a library? Nobody!
  • I would be fine with using DynamicLibrary.open instead of dlopen? Sure, but I'm using it precisely because with the first method I can't get the global symbols (and unfortunately the problem is that I can't even with the second one).
  • If I use lookup with library from DynamicLibrary.open, instead of @FfiNative, can I use the native function? Yes, but I'm not interested in doing it.

Is there a particular reason DynamicLibrary.open does not work for your use case?

But of course there is! :-)
Because in the study of ffi use cases this is the last one I need to get working and it's also the one I would most like to be able to use if necessary (besides doing the tests above) because I find that the external + @FfiNative (or @Native if the SDK version allows) is much more practical if I want to declare that function in a class or otherwise in a file. It's nice clean, professional code and requires fewer lines of code than a lookup.

Using the @FfiNatives on Android works for me locally.

OK, wait! This is all I want! A river of writings for nothing!? So, how? Please!

@dcharkes
Copy link
Collaborator

Using the @FfiNatives on Android works for me locally.

OK, wait! This is all I want! A river of writings for nothing!? So, how? Please!

We run tests with this hack on our unit tests on the Dart CI. For example this test is using the workaround on Android devices: https://dart-current-results.web.app/#/filter=ffi/function_structs_by_value_generated_ret_native_test&showAll.

I'll take some time to apply my own suggestions to flutter create --template=plugin_ffi to show an example.

is much more practical if I want to declare that function in a class or otherwise in a file. It's nice clean, professional code and requires fewer lines of code than a lookup.

Agreed, @Native external functions are indeed cleaner syntax. That's why we are working on the "native assets" feature to make them available everywhere. Right now they are not fully supported and we have to use these kind of workarounds to make things available in the process lookup.

@virtualzeta
Copy link
Author

We run tests with this hack on our unit tests on the Dart CI. For example this test is using the workaround on Android devices: https://dart-current-results.web.app/#/filter=ffi/function_structs_by_value_generated_ret_native_test&showAll.

OK, I've seen it and it's interesting.
Unfortunately, however, outside the test environment, using the files as I presented them in debug mode, the result is what I was complaining about.

@dcharkes
Copy link
Collaborator

I don't have access to an arm 32 bit device. Your armabi-v7 indicates you're targeting 32 bit arm.

For arm64 for it works for me on a physical device, but not on an emulator.

See my example project here: https://github.com/dcharkes/ffi-plugin-with-ffi-native

Could you try to run it on your arm32 device?

@virtualzeta
Copy link
Author

virtualzeta commented Aug 17, 2023

To tell the truth, I have indicated armabi-v7 only as an example because, as I said, the library is generated for each ABI, but it is true that when I build the test APKs, I use that to try them on my smartphone.
For these tests I'm only using an emulator and among other things I was wondering if this could somehow affect the result.

As soon as my programming session is over, I'll do a test with your project and thank you for now.

PS: But if it doesn't work for you with the emulator, which is perhaps the same problem I'm having, how can your test environment be successful if there certainly won't be any physical devices connected? :-|

@virtualzeta
Copy link
Author

virtualzeta commented Aug 18, 2023

Update on what I've done about it.

Since @dcharkes reported that he wasn't successful using the emulator first I stopped using mine and switched to the physical ARM64 device (although that's quite a bummer for someone who is developing and is in a stage where he's just debugging regardless of performance).

  1. Trying with path and without but above all using the RTLD_GLOBAL flag which was giving me problems, the result was that with my code the handle finally arrived with a populated and not reset address.
    Unfortunately even in this case, contrary to what I thought, I still have no visibility of the symbols in DynamicLibray.process().
    Obviously there is still something missing.

  2. Cloning @dcharkes' project I couldn't use it simply because my configuration (SDK: Flutter 3.7.3 • stable channel • Dart 2.19.2) doesn't allow me to use what is expected.
    Not wanting to give up on it, and keeping the template configuration as default as much as possible, I made all the changes to allow me to run the project and debug.
    I indicate below the steps performed hoping they may be useful to others specifying that this workaround is the fastest one to avoid various error messages at the yaml, dart, gradle and xml file level.

Inside a folder dedicated to tests I launched the command flutter create --template=plugin_ffi --platforms=android my_plugin.

Although not essential I renamed the my_plugin folder to ffi-plugin-with-ffi-native to have the same project folder name. Obviously what matters is my_plugin set in the project files and then calling the project ffi-plugin-with-ffi-native is first of all not feasible because dashes are not allowed as in the project name and secondly because it would be different from the original project that I want to replace.

I then took the lib folder in the root of the original project and overwrote it over those of the replacement project and the same with the example/lib folder.

In the root level pubspec.yaml file I replaced:

sdk: '>=3.2.0-53.0.dev <4.0.0' --> sdk: '>=2.19.2 <3.0.0'
ffigen: ^9.0.0 --> ffigen: ^7.2.11
native_assets_cli: ^0.2.0 --> # native_assets_cli: ^0.2.0

In the pubspec.yaml file inside example I replaced:

sdk: '>=3.2.0-53.0.dev <4.0.0' --> sdk: '>=2.19.2 <3.0.0'

In my_plugin_bindings_generated.dart and my_plugin.dart I replaced @Native with @FfiNative respecting the syntax (ie adding the name of the symbol between the empty brackets as a mandatory argument and eliminating the symbol label of the named optional variables).

In my_plugin.dart file I replaced final dylibName = Target.current.os.dylibFileName(_libName); with final dylibName = "lib${_libName}.so"; or final dylibName = "/data/user/0/com.example.my_plugin/lib/lib${_libName}.so";

In my_plugin_bindings_generated.dart file I commented import 'package:native_assets_cli/native_assets_cli.dart';.

And since I was writing from memory I think that's all.

So, having done this and starting in debug mode with the physical device connected, it worked BUT with some quirks that make the difference in the reasoning made up to now.

In practice, using the path indicated above or indicating only the library name without path I always get the handle with the address and see the symbols in DynamicLibrary.process() as I wanted, but I get exactly the same thing even if I remove the flag RTLD_GLOBAL (which should have been critical) leaving only RTLD_LAZY and even giving it the RTLD_LOCAL flag (which I added with const RTLD_LOCAL = 0x00000;) and then doing exactly the opposite of what I asked for.

And to demonstrate that it seems superfluous I add that it also works by removing dlopen and using DynamicLibrary.open() even if the first should be used specifically because the second cannot specify to make the symbols global.

And to confirm that all this is useless I add that you can also remove DynamicLibrary.open() because the symbols are already visible with DynamicLibrary.process().providesSymbol('sum'); right from the start just as you would expect a Windows or iOS platform to behave.

Conclusions

The fact that the handle was received zeroed with Android is derived from the emulator since it is useless to load the libraries by hand and it would seem that everything depends on something (which I still have to look for) that differentiates the template from the configuration file I was trying to using me (although I thought I had replicated everything needed).

As soon as I find it, I'll let you know.

@virtualzeta virtualzeta changed the title Loading/unloading issues with native dlopen/dlclose functions in Flutter Using @Native/@FfiNative in an FFI plugin on Android Aug 18, 2023
@dcharkes dcharkes transferred this issue from dart-archive/ffi Jan 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants