dart-archive/ffi

Does ffi have function "-Wl,rpath"?

fayfive opened this issue ยท 21 comments

Does ffi have function "-Wl,rpath" to redirect the *.so or similar function?

What are you trying to accomplish? Can you give more information? Are you doing Dart standalone or Flutter?

I want to use ffmpeg to display net streams.
I will do it flutter linux android ios and windows.

Do you want to bundle the .so files in the package? or have users install it on their machine?

For Android and iOS see the bundling documentation on https://dart.dev/guides/libraries/c-interop (I don't think we have Linux and Windows documentation as of yet.) Also see https://pub.dev/packages/webcrypto which ships a part of BoringSSL to learn how to set things up.

I see.
But there is one more thing.
For example, when I DynamicLibrary.open libavformat.so, the libavformat.so will call libavcodec.so, and it will show that libavcodec.so not found.
In C, when I compile, I can do it like that "gcc -Wl,-rpath,'$ORIGIN/lib'" to redefine the path of the *.so

In a Flutter app, both Android and iOS you will need to package all required .so into the respective places where an app expects them. When you ship the app to the store they need to be contained in the final .app or .apk. See the documentation.

I have read the link https://dart.dev/guides/libraries/c-interop
I think my state is Closed-source third-party library. Because I have got all *.so files and transformed all headers to *.dart files.
But How to process the Closed-source third-party library is not very detail in the link.
Can you show me the detail method of Closed-source third-party library or the link?

But what about linux and windows?

Linux when already having an .so file:

Add the path of the .so files to: linux/CMakeLists.txt. For example for native/lib/linux_x64/libmylib_dylib.so:

# List of absolute paths to libraries that should be bundled with the plugin
set(mylib_dylib_bundled_libraries
  "${CMAKE_CURRENT_SOURCE_DIR}/../native/lib/linux_x64/libmylib_dylib.so"
  PARENT_SCOPE
)

This will copy the .so file to the bundle/lib (the main executable is in bundle itself).

And then find the .so file relative to the executable:

      if (Platform.isLinux) {
        // The default include path is from the root of the application folder.
        // Opening 'build/linux/x64/debug/bundle/lib/lib$libName.so' works but
        // is not portable for when the app is shipped.
        // Instead, find it relative to the executable in the bundle.
        final path = File(Platform.resolvedExecutable)
            .parent
            .uri
            .resolve('lib/lib$libName.so')
            .path;
        return DynamicLibrary.open(path);
      }

Just opening DynamicLibrary.open('lib/lib$libName.so') or DynamicLibrary.open('lib$libName.so') does not seem to work. So there is something going on with the include path on Linux for Flutter desktop.

@dcharkes
Cannot set "mylib_dylib_bundled_libraries": current scope has no parent.

Can you elaborate? (Please do so in general, half of my posts are asking you for more information.)

The boilerplate generated by flutter create -t plugin --platforms=linux already has the CMakeLists.txt generated:

# List of absolute paths to libraries that should be bundled with the plugin
set(<your plugin name>_bundled_libraries
  ""
  PARENT_SCOPE
)

So you can add it in there.

@dcharkes
I see. Here is my test code. https://github.com/fayfive/ffitetst

I just want to use libffmpeg on flutter.
My system is ubunt18.04.
I create the project by command "flutter create".
I used ffigen to generate the *.dart in the folder
When I run it, it will report the error "libavcodec.so not found".
I know that is because libavformat.so contains libavcodec.so.
In C, I can use -Wl,rpath,'$ORIGIN' to solve it.
I can see linux/CMakeLists.txt has contianed set(CMAKE_INSTALL_RPATH "$ORIGIN/lib"), but it doesn't seem to work.

Thank you for your help!

Please use flutter create -t plugin --platforms=... for creating the project. The plugin template generates the necessary code to include the native libraries.

Are all the dependencies in bundle/lib? If so, you're probably running into flutter/flutter#78819 (comment). You could try dlopening the dependencies first.

@dcharkes
I use flutter create -t plugin --platforms=linux to generate the project.
But it still could not find the *.so.
All the dependencies are in bundle/lib.
I use "ldd" to see the app's dependencies, but I can't see the private *.so
The app path is "example/build/linux/x64/debug/bundle/ffiplugin_example"
Here is my main.dart
import 'package:flutter/services.dart';
import 'package:ffiplugin/ffiplugin.dart';
import 'package:ffiplugin/ffmpeg/libavformat.dart';
void main() {
final path = File(Platform.resolvedExecutable)
.parent
.uri
.resolve('lib/libavformat.so')
.path;
var avformate = libavformat(DynamicLibrary.open(path));
avformate.av_version_info();
runApp(MyApp());
}

libavformat.so contains libavcodec.so, it can find libavformat.so,but libavcodec.so can't be found.

[ERROR:flutter/lib/ui/ui_dart_state.cc(199)] Unhandled Exception: Invalid argument(s): Failed to load dynamic library 'ffiplugin/example/build/linux/x64/debug/bundle/lib/libavformat.so': libavcodec.so.58: cannot open shared object file: No such file or directory
#0 _open (dart:ffi-patch/ffi_dynamic_library_patch.dart:11:55)
#1 new DynamicLibrary.open (dart:ffi-patch/ffi_dynamic_library_patch.dart:20:12)
#2 main (package:ffiplugin_example/main.dart:17:47)
#3 _runMainZoned.. (dart:ui/hooks.dart:142:25)
#4 _rootRun (dart:async/zone.dart:1354:13)
#5 _CustomZone.run (dart:async/zone.dart:1258:19)
#6 _runZoned (dart:async/zone.dart:1789:10)
#7 runZonedGuarded (dart:async/zone.dart:1777:12)
#8 _runMainZoned. (dart:ui/hooks.dart:138:5)
#9 _delayEntrypointInvocation. (dart:isolate-patch/isolate_patch.dart:283:19)
#10 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

@dcharkes
I solve it by adding
target_link_libraries(${BINARY_NAME} PRIVATE "${INSTALL_BUNDLE_LIB_DIR}/libavformat.so")
target_link_libraries(${BINARY_NAME} PRIVATE "${INSTALL_BUNDLE_LIB_DIR}/libavcodec.so")
target_link_libraries(${BINARY_NAME} PRIVATE "${INSTALL_BUNDLE_LIB_DIR}/libavutil.so")
to the end of the example/linux/CMakeLists.txt
This method is not too standard, Because I should add them after I build the example. That means first I should generate the bundle/lib. If I "flutter clean" and debug, it will report error.
Can you show me the method?

This is my WIP:

# Link bundled libraries from plugins into main executable to enable dlopen
# with only library name.
foreach(plugin ${FLUTTER_PLUGIN_LIST})
  if(${plugin}_bundled_libraries)
    target_link_libraries(
      ${BINARY_NAME}
      PRIVATE
      ${${plugin}_bundled_libraries}
    )
  endif()
endforeach(plugin)

It links the libraries from their source location in the build step, before the install step copies them to the final location.

When the libraries are linked in the build step, we can do DynamicLibrary.open('lib/lib$libName.so') instead of opening an absolute path constructed via Platform.resolvedExecutable.

(I am not sure why shared libraries that are not linked during building are not working, this requires more digging.)

I still find this somewhat hacky, and it requires developers making a flutter_app to modify their flutter_app/linux/CMakeLists.txt if they use a plugin package that ships dynamic libraries. I'm looking into making a linux/lib folder in plugin packages, and always linking and bundling all .so files from the lib folder in any flutter app in which it is used.

@fayfive It turns out we were looking at the wrong RUNPATH.

(build flutter app)
$ ./build/linux/x64/release/bundle/flutter_app
(fails)
$ readelf -d build/linux/x64/release/bundle/flutter_app | grep RUNPATH
 0x000000000000001d (RUNPATH)            Library runpath: [$ORIGIN/lib]
$ readelf -d build/linux/x64/release/bundle/lib/libflutter_linux_gtk.so | grep RUNPATH
(not result)
$ patchelf --set-rpath '$ORIGIN' build/linux/x64/release/bundle/lib/libflutter_linux_gtk.so
$ readelf -d build/linux/x64/release/bundle/lib/libflutter_linux_gtk.so | grep RUNPATH
 0x000000000000001d (RUNPATH)            Library runpath: [$ORIGIN]
$ ./build/linux/x64/release/bundle/flutter_app
(works)

When the RUNPATH is set in libflutter_linux_gtk.so, we don't need to dynamically link the libraries at all, we can just dynamically load them.

@dcharkes
That's great! Thanks for your help!
I think it will be fixed next flutter version^_^

maks commented

@dcharkes thanks for your work on this! Especially documenting the comments the workarounds.

I just wanted to report that combining your instructions from this comment to add to my plugin's linux/CMakeLists.txt:

# List of absolute paths to libraries that should be bundled with the plugin
set(mylib_dylib_bundled_libraries
  "${CMAKE_CURRENT_SOURCE_DIR}/my_native_lib.so"
  PARENT_SCOPE
)

and then also your workaround described in the PR in my apps linux/CMakeLists.txt:

# Link bundled libraries from plugins into main executable to enable dlopen
# with only library name.
# Will NOT be needed once: https://github.com/flutter/engine/pull/28525 lands in Flutter channel we are using
foreach(plugin ${FLUTTER_PLUGIN_LIST})
  if(${plugin}_bundled_libraries)
    target_link_libraries(
      ${BINARY_NAME}
      PRIVATE
      ${${plugin}_bundled_libraries}
    )
  endif()
endforeach(plugin)

Gets me a working Flutter Linux Desktop using a plugin which calls into a (Zig compiled) native lib via FFI ๐ŸŽ‰

I'll look out for your recently merged PR to make its way into Flutter beta channel to test that out later on too.

maks commented

@dcharkes following up on this, I'm now trying to use a prebuilt .so in my plugin. What I did was follow your example code in previous comment here and added to my plugin's CMakeLists.txt:

set(mylib_dylib_bundled_libraries
  "${CMAKE_CURRENT_SOURCE_DIR}libmyspecial.so"
  PARENT_SCOPE
)

I then depend on this plugin in my Flutter Linux desktop app and it all works except I found that in the Flutter apps build/linux/x64/debug/bundle/lib I have the so named as: libmyspecial_plugin_plugin.so

As I said, it all works, but I wanted to check if it's expected that the plugin_plugin suffix gets added or am I doing something wrong? BTW This is using beta channel, version 2.6.0-5.2.pre

maks commented

oops sorry, I have been rather dense here! Of course I just spotted that I have this in my CMake file:

# This value is used when generating builds using this plugin, so it must
# not be changed
set(PLUGIN_NAME "myspecial_plugin_plugin")

@fayfive It turns out we were looking at the wrong RUNPATH.

(build flutter app)
$ ./build/linux/x64/release/bundle/flutter_app
(fails)
$ readelf -d build/linux/x64/release/bundle/flutter_app | grep RUNPATH
 0x000000000000001d (RUNPATH)            Library runpath: [$ORIGIN/lib]
$ readelf -d build/linux/x64/release/bundle/lib/libflutter_linux_gtk.so | grep RUNPATH
(not result)
$ patchelf --set-rpath '$ORIGIN' build/linux/x64/release/bundle/lib/libflutter_linux_gtk.so
$ readelf -d build/linux/x64/release/bundle/lib/libflutter_linux_gtk.so | grep RUNPATH
 0x000000000000001d (RUNPATH)            Library runpath: [$ORIGIN]
$ ./build/linux/x64/release/bundle/flutter_app
(works)

When the RUNPATH is set in libflutter_linux_gtk.so, we don't need to dynamically link the libraries at all, we can just dynamically load them.

Any PR we could track which Flutter version fixes the problem?

This has been fixed since flutter/engine#28525.