原文地址:nick-fisher.com/articles/ca…
原文作者:nick-fisher.com/
发布时间:2019年9月2日
我有义务向任何找到这里的人发出严重警告。
不要做任何事情,我下面解释。
认真地。
我很快就会推荐玩弄一对火焰电锯。我要说的一切都是一个可怕的想法,你应该完全忽略它。
为什么这么说?
因为虽然可以将Mono和Flutter运行时粘合在一起,但会让你的应用程序看起来像1916年左右的Somme的软件一样。
只有当你真的,真的想要一个应用程序时,你才可以尝试这样做。
不能端到端测试 双倍于原来的二进制大小 依赖于C、CMake、F#和MSBuild,其中大部分对普通移动开发者来说是陌生的(至少是新的)。 由于没有一个单一的构建系统能够解决传统的Android/Objective-C/Swift构建系统在.NET端遇到的汇编依赖/版本问题,所以只能分块构建。 如果你真的需要在移动应用中使用.NET,请使用Xamarin。
如果你真的想使用Flutter,用Dart重写你的库。
我求你--不要混搭。
我这么做了,结果弄得一塌糊涂,以至于我又回到了第二种选择。我想说的是,比我做下面的事情花的时间还少。
说实话,我实在想不出有哪种情况下的取舍会是净正值。
说完了这些,我们来看看动机。
用Mono衔接Flutter和.NET
假设你有一个用Flutter/Dart编写的现有跨平台移动应用程序。
有一天,你偶然发现了一个用(比如)F#编写的奇妙的.NET库,它将你的应用程序从平淡无奇变成了独角兽。
你很想把这两个库整合起来。你要怎么做呢?
首先,快速回顾一下Flutter应用程序的样子。
一个单一的Dart代码库,有一个漂亮的布局/渲染框架,通过Dart虚拟机在Android和iOS上运行。
在Android上,你的应用程序代码和框架本身被编译成字节码,然后由Dart VM进行JIT编译,并由操作系统在CPU上执行。
苹果在iOS上不允许JIT编译代码,所以代码是AOT编译,跳过字节码-JIT编译步骤。
请记住,我在这里使用的 "操作系统 "的定义相当宽松,包括仿真器、模拟器、系统库和本地API。
现在在.NET方面,事情看起来相当相似。F#代码,编译成CIL(字节码),通过运行时(.NET或Mono)进行JIT编译,然后由操作系统在CPU上执行。
对于Flutter应用程序与本地代码的互操作,两个运行时(或静态库,在AOT背景下)需要进行通信。
虽然这一点正在开发中,但Dart目前并不支持本地互操作。Flutter只能通过Java(Android)或Objective C/Swift的平台通道与本地代码进行通信。这些都是简单明了的,而且有详细的文档,所以我在这里就不多说了。
要与F#/.NET程序集进行互操作,我们需要嵌入一个能够在arm64、armv7或x86(针对仿真器)以及iOS和Android操作系统上运行的本地CLR。
Mono是唯一符合这里要求的运行时。.NET Framework和.NET Core都不能用于移动架构--我猜想这是因为微软收购了Xamarin(它在Mono的外壳下运行),支持两个跨平台的CLR实现并不谨慎。
嵌入Mono
那么,我们该如何 "嵌入 "Mono呢?这到底是什么意思呢?
与.NET Framework类似,Mono "安装 "是一组库(静态或动态编译),包含基本的CLR类型(String、Object等)、系统库(文件、内存分配等)、JIT编译器和调用JIT编译代码所需的指令("trampolines")。自己的.NET汇编中的方法是通过Mono运行时来调用的。
"嵌入 "Mono基本上意味着。
- 为给定的操作系统/架构组合构建/部署正确的Mono库。
- 初始化Mono运行时,以确保所有基础库被正确加载。
- 将数据从 "原生 "数据类型转换为 "Mono "类型(例如将JNI JStrings或Objective-C NSString转换为Mono Strings)的胶合代码。
- 胶水代码来定位你自己的程序集(以及你想使用的方法/类),这样你就可以将你的数据传入并处理任何异常。
请注意,这一切都发生在原生(C)代码中--所以在Android上,例如,你的应用程序最终会看起来像这样。
现在,你可能已经开始明白为什么复杂的东西不值得。
编译单片机
首先,你需要编译Mono运行时和一个交叉编译器(因为所有的汇编都需要为iOS进行AOT编译)。
克隆github.com/mono/mono/ ,然后按照sdks目录下的说明进行操作(注意,这需要一个现有的Mono安装来引导,以及automake和ninjna)。
对于Android来说,理论上可以在Windows上实现,但我通过cygwin和WSL都遇到了重大问题。最后,我在Linux上构建了所有的Android专用库。对于iOS库,一切都需要在MacOS上构建。
在sdks/out下,你将最终为每个架构/操作系统组合建立Mono构建。
- ./android -arm64 -v8a -release
- ./android-armeabi-v7a-release
- ./ios-bcl
- ./ios-cross64-release
- ./ios-target64-release
这应该会构建基类库(BCLs - 处理核心任务的标准 CLI 库,如汇编加载、安全等)。然而,如果没有,你可能需要先在根目录下运行autogen和make。
./autogen.sh --with-monodroid # for Android
./autogen.sh --with-monotouch # for iOS
make
编译自己的汇编
假设你要调用的F#模块是这样的。
module Hello
[<EntryPoint>]
let say x =
sprintf "You said %s" x
用fsharpc(或dotnet build)编译它,将你的主项目定位为.NET 4.6.2+控制台应用程序。
Mono需要一个汇编入口点来设置正确的AppDomain路径,并且与.NET Core应用程序有一些不兼容。请注意 EntryPoint 注释。
[Android]复制库
在Android上,将所有项目的DLLs、BCLs和配置一起压缩。
mkdir myproj
mkdir myproj/lib
mkdir myproj/lib/mono
mkdir myproj/lib/mono/4.5
mkdir myproj/etc
mkdir myproj/etc/mono
cp $SRCDIR/hello.dll myproj
cp -R $MONO_REPO_DIR/mcs/class/lib/monotouch/*.dll myproj/lib # replace monotouch with monodroid for Android
cp -R $MONO_REPO_DIR/mcs/class/lib/monotouch/Facades/*.dll myproj/lib
mv myproj/lib/mscorlib.dll myproj/lib/mono/4.5
cp $MONO_REPO_DIR/sdks/builds/ios-target64-release/data/config myproj/etc/mono # replace ios-target-64 with android-arm64-v8a-release for Android
我任意使用了环境变量SRCDIR和MONO_REPO_DIR--你需要将它们设置为正确的目录。
配置文件是需要的,因为Mono会根据所选择的平台,将某些DLLs映射到特定的本地库。
在我的构建中,这些配置文件在不同的架构下是一样的(例如 armv8 与 armv7),所以无论你选择 ios-target-64/ios-target-32/等等,都没有关系。不过,以后可能会有变化,所以要注意。
这个文件夹结构并没有什么特别重要的地方。我们很快就会看到,Mono允许我们手动设置配置和基础库目录。
不过,有两点需要注意。
- Mono希望在mono/4.5子文件夹下找到mscorlib.dll。
- Mono在其DLLs的同一目录下寻找本地重映射库,这意味着我们需要将我们的架构特定库复制到应用程序内部的myproj/lib目录。
在你的Flutter pubspec.yaml中,将这个zip文件添加为资产。你需要在启动时写你自己的脚本来解压这些库到应用程序的${dataDir}/app_flutter子目录。我在这里创建了一个要点,会有帮助。
理想情况下,我会将其与Gradle build集成在一起,但我想不出嵌入动态库(.so)以外的文件的方法。
我假设Android要求所有非库文件都要打包成应用资产。
[iOS]AOT编译库并通过Xcode进行链接
在iOS上,所有的程序集都需要通过Xcode进行AOT编译和复制(而不是在运行时进行压缩和解压)。
我在让这一切工作时遇到了很多困难。
编译成功并不意味着库会真正运行。
引擎盖下有很多强大的、隐藏的依赖关系,这意味着你需要使用正确版本的mscorlib.dll、FSharp.Core.dll、编译开关等等。
大致来说,我需要
- 使用Xamarin.iOS二进制库中的FSharp.Core.dll。不要从NuGet或你的项目构建文件夹中复制--我相信Mono下需要一个较旧的构建。
- 使用与你的Mono构建中完全相同的BCL,而不是用于构建项目的汇编。
- 用 -static 和 -direct-icalls 来编译 mscorlib。
如果你针对icall库进行链接,这应该不是必要的,但是Mono每次都会在没有它的情况下崩溃,抱怨找不到icall查找表。
- 为每个架构创建自己的ld/assembler脚本。
同样,这应该是不需要的,但我的Mono安装没有正确调用本地汇编器/链接器,所以我不得不手动设置这个。
当你编译的时候,使用正确的工具前缀开关(即工具前缀=aarch64-,或工具前缀=armv7s-)。
$ cat /usr/local/bin/aarch64-as
as -arch arm64 $@
$ cat /usr/local/bin/aarch64-ld
clang -Xlinker -v -Xlinker -F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/ -arch arm64 $@
$ cat /usr/local/bin/armv7s-as
as -arch armv7 $@
$ cat /usr/local/bin/armv7s-ld
clang -Xlinker -v -Xlinker -F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/ -arch armv7s $@
mono --aot=full,static,tool-prefix=aarch64,direct-icalls mscorlib.dll
冲洗并重复所有BCLs和项目程序集(不包括mscorlib.dll以外的DLLs的静态和直接icalls开关)。
[Android] 通过CMake链接库
我不会介绍通过Gradle/Flutter构建过程将CMake纳入构建管道的来龙去脉。长话短说,你需要为每个架构复制libmonosgen-2.0.so到你的Flutter项目的android/src/main/jniLibs目录下的相应文件夹。
cp $MONO_REPO_DIR/sdks/out/android-arm64-v8a-release/lib/libmonosgen-2.0.so $FLUTTER_PROJECT_DIR/android/src/main/jniLibs/arm64-v8a
cp $MONO_REPO_DIR/sdks/out/android-armeabi-v7a-release/lib/libmonosgen-2.0.so $FLUTTER_PROJECT_DIR/android/src/main/jniLibs/armeabi-v7a
# repeat for all architectures
您的 CMakeLists.txt 文件将需要包含以下内容。
link_directories("${Project_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}")
add_library(hello SHARED hello.c)
target_include_directories (parser
PUBLIC
"$MONO_REPO_DIR/sdks/out/android-x86-release/include/mono-2.0"
"$GLIB_SRC_DIR"
"$GLIB_SRC_DIR/glib"
"$GLIB_BUILD_DIR/glib" )
target_link_libraries(hello monosgen-2.0 android log gcc m)
[iOS]通过Xcode链接库
在Xcode中,你需要将你的整个应用程序与Mono运行时和你刚刚编译的库(静态或其他)链接起来。你还需要复制原始的程序集(即使实际编译的代码是静态链接的,运行时仍然需要来自程序集的引用元数据来加载这些代码)。
在你的Runner--Build Phase--"Link Binary with Libraries "下,将所有AOT编译的库链接进去。对于动态库,这些也需要包含在 "嵌入框架 "下。
对于动态库,你还需要使用install-name-tool来改变每个动态库的ID和它的rpath。
你还需要包含glib和Mono头文件。
不要尝试直接通过XCode中的Pods项目来设置这些--这些设置会被覆盖。通过ios/yourapp.podspec文件来设置这些,如下所示。
s.pod_target_xcconfig = {
'USER_HEADER_SEARCH_PATHS' => '/usr/local/lib/glib-2.0/include /usr/local/include/glib-2.0 $MONO_REPO_DIR/include/mono-2.0',
'ALWAYS_SEARCH_USER_PATHS' => 'YES' }
标准的Mono SDK构建并不包含x86-64二进制文件,所以如果你想在模拟器上运行,你需要手动将所有的libmonosgen-2.0-compat.dylib文件压缩成一个 "胖 "库,并引用它。
lipo $MONO_REPO_DIR/sdks/out/target-ios-target-64/libmonosgen-2.0.compat.dylib $MONO_REPO_DIR/sdks/out/target-ios-target-32/libmonosgen-2.0.dylib **repeat for all architectures** -create -output libmonosgen-2.0_fat.dylib
加载Mono运行时并调用汇编方法的C代码。
这一步是相当复杂的,一旦加载了运行时,就会变得非常针对应用程序。
关于如何将本地类型转换为传递给Mono运行时、寻找方法/类等,请参考Mono文档。
我在这里只介绍运行时的初始化。
请注意,你的interop代码需要在glib上构建。
yum install glib # Android
brew install glib # ios
我有一个担心的问题是,glibconfig.h是一个生成的/架构特定的文件,包含了某些大小的类型定义。虽然我没有从中遇到任何问题,但我不能保证这一点是确定的。有更多经验的人需要评论一下。
大致来说,原生代码会。
- 找到应用程序安装目录的绝对路径
- 找到XCode复制动态库和DLLs的子目录。
- 在这个路径上调用mono_set_dirs。
- [iOS] 在任何静态编译的DLL上调用mono_aot_register_module。
- [iOS] 调用mono_jit_set_aot_mode(MONO_AOT_MODE_FULL)。
- 调用mono_jit_init("myapp")。
- 调用mono_domain_assembly_open(myDomain, path_to_your_dll)。
下面是我的iOS部分代码的直接复制和粘贴(注意,我实际上是把所有的Mono库/DLLs作为一个单独的框架捆绑在一起,所以你的代码可能看起来略有不同)。
char* subdir = "/Frameworks/ParserAOT.framework/lib/";
assembly_path = malloc(strlen(path) + strlen(subdir));
strcpy(assembly_path, path);
strcat(assembly_path, subdir);
parser_dll_path = malloc(strlen(assembly_path) + strlen(ASSEMBLY_FILE_NAME));
strcpy(parser_dll_path, assembly_path);
strcat(parser_dll_path, ASSEMBLY_FILE_NAME);
config_path = malloc(strlen(assembly_path) + strlen("config"));
strcat(config_path, assembly_path);
strcat(config_path, "config");
mono_set_dirs(assembly_path, assembly_path);
mono_aot_register_module(mono_aot_module_mscorlib_info);
mono_config_parse (config_path);
mono_jit_set_aot_mode(MONO_AOT_MODE_FULL);
myDomain = mono_jit_init("myapp");
MonoAssembly *assembly = mono_domain_assembly_open (myDomain, parser_dll_path);
桥接Dart/Java/Objective C代码到本地代码
你的Dart代码需要通过平台通道向你的Java/Objective C代码发送消息。这是很直接的,所以我不会在这里介绍它。
在Android/Java方面,你将需要通过JNI方法将这个方法反弹到本地代码。这涉及到在你的Java类中添加一个方法签名,比如。
public native String invokeJNI(String method, String data);
...和一个匹配的C方法,比如:JNIEXPORT jobjectArray()
JNIEXPORT jobjectArray JNICALL
Java_com_avinium_parser_ParserPlugin_invokeJNI(JNIEnv *env, jobject obj, jstring method, jstring json) {
再次,阅读JNI的相关知识,以获得正确的实现。
对于iOS应用,直接从Objective C代码中调用C方法。
后脚本
这就是了。一个非常高层次的观点,让Flutter应用程序与F#(或任何.NET)程序集通信所需的东西。
值得吗?绝对不值得。
这是一个疯狂的工作量,最终收益为零。
如果我完全诚实的话,我应该在中途就放弃了。
我之所以坚持,是因为强迫症占据了上风,我不得不完成我开始的工作,无论多么愚蠢。
顺便说一下--你可能认为Mono的mkbundle会把所有的东西都打包成一个单一的共享库--Mono运行时、BCLs、你的应用程序DLL和它的依赖关系。
这实际上是可行的--但只适用于x86_64。mkbundle是为桌面二进制文件设计的,不会处理移动设备。
通过www.DeepL.com/Translator (免费版)翻译