dart-archive / ffi

Utilities for working with Foreign Function Interface (FFI) code

Home Page:https://pub.dev/packages/ffi

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[bug]: Unable to call a function that have a callback with a const char*

Milerius opened this issue · comments

I would like to say thanks for the amazing work around ffi in dart.

I'm unable to call a simple C function build in rust with a callback:

/// Starts the MM2 in a detached singleton thread.
#[no_mangle]
pub unsafe extern "C" fn mm2_main(conf: *const c_char, log_cb: extern "C" fn(line: *const c_char)) -> i8

link to the code: link

But i'm able to call:

#[no_mangle]
pub extern "C" fn mm2_main_status() -> i8 { mm2_status() as i8 }

First of all i would like to share my approach to load my functions:

mm2_dative.dart

//! Dart packages
import 'dart:async';
import 'dart:ffi' as ffi; // For FFI
import 'dart:io'; // For Platform.isX

//! Flutter packages
import 'package:flutter/services.dart';
import 'package:ffi/ffi.dart';

final ffi.DynamicLibrary mm2NativeLib = Platform.isAndroid
    ? ffi.DynamicLibrary.open("libmm2.so")
    : ffi.DynamicLibrary.process();

//int8_t mm2_main (const char* conf, void (*log_cb) (const char* line));
typedef mm2_log_cb_func = ffi.Void Function(ffi.Pointer<Utf8> line);
typedef mm2_main_func = ffi.Int8 Function(
    ffi.Pointer<Utf8> conf, ffi.Pointer<ffi.NativeFunction<mm2_log_cb_func>>);
typedef MM2Start = int Function(
    ffi.Pointer<Utf8> conf, ffi.Pointer<ffi.NativeFunction<mm2_log_cb_func>>);
//typedef MM2LogCB = ffi.Void Function(ffi.Pointer<Utf8> line);

//! int8_t mm2_main_status (void);
typedef mm2_main_status_func = ffi.Int8 Function();
typedef MM2MainStatus = int Function();

//typedef MM2Status = Int8 Function();
//typedef Mm2Status = int Function();

class Mm2Native {
  static const MethodChannel _channel = const MethodChannel('mm2_native');

  static Future<String?> get platformVersion async {
    final String? version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }

  static int mm2Status() {
    print('before mm2 status');
    MM2MainStatus func = mm2NativeLib
        .lookup<ffi.NativeFunction<mm2_main_status_func>>("mm2_main_status")
        .asFunction();
    print('mm2_status function retrieved');
    int status = func();
    print('mm2_status retrieved');
    return status;
  }

  static void mm2Callback(ffi.Pointer<Utf8> str) {
    print('from mm2_callback: str=' + str.toString());
  }

  // function
  static int mm2Start() {
    //await _channel.invokeMethod('mm2Start');
    print('before mm2 start');
    MM2Start func = mm2NativeLib
        .lookup<ffi.NativeFunction<mm2_main_func>>("mm2_main")
        .asFunction();
    print(func.toString());
    print('mm2 main function retrieved');
    String cfg =
        "{\"gui\":\"MM2GUI\",\"netid\":7777, \"userhome\":\"foo\"}\", \"passphrase\":\"YOUR_PASSPHRASE_HERE\", \"rpc_password\":\"YOUR_PASSWORD_HERE\"}";
    func(cfg.toNativeUtf8(),
        ffi.Pointer.fromFunction<mm2_log_cb_func>(mm2Callback));
    print('mm2 started');
    return 0;
  }
}

IOS Plugin:

#import <Flutter/Flutter.h>

@interface Mm2NativePlugin : NSObject<FlutterPlugin>
@end

//! mm2_main from libmm2.a
int8_t mm2_main (const char* conf, void (*log_cb) (const char* line)); ///< this function dont work
int8_t mm2_main_status (void); ///< this function work

Forcing the symbol usage otherwise xcode seems to exclude them from the binary:

import Flutter
import UIKit
import os.log

public class SwiftMm2NativePlugin: NSObject, FlutterPlugin {
  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "mm2_native", binaryMessenger: registrar.messenger())
    let instance = SwiftMm2NativePlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    switch call.method {
        case "getPlatformVersion":
            os_log("getPlatformVersion iOS", type: OSLogType.default)
            result("iOS " + UIDevice.current.systemVersion)
        default:
            result(FlutterMethodNotImplemented)
        }
  }

   public func dummyMethodToEnforceBundling() {
        let error = Int32(mm2_main("", { (line) in
                let mm2log = ["log": "AppDelegate] " + String(cString: line!)]
            }));

        let res = Int32(mm2_main_status());
   }
}

Podspec:

#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint mm2_native.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
  s.name             = 'mm2_native'
  s.version          = '0.0.1'
  s.summary          = 'A new flutter plugin project.'
  s.description      = <<-DESC
A new flutter plugin project.
                       DESC
  s.homepage         = 'http://example.com'
  s.license          = { :file => '../LICENSE' }
  s.author           = { 'Your Company' => 'email@example.com' }
  s.source           = { :path => '.' }
  s.public_header_files = 'Classes**/*.h'
  s.source_files = 'Classes/**/*'
  s.static_framework = true
  s.vendored_libraries = "**/*.a"
  s.dependency 'Flutter'
  s.platform = :ios, '10.0'

  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
  s.swift_version = '5.0'
end

Usage from example dart project:

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:mm2_native/mm2_native.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String _platformVersion = 'Unknown';

  @override
  void initState() {
    super.initState();
    initPlatformState();
    initMM2();
  }

  void initMM2() {
    print("status: " + Mm2Native.mm2Status().toString());
    Mm2Native.mm2Start();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    String platformVersion;
    // Platform messages may fail, so we use a try/catch PlatformException.
    // We also handle the message potentially returning null.
    try {
      platformVersion =
          await Mm2Native.platformVersion ?? 'Unknown platform version';
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
          child: Text('Running on: $_platformVersion\n'),
        ),
      ),
    );
  }
}

Log output:

➜  example git:(main) ✗ flutter run
Launching lib/main.dart on iPhone 12 Pro Max in debug mode...
Warning: Missing build name (CFBundleShortVersionString).
Warning: Missing build number (CFBundleVersion).
Action Required: You must set a build name and number in the pubspec.yaml file version field before submitting to the App Store.
Running Xcode build...                                                  
 └─Compiling, linking and signing...                         4,2s
Xcode build done.                                           18,7s
getPlatformVersion iOS
flutter: before mm2 status
flutter: mm2_status function retrieved
flutter: mm2_status retrieved
flutter: status: 0
flutter: before mm2 start
flutter: Closure: (Pointer<Utf8>, Pointer<NativeFunction<(Pointer<Utf8>) => Void>>) => int
flutter: mm2 main function retrieved
flutter: mm2 started
version=2.13.3 (stable) (Wed Jun 9 12:44:44 2021 +0200) on "ios_x64"
pid=50916, thread=30723, isolate_group=(nil)(0x0), isolate=(nil)(0x0)
isolate_instructions=0, vm_instructions=10c5d21e0
  pc 0x000000010c6f3ab4 fp 0x0000700002f27c20 dart::Profiler::DumpStackTrace(void*)+0x64
  pc 0x000000010c5d23b4 fp 0x0000700002f27d00 dart::Assert::Fail(char const*, ...)+0x84
  pc 0x000000010c73fc6d fp 0x0000700002f27d40 dart::GetThreadForNativeCallback(unsigned long, unsigned long)+0xdd
  pc 0x000000011207ef6e fp 0x0000700002f27d50 Unknown symbol
  pc 0x00000000000000dc fp 0x0000700002f27f20 Unknown symbol
  pc 0x00000001099ceb41 fp 0x0000700002f27f80 core::ops::function::FnOnce::call_once$u7b$$u7b$vtable.shim$u7d$$u7d$::h705f45fbc9a0b3ab+0x61
  pc 0x000000010aabe1ab fp 0x0000700002f27fb0 std::sys::unix::thread::Thread::new::thread_start::ha532fcedf57aa038+0x1b
  pc 0x00007fff6116a950 fp 0x0000700002f27fd0 _pthread_start+0xe0
  pc 0x00007fff6116647b fp 0x0000700002f27ff0 thread_start+0xf
-- End of DumpStackTrace

The function mm2_main_status can be called, but the one with the callback seems to crash when calling the dart callback :/

From what i understand, it's likely because the function mm2_main start into a detached thread, but even if it's the case passing a string in between should not cause any problem if a deep copy of the string is made (there is no risk of data race here or gc problem), why this is not allowed ?

In this particular case it's only a log callback, so it's a read-only ops

The idea is to start our mm2 service in a detached thread but still get the logs into the main dart console as a read-only ops

any guide/advice are highly appreciated to achieve this behaviour

From what i understand, it's likely because the function mm2_main start into a detached thread

Yes, you cannot call back into Dart from another thread. That would have two threads executing in a Dart isolate which breaks Darts concurrency model. Asynchronous callbacks directly in FFI are tracked in dart-lang/sdk#37022. Until then, you'll need to use native ports to asynchronously send messages to an isolate while it already has a Dart thread running. Documentation in the same issue.

From what i understand, it's likely because the function mm2_main start into a detached thread

Yes, you cannot call back into Dart from another thread. That would have two threads executing in a Dart isolate which breaks Darts concurrency model. Asynchronous callbacks directly in FFI are tracked in dart-lang/sdk#37022. Until then, you'll need to use native ports to asynchronously send messages to an isolate while it already has a Dart thread running. Documentation in the same issue.

Oh i see, then i will wait for the official implementation in order to avoid to write any kotlin/swift code if possible, the idea is to enjoy maximum the feature of dart instead of depending on native code - so i guess once it's supported by dart-lang/sdk#37022 i could just move forward !

Thanks for the explanation :)

Do i have to add anything to link to dart_api_dl.h ?

Yes, see the documentation: https://github.com/dart-lang/sdk/blob/master/runtime/include/dart_api_dl.h#L21-L22

Oh, but it's likely what i absolutely don't want to do, will require to modify the rust code, so i guess i will just wait a more flexible support that doesn't require any change from the C code in this case.

From a more different perspective, can we likely expect calling a dart callback from another thread in C/Rust without adding extras C code using dart API ?

What i mean is in this particular case i do understand dart isolate context, but should be possible to have a channel for read-only ops between threads no ?

Somehow i misunderstand some part of the dart VM maybe.

If i understand correctly with async callback i will not need to write any extra C code from my end user library (if i understand it correctly)

Otherwise i have to write a C library which link to the rust library, using the C functions, and use the dart API directly in this library, which i would like to avoid for sure.

From a more different perspective, can we likely expect calling a dart callback from another thread in C/Rust without adding extras C code using dart API ?

At some point maybe, but I cannot give any timelines.

but should be possible to have a channel for read-only ops between threads no ?

Doing a callback is not a read-only op. You might be calling code that has not been compiled yet in JIT mode. Also, you're printing to stdout which is a side effect. And in general we can't see from Dart code if it does not modify global objects etc.

If i understand correctly with async callback i will not need to write any extra C code from my end user library (if i understand it correctly)

Yes, the goal would be that an async callback would schedule execution in the "event loop" or in "micro tasks", while a synchronous callback just immediately starts executing Dart code.

From a more different perspective, can we likely expect calling a dart callback from another thread in C/Rust without adding extras C code using dart API ?

At some point maybe, but I cannot give any timelines.

but should be possible to have a channel for read-only ops between threads no ?

Doing a callback is not a read-only op. You might be calling code that has not been compiled yet in JIT mode. Also, you're printing to stdout which is a side effect. And in general we can't see from Dart code if it does not modify global objects etc.

If i understand correctly with async callback i will not need to write any extra C code from my end user library (if i understand it correctly)

Yes, the goal would be that an async callback would schedule execution in the "event loop" or in "micro tasks", while a synchronous callback just immediately starts executing Dart code.

Would it make sense to take a file descriptor (created from the dart isolated thread) instead of a callback and writing to this file descriptor from another thread in rust ?

If you're just interested in writing logs somewhere, yes.

Though, you cannot get a file descriptor from Dart and pass it to Rust, so you'd need to pass the path of the file you want to open.

If you're just interested in writing logs somewhere, yes.

Though, you cannot get a file descriptor from Dart and pass it to Rust, so you'd need to pass the path of the file you want to open.

Make sense, thanks for your time and precious explanation !