首页
统计
留言板
关于
Search
1
Rust 包装 objc Block
166 阅读
2
Flutter 调用 Rust 生成的 共享/静态 库
157 阅读
3
02. Rust 内存管理 Copy & Clone(上)
157 阅读
4
用 Rust 开发 iOS 应用(粗糙版)
104 阅读
5
纯 C 写个 iOS App(误)
99 阅读
默认分类
Rust
Apple
iOS
Swift
Android
emulator
NES
登录
/
注册
Search
标签搜索
Rust
iOS
NES
Swift
Android
杂项
limit
累计撰写
18
篇文章
累计收到
0
条评论
首页
栏目
默认分类
Rust
Apple
iOS
Swift
Android
emulator
NES
页面
统计
留言板
关于
搜索到
11
篇与
的结果
2023-04-11
用 Rust 写个 CLI 解决 UWP Bilibili 下载的视频无法播放问题
发现问题众所周知,使用 wINDOWS 微软商店下载的 Bilibili 客户端是有视频下载功能的,不过它有个问题,下载后的视频无法直接使用一般的打开。 稍微研究(使用搜索引擎)一下发觉,原来是该客户端下载后的文件开头补了三个 0xFF。解决方案最简单的方法就是直接使用一些支持 hex 编辑的 editor 手动把它们删掉再保存,不过如果有多个文件(很明显这是常态),那一个一个手动处理太机械了,于是直接写个命令行工具解决。实现原理也很简单,就是直接跳过读取开头三个 0xFF 再写入到新文件。开始编码创建项目创建 Rust 项目先,直接执行 cargo new xxx 就把项目生成好啦。然后添加几个第三方库,首先自然是 clap,然后是错误处理,这种小工具就不手写自定义错误处理啦,使用 anyhow,还可以加个进度条指示器indictif,因为要批处理文件,文件夹内可能有非视频文件,所以加个 regex 包用正则过滤一下文件。其中 clap 用得是它的派生宏版本。cargo add clap -F derive cargo add anyhow cargo add indicatif cargo add regex可以 cat 查看一下 Cargo.toml 内容$ cat ./Cargo.toml --------------------------- [package] name = "xxx" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1.0.70" clap = { version = "4.2.1", features = ["derive"] } indicatif = "0.17.3" regex = "1.7.3"正式编码先定义好传参,其实就是接收输入的目录,所以用派生宏包一下结构体 Args#[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] struct Args { #[clap(value_parser)] input: String, }然后在 main 函数写上下面的代码,主要是获取待处理的目录,当它不是一个文件夹时直接 panicfn main() { let args: Args = Args::parse(); let p = Path::new(&args.input); if !p.is_dir() { panic!("The input is not directory!"); } read_file(p); }我们还得把 read_file 函数补上,这一步就是基本逻辑就是使用 std::fs::read_dir 读取文件夹内的子路径,判断一下读取到的是否是文件或文件夹,如果是文件夹就递归继续读取,直到读到文件为止,读取到文件就用 process_file 处理它,出错了不管直接 unwrap 死就死吧,小项目是这样的捏。fn read_file(dir: &Path) { for sub_path in read_dir(dir).expect("Failed to read file path.").flatten() { let p = sub_path.path(); if p.is_file() { process_file(&p).unwrap(); } else { read_file(&p); } } }个么很自然地就是实现 process_file 函数,先取到文件名(其实是取得完整的文件路径),然后通过正则过滤一下副档名是 mp4 的文件,读取文件大小给进度条结构用,判断下原文件开头三个字节是 0xFF 下继续进行下一步操作,主要是为了防止处理错文件。然后创建个空文件,循环读取原文件内容写入到新文件,后面再把原文件删除,把新文件修改一下名字,至此处理完成。const BUFFER_SIZE: usize = 1024; fn process_file(p: &Path) -> Result<(), anyhow::Error> { let filename = p .file_name() .expect("Failed to get filename.") .to_str() .expect("Failed to unwrap path str."); let mut buffer = [0u8; BUFFER_SIZE]; let file_regex = Regex::new(r"\.mp4$").expect("Failed to new file regex."); if file_regex.is_match(filename) { let mut original_file = File::open(p)?; let file_size = original_file.metadata()?.len(); let pb = ProgressBar::new(file_size); let mut count = original_file.read(&mut buffer)?; if buffer[0] == 0xFF && buffer[1] == 0xFF && buffer[2] == 0xFF { let new_filename = String::from("new_") + filename; let new_filename = p.with_file_name(new_filename); let new_file_path = Path::new(&new_filename); let mut new_file = File::create(new_file_path)?; let mut is_first = true; while count != 0 { if is_first { new_file.write_all(&buffer[3..count])?; pb.inc((count - 3) as u64); is_first = false; } else { new_file.write_all(&buffer[..count])?; pb.inc(count as u64); } count = original_file.read(&mut buffer)?; } remove_file(p)?; rename(new_file_path, p)?; pb.finish_with_message("done"); } } Ok(()) }然后就可以通过一般的视频播放器打开文件啦。
2023年04月11日
94 阅读
0 评论
0 点赞
2022-05-30
Flutter 调用 Rust 生成的 共享/静态 库
日常发病前面写过一两篇使用水文, 用 Rust 写点东西, 编译成 shared/static lib 给 Android 或者 iOS 端调用.这篇水文需要用到之前写得 MD5 的那个 Rust 项目. Android 使用 Rust 生成的动态库 用 Rust 开发 iOS 应用(粗糙版) 今天发病的主题是 Flutter FFI 相关.创建 Flutter 项目这里直接通过下面这条命令创建flutter create --platforms=android,ios --template=plugin ffi_demo这里使用的是 plugin 的模板, 如果直接使用 project 模板也是可以的, 现在直接用 Android Studio 打开项目. 我们先来看看项目结构tree -L 1 . ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── example ├── ffi_demo.iml ├── ios ├── lib ├── pubspec.lock ├── pubspec.yaml └── test 5 directories, 7 files处理 Android 端Android 端的比较好处理, 把开头 Android 的那篇文章的 md5 项目生成的动态库拷贝出来, 放到 android/src/main/jniLibs/arm64-v8a 下面, 事实上这里不需要用到 CMakeLists, 所以直接放到 jniLibs 下面, 不过有个问题, Android 项目并不知道你放这边了, 所以还得配置一下 build.gradle 的 sourceSetsandroid { // ... sourceSets { // ... main.jniLibs.srcDirs = ['src/main/jniLibs'] } // ... }这一步完成后, 我们先把 Android 这边的事放一放处理 iOS 端由于之前的 md5 那个项目, 没编译 iOS 的静态库, 所以我们先得把 iOS 静态库生成一下.喜闻乐见的添加对应 target 环节rustup target add aarch64-apple-ios如果你想跑在 Intel 设备的 iOS 仿真器上, 需要添加 x86 的 target, 这里只添加了手机 aarch64 的, 如果你想跑在 M1 设备的 iOS 仿真器上, 你需要添加rustup target add aarch64-apple-ios-sim接着跑到 md5 的项目中运行构造命令cargo build --target aarch64-apple-ios --release构建成功后把 target 目录对应平台的静态库拷贝出来放到 ios/Frameworks 下, 没有 Frameworks 文件夹就自己创建, 文件夹名字随便起, 然后把 ffi_demo.podspec 文件改一下, 总之找到对应的 .podspec 文件, 添加指定静态库的路径Pod::Spec.new do |s| # ... s.vendored_libraries = 'Frameworks/libmd5.a' # ... end处理 Flutter 端上面的准备工作完成后, 我们可以来暴露接口给 Dart 使用啦! 找到 lib/ffi_demo.dart 文件, 修改里面的代码import 'dart:io'; import 'dart:ffi'; // third part package import 'package:ffi/ffi.dart'; typedef LLMD5 = Pointer<Int8> Function(Pointer<Int8>); class FfiDemo { String md5(String str) { final DynamicLibrary nativeLib = Platform.isAndroid ? DynamicLibrary.open("libmd5.so") : DynamicLibrary.process(); LLMD5 md5 = nativeLib.lookupFunction<LLMD5, LLMD5>("ll_md5"); var result = md5(str.toNativeUtf8().cast<Int8>()); return result.cast<Utf8>().toDartString(); } }其实就是把 dart 的字符串转成 C 的字符串传给 md5 函数使用, 返回的结果再转成 dart 的字符串, 我们代码中用到一个第三方包来处理 dart 字符串跟 C 字符串的转换, 直接执行添加第三方包的命令flutter pub add ffi执行 exampleAndroid为了验证我们的插件是否正常工作, 我们把 example 里面的 lib/main.dart 修改一下import 'package:flutter/material.dart'; import 'package:ffi_demo/ffi_demo.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State<MyApp> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { String _md5Str = ''; final _ffiDemoPlugin = FfiDemo(); @override void initState() { super.initState(); initMD5(); } void initMD5() { String str = ''; try { str = _ffiDemoPlugin.md5("foo"); } on Exception { str = 'MD5 failed'; } if (!mounted) return; setState(() { _md5Str = str; }); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('Plugin example app'), ), body: Center( child: Text('$_md5Str\n'), ), ), ); } }然后连接对应 ABI 的 Android 手机, 用 Android Studio 执行一下, 可以看到屏幕中间显示了一串经过 md5 运算后的字符串.iOSiOS 这边, 因为我们修改了 podspec 文件, 所以我们需要在 example/ios 目录下执行pod install然后就是更新 xcworkspace, 完成后, 就可以在 Pod 项目中找到我们的静态库了然后用 Android Studio 打开插件项目(直接用 Xcode 执行 Runner 可能会出现找不到符号的问题), 执行到对应的设备(或者仿真器), 就能看到对应的效果Flutter FFI 使用起来比较简单, 某些情况也很有用, 譬如要使用 FFmpeg 理论上可以通过 FFI 来达成我们的目标.
2022年05月30日
157 阅读
0 评论
0 点赞
2022-05-22
Android 使用 Rust 生成的动态库
Android NDK 可以使用一些第三方的动态库, 如何用 Rust 写个东西生成动态库, 给 Cpp 这边调用, 这边记录一下过程.配置 Rust 工程首先写个 Rust 工程, 搞出个动态库出来, 先是创建个项目, 这里取名叫 ffi-examplecargo new ffi-example --lib打开 Cargo.toml 文件, 里面的内容长这样[package] name = "ffi-example" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "ffi_example" crate-type = ["staticlib", "cdylib"] [dependencies] md5 = "0.7"我们的初衷是为了把 Rust 生成的动态库给 Android 端使用, 这里就不添加 jni 相关的 crate 了, 如果要写很多 native 的代码, 建议补上这个 crate. 这个工程主要是使用到了一个 md5 的 crate, 顺便把后续要生成的 crate 类型标注成 staticlib 跟 cdylib.然后跳到工程中的 lib.rs 文件, 把里面的内容改成下面这些use md5::compute; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_uchar}; #[no_mangle] extern "C" fn ll_md5(buf: *const c_char) -> *const c_uchar { let buf = unsafe { CStr::from_ptr(buf) }.to_str().unwrap().as_bytes(); let digest = format!("{:x}", compute(buf)); CString::new(digest).unwrap().into_raw() as *const c_uchar }代码可以加点自己的 lint, 可以补充个 rustfmt.toml 文件, 譬如我这里用得是两个空格的代码风格tab_spaces = 2现在把我们的代码构建成动态库, 可以把对应 x86 的 target 安装上, 安装对应的 target, 需要用 rustup 安装, 可以先搜索一下有哪些 targetrustup target list如果你用得是水果 M1 芯片的设备, 可以直接使用 ARM64 的 Android 仿真器, 下面这条命令就可以兼顾 Android 真机跟 M1 上的 Android 仿真器 (只要你 Android Studio 设置的仿真器是 ARM64 的)rustup target add aarch64-linux-android假设你已经装好了必要的 target, 可以执行下面的命令打包cargo build --target aarch64-linux-android --release然后我们看到工程的 target 文件夹下生成了一个 aarch64-linux-android 文件夹, 里面的 release 文件夹下就有我们想要的 libffi_example.so 文件如果编译出错其实还有一个事情没讲, 那就是 Rust 编译 Android 可用的动态库, 需要配置 NDK standalone.先把 ndk 装好, 直接在 Android Studio SDK Tools 的 NDK (Side by side) 选一个版本安装.然后执行下面的命令, 具体目录根据自己的情况而定export ANDROID_HOME=$HOME/Library/Android/sdk export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/21.4.7075529 cd ~/Library/Android/sdk/ndk python3 $ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --api 28 --arch arm64 --install-dir ./arm64上面只处理 arm64 的情况, 具体 ABI 根据自己的需要而定, 然后设置一下 .cargo/config 里面的内容[target.aarch64-linux-android] ar = "/Users/your name/Library/Android/sdk/ndk/arm64/bin/aarch64-linux-android-ar" linker = "/Users/your name/Library/Android/sdk/ndk/arm64/bin/aarch64-linux-android-clang" [target.armv7-linux-androideabi] ar = "/Users/your name/Library/Android/sdk/ndk/arm/bin/arm-linux-androideabi-ar" linker = "/Users/your name/Library/Android/sdk/ndk/arm/bin/arm-linux-androideabi-clang" [target.i686-linux-android] ar = "/Users/your name/Library/Android/sdk/ndk/x86/bin/i686-linux-android-ar" linker = "/Users/your name/Library/Android/sdk/ndk/x86/bin/i686-linux-android-clang"其实就是根据你指定的 target 使用用对应平台的链接器, 这里建议使用 NDK 的版本是 21, 更高版本的我还没测试过能不能编译通过.配置 Android 工程现在用 Android Studio 来创建个 Android 的项目, 模板选择 Cpp 的那个, 语言不论 Kotlin 还是 Java 都可以, Minimum SDK 随便选一个, 我这里选得是 API 26 以上的.接着要来改改配置, 找到项目中的 CMakeLists.txt 文件, 在 find_library 上面添加一些内容, 这里的 CMAKE_ANDROID_ARCH_ABI 对根据环境自动指定对应的文件夹(target_link_libraries 也要加入相应的动态库名字)add_library(ffi_example SHARED IMPORTED) set_target_properties(ffi_example PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/lib/${CMAKE_ANDROID_ARCH_ABI}/libffi_example.so) # ... target_link_libraries( # Specifies the target library. ffidemo # 你导入的动态库 ffi_example # Links the target library to the log library # included in the NDK. ${log-lib})然后把之前生成的动态库拷贝到 Android 项目中来, 直接放到 src/main/cpp/lib/arm64-v8a 目录下(如果你有其他的 ABI 的动态库, 你也可以加上对应的文件夹, 放入相应的动态库), 网上很多文章说要放到 libs 或者 jniLibs 之类的文件夹, 现在新版本不需要这样做了, 我们以官方的文档为准. 此外, build.gradle (:app) 可以把对应的 ndkVersion 加上android { compileSdk 32 defaultConfig { applicationId "wiki.mdzz.ffidemo" minSdk 26 targetSdk 32 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { cppFlags '' } } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } externalNativeBuild { cmake { path file('src/main/cpp/CMakeLists.txt') version '3.18.1' } } buildFeatures { viewBinding true } ndkVersion '24.0.8215888' }然后我们执行一下, 就会发现, 你的 Logcat 告诉你说java.lang.UnsatisfiedLinkError: dlopen failed: library "~/FFIDemo/app/src/main/cpp/lib/arm64-v8a/libffi_example.so" not found我们把 app-debug.apk 的文件拿出来, 副档名改成 zip, 然后解压, 找到里面的 libffi_example.so 文件, 用 readelf 命令读取一下文件看看, 再把另一个 so 文件用 readelf 读取一下看看内容readelf -d libffi_example.so Dynamic section at offset 0x3fc70 contains 25 entries: Tag Type Name/Value 0x0000000000000001 (NEEDED) Shared library: [libdl.so] 0x0000000000000001 (NEEDED) Shared library: [libc.so] 0x0000000000000001 (NEEDED) Shared library: [libm.so] 0x000000000000001a (FINI_ARRAY) 0x3eb90 0x000000000000001c (FINI_ARRAYSZ) 16 (bytes) 0x0000000000000004 (HASH) 0x1c8 0x000000006ffffef5 (GNU_HASH) 0x3a0 0x0000000000000005 (STRTAB) 0x7a0 0x0000000000000006 (SYMTAB) 0x3c8 0x000000000000000a (STRSZ) 412 (bytes) 0x000000000000000b (SYMENT) 24 (bytes) 0x0000000000000003 (PLTGOT) 0x40e40 0x0000000000000002 (PLTRELSZ) 864 (bytes) 0x0000000000000014 (PLTREL) RELA 0x0000000000000017 (JMPREL) 0x3688 0x0000000000000007 (RELA) 0x9d0 0x0000000000000008 (RELASZ) 11448 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) 0x000000000000001e (FLAGS) BIND_NOW 0x000000006ffffffb (FLAGS_1) Flags: NOW 0x000000006ffffffe (VERNEED) 0x990 0x000000006fffffff (VERNEEDNUM) 2 0x000000006ffffff0 (VERSYM) 0x93c 0x000000006ffffff9 (RELACOUNT) 476 0x0000000000000000 (NULL) 0x0readelf -d libffidemo.so Dynamic section at offset 0x32aa8 contains 27 entries: Tag Type Name/Value 0x0000000000000001 (NEEDED) Shared library: [~/FFIDemo/app/src/main/cpp/lib/arm64-v8a/libffi_example.so] 0x0000000000000001 (NEEDED) Shared library: [liblog.so] 0x0000000000000001 (NEEDED) Shared library: [libm.so] 0x0000000000000001 (NEEDED) Shared library: [libdl.so] 0x0000000000000001 (NEEDED) Shared library: [libc.so] 0x000000000000000e (SONAME) Library soname: [libffidemo.so] 0x000000000000001a (FINI_ARRAY) 0x30d50 0x000000000000001c (FINI_ARRAYSZ) 16 (bytes) 0x000000006ffffef5 (GNU_HASH) 0x228 0x0000000000000005 (STRTAB) 0x36a0 0x0000000000000006 (SYMTAB) 0xe68 0x000000000000000a (STRSZ) 8493 (bytes) 0x000000000000000b (SYMENT) 24 (bytes) 0x0000000000000003 (PLTGOT) 0x33c98 0x0000000000000002 (PLTRELSZ) 1848 (bytes) 0x0000000000000014 (PLTREL) RELA 0x0000000000000017 (JMPREL) 0xd8d8 0x0000000000000007 (RELA) 0x5b68 0x0000000000000008 (RELASZ) 32112 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) 0x000000000000001e (FLAGS) BIND_NOW 0x000000006ffffffb (FLAGS_1) Flags: NOW 0x000000006ffffffe (VERNEED) 0x5b28 0x000000006fffffff (VERNEEDNUM) 2 0x000000006ffffff0 (VERSYM) 0x57ce 0x000000006ffffff9 (RELACOUNT) 886 0x0000000000000000 (NULL) 0x0然后你会发觉, Rust 生成的动态库, 没有 Library soname, 所以我们得再生成一个带 soname 的动态库. 回到 Rust 项目, 通过下面的命令构建一下, 可以先 cargo clean 清理一下 target 目录cargo clean RUSTFLAGS="-Clink-arg=-Wl,-soname=libffi_example.so" cargo build --target aarch64-linux-android --release这里我们手动给动态库加上了 soname, 再把生成的动态库放到 Android 工程中. 在重新执行之前, 可以把项目中 app 目录下的 .cxx 跟 build 文件夹删一下, 防止出现奇怪的问题. 再次执行时, 我们的 App 已经可以跑起来. 接着把 native-lib 的 Cpp 代码 stringFromJNI, 修改一下, 用用看原生库的效果, 因为现在仿真器的屏幕上显示得还是 Hello from C++.#include <jni.h> #include <string> #include "llmd5.h" extern "C" JNIEXPORT jstring JNICALL Java_wiki_mdzz_ffidemo_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { auto fooMD5 = ll_md5("foo"); return env->NewStringUTF(fooMD5); }我们还忘了把头文件加上, 头文件内容长这样#ifndef FFIDEMO_LLMD5_H #define FFIDEMO_LLMD5_H #if __cplusplus extern "C" { #endif const char *ll_md5(const char *buf); #if __cplusplus } #endif #endif //FFIDEMO_LLMD5_H因为我们原生语言用得是 Cpp, 所以需要加上 extern "C".然后再编译执行 App, 应该能看到仿真器的屏幕上显示一串字符串. 为了让这个函数更通用, 可以接收 Java/Kotlin 那边传过来的字符串, 再生成对应的 md5 字符串.#include <jni.h> #include <string> #include "llmd5.h" extern "C" JNIEXPORT jstring JNICALL Java_wiki_mdzz_ffidemo_MainActivity_stringFromJNI( JNIEnv *env, jobject /* this */, jstring buf) { auto data = env->GetStringUTFChars(buf, nullptr); auto result = ll_md5(data); env->ReleaseStringUTFChars(buf, data); return env->NewStringUTF(result); }Kotlin/Java 的代码也可以改一下private external fun stringFromJNI(buf: String): Stringpublic native String stringFromJNI(String buf);不想看文字, 可以直接看项目 https://e.coding.net/limitLiu/java/FFIDemo.git
2022年05月22日
47 阅读
0 评论
0 点赞
2022-04-25
Rust 包装 objc Block
Block 简介使用 objc 开发 App 时, 经常会使用到 Block, 这个语法糖是 Clang 给 C 语言实现的一个拓展. Block 是可以被编译成 C 语言的代码的. 如果有想法可以直接看 Clang 官方关于 Block 的文档 Block-ABI-Applerewrite-objc 生成 cpp 代码先来用 Clang 把一个普通的 objc 文件生成到 Cpp 代码, 看看 Block 生成的 C 语言代码长啥样. 先写个简单的 hello world 程序#import <stdio.h> int main(void) { @autoreleasepool { void (^test)(void) = ^{ printf("hello, world!\n"); }; test(); } return 0; }然后再用 clang 程序把上面的代码生成到 cpp 代码clang -rewrite-objc ./test.m然后会生成一堆代码, 我们找里面的关键内容struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("hello, world!\n"); } static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; int main(void) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; void (*test)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); ((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test); } return 0; }从代码上基本可以肯定static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("hello, world!"); }表示的是^{ printf("hello, world!"); };因为 __main_block_impl_0 包含 __block_impl 这个结构体, 所以struct __main_block_impl_0 { struct __block_impl impl; /* void *isa; int Flags; int Reserved; void *FuncPtr; */ struct __main_block_desc_0* Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };接着看 main 函数里, 把 __main_block_impl_0 构造函数用指针指向它// void (*test)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA); void (*test)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));结构体的构造函数执行后把 fp 指针传给 FuncPtr, fp 指针就是 __main_block_func_0, 也就是那个 hello world 代码.__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { // ... impl.FuncPtr = fp; // ... }使用外部变量的 BlockBlock 具备使用外部变量的能力, 有些类似其他语言的闭包, 对于变量的使用分为局部变量跟全局变量, 先来看局部变量局部变量局部变量的处理, 又分别针对 auto 变量跟 static 变量有对应的实现.auto 变量上面只是简单的 hello world 的 Block, 现在来使用一个 Block 之外的 auto 变量, rewrite 后会发生什么.#import <stdio.h> int main(void) { @autoreleasepool { int number = 10; void (^test)(void) = ^{ printf("hello, world!, number = %d\n", number); }; test(); } return 0; }// ... struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int number; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, int flags=0) : number(_number) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int number = __cself->number; // bound by copy printf("hello, world!, number = %d\n", number); } // ...这次我们发现, 其他东西没啥变化, 不过 __main_block_impl_0 跟 __main_block_func_0 多了个跟 int 类型的 number, 其中还能看出 __main_block_impl_0 赋值给 __cself, 直接通过 __cself 使用 __main_block_impl_0 的 number.static 变量再来看看 static 变量的情况#import <stdio.h> int main(void) { @autoreleasepool { int number = 10; static int b = 10; void (^test)(void) = ^{ printf("hello, world!, number = %d, b = %d\n", number, b); }; test(); } return 0; }// ... struct __main_block_impl_0 { // ... int *b; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, int *_b, int flags=0) : number(_number), b(_b) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int number = __cself->number; // bound by copy int *b = __cself->b; // bound by copy printf("hello, world!, number = %d, b = %d\n", number, (*b)); } int main(void) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; int number = 10; static int b = 10; void (*test)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, number, &b)); ((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test); } return 0; }从代码中我们可以看出, 通过 & 操作符把 b 的地址传给 __main_block_impl_0 的构造函数, 同时 __main_block_impl_0 有一个 int *b 的成员, 同时在 __main_block_func_0 里进行解指针操作取值, 其实可以猜到一个行为, 如果在 block 调用之前修改 b, 最后取到的 b 是修改过的值, 因为它是通过 b 的指针进行取值.全局变量现在来看看全局变量的情况, 这种情况其实可以猜到, Block 直接使用全局变量, 不会在 struct 里添加成员. 现在来验证一下#import <stdio.h> int number_= 11; static int b_ = 11; int main(void) { @autoreleasepool { int number = 10; static int b = 10; void (^test)(void) = ^{ printf("hello, world!, number = %d, b = %d, number_ = %d, b_ = %d\n", number, b, number_, b_); }; test(); } return 0; }static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int number = __cself->number; // bound by copy int *b = __cself->b; // bound by copy printf("hello, world!, number = %d, b = %d, number_ = %d, b_ = %d\n", number, (*b), number_, b_); }跟我们刚才猜得行为是一致的.多参数 Block继续尝试修改代码后再 rewrite#import <stdio.h> int main(void) { @autoreleasepool { int number = 10; void (^test)(int a) = ^(int a) { printf("hello, world!, number = %d, a = %d\n", number, a); }; test(11); } return 0; }// ... static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a) { int number = __cself->number; // bound by copy printf("hello, world!, number = %d, a = %d\n", number, a); } // ... int main(void) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; int number = 10; void (*test)(int a) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, number)); ((void (*)(__block_impl *, int))((__block_impl *)test)->FuncPtr)((__block_impl *)test, 11); } return 0; }__main_block_func_0 参数改变了, 增加了一个 int a 的参数, 当然相应的调用的代码也要改变下, 至于其他的地方, 倒没啥变化.现在来稍微总结一下, 等于讲 Clang 把 Block 转成 objc 的对象, 涉及捕获auto 变量时就给 struct 加个外部变量同名的成员, 涉及 static 变量, 就给 struct 加个同名的指针; 如果是访问全局变量, 则会直接在函数内部使用到; 涉及多参数的就给 __main_block_func_0 加更多形参.关于 _NSConcreteStackBlock我们再来看最初的 hello world#import <stdio.h> int main(void) { @autoreleasepool { void (^test)(void) = ^{ printf("hello, world!\n"); }; test(); } return 0; }struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };可以看到有个 isa 的指针, 给 isa 传得是 &_NSConcreteStackBlock, 由此可以看出 Block 是一个 objc 的对象, 同时它的 isa 可能是 _NSConcreteStackBlock.通过 rewrite-objc 看到 Block 的类型是 _NSConcreteStackBlock, 此外还有另外两个 _NSConcreteGlobalBlock, _NSConcreteMallocBlock, 分别对应以下类型类型class指定因素__NSGlobalBlock___NSConcreteGlobalBlock没有访问 auto 变量时__NSStackBlock___NSConcreteStackBlock访问了 auto 变量__NSMallocBlock___NSConcreteMallocBlock__NSStackBlock__ 使用 copy如果对 __NSGlobalBlock__ 使用 copy, 它还是 __NSGlobalBlock__, 并不会改变. Block 使用 copy 后的结果class源区域copy 结果_NSConcreteGlobalBlockdata无动作_NSConcreteStackBlockstackstack -> heap_NSConcreteMallocBlockheap引用计数增加既然 Block 是 objc 对象, 那意味着我们可以#import <Foundation/Foundation.h> int main(void) { @autoreleasepool { void (^test)(void) = ^{ printf("hello, world!\n"); }; NSLog(@"%@", [test class]); // __NSGlobalBlock__ int a = 10; NSLog(@"%@", [^{ NSLog(@"hello world!, a = %d\n", a); } class]); // __NSStackBlock__ NSLog(@"%@", [[^{ NSLog(@"hello world!, a = %d, b = %d\n", a); } copy] class]); // __NSMallocBlock__ } return 0; }然后对比 rewrite 后的代码就会发现, 第一条 NSLog 后出来的是 __NSGlobalBlock__, 说明其类型是 _NSConcreteGlobalBlock, 然而 rewrite-objc 出来的却是 _NSConcreteStackBlock, 第二第三条的 Block 也都是 _NSConcreteStackBlock, 很早之前的 Clang rewrite-objc 出来的内容不是这样的 (至少我 2014 年看到的不是这样的), 这里就不深究了, 以实际执行时的结果为准. 不过这也算是一个好事, 因为我们用 Rust 包装 Block 时只要处理 _NSConcreteStackBlock 就行啦!其他其实还有一些 MRC 跟 ARC 相关的, 以及使用 objc 对象时的情况.使用 Rust 包装了解到上面关于 Block 的一些基本原理, 现在来尝试用 Rust 包装一下 Block, 内容来源 rust-block 这个 crate. 首先创建一个 Rust 项目, 直接cargo new block --lib然后把 lib.rs 的内容删掉, 写上这玩意enum Class {} #[cfg_attr( any(target_os = "macos", target_os = "ios"), link(name = "System", kind = "dylib") )] #[cfg_attr( not(any(target_os = "macos", target_os = "ios")), link(name = "BlocksRuntime", kind = "dylib") )] extern "C" { static _NSConcreteStackBlock: Class; }这里主要是把 _NSConcreteStackBlock extern 出来, 至于 enum Class {} 是 Rust 的一个技巧, 这里是为了让编译通过, 不想用它可以直接用 (). 至于#[cfg_attr( any(target_os = "macos", target_os = "ios"), link(name = "System", kind = "dylib") )] #[cfg_attr( not(any(target_os = "macos", target_os = "ios")), link(name = "BlocksRuntime", kind = "dylib") )]是预处理一下 extern 块, 前面一段适用于一般的 macOS/iOS 环境, 后面一段适用于带 BlocksRuntime 的 Linux 环境.然后照着 rewrite 后的 Cpp 代码的样子写一下 Rust#[repr(C)] struct BlockBase<A, R> { isa: *const Class, flags: c_int, _reserved: c_int, invoke: unsafe extern "C" fn(*mut Block<A, R>, ...) -> R, }这里 repr(C) 表示的是使用 C 的内存布局, 这里 A 跟 R 泛型表示的是参数类型跟返回结果, 接着我们要描述 Block#[repr(C)] struct ConcreteBlock<A, R, F> { base: BlockBase<A, R>, descriptor: BlockDescriptor<ConcreteBlock<A, R, F>>, } #[repr(C)] struct BlockDescriptor<B> { _reserved: c_ulong, block_size: c_ulong, copy_helper: unsafe extern "C" fn(&mut B, &B), dispose_helper: unsafe extern "C" fn(&mut B), }copy 跟 dispose这里多了两个叫 copy, dispose 的东西, 前面讲到的 Block 全是跟基础类型(譬如 int) 相关的行为, 如果跟 objc 对象打交道, rewrite-cpp 后就会生成 copy 跟 dispose, 主要是为了管理 objc 对象的内存, 我们可以来验证一下#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface Person : NSObject { @public int _number; } @end NS_ASSUME_NONNULL_END @implementation Person @end int main(void) { @autoreleasepool { Person *person = [[Person alloc] init]; person->_number = 10; void (^test)(void) = ^{ NSLog(@"%d", person->_number); }; test(); } return 0; }然后做一下 rewrite 操作// ... static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);} static struct __main_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}; // ...所以我们得在 Rust 这边加上这两个玩意, 由于这两个函数是 objc 管理的, 所以 Rust 这边主要是利用一下 drop 的行为unsafe extern "C" fn block_context_dispose<B>(block: &mut B) { std::ptr::read(block); } unsafe extern "C" fn block_context_copy<B>(_dst: &mut B, _src: &B) {}现在来定义一下 Block#[repr(C)] pub struct Block<A, R> { _base: PhantomData<BlockBase<A, R>>, }Block 内部是由 BlockBase 组成, 但其实并没有用到它, 所以直接用幽灵数据包裹一下, 接着写个 RcBlock 来包装一下 Block 结构体, 顺便把 _Block_copy _Block_release extern 出来, 在 RcBlock drop 时调用 _Block_release, 引用计数增加时调用 _Block_copyextern "C" { // ... fn _Block_copy(block: *const c_void) -> *mut c_void; fn _Block_release(block: *const c_void); } pub struct RcBlock<A, R> { ptr: *mut Block<A, R>, } impl<A, R> RcBlock<A, R> { pub unsafe fn new(ptr: *mut Block<A, R>) -> Self { RcBlock { ptr } } pub unsafe fn copy(ptr: *mut Block<A, R>) -> Self { let ptr = _Block_copy(ptr as *const c_void) as *mut Block<A, R>; RcBlock { ptr } } } impl<A, R> Clone for RcBlock<A, R> { fn clone(&self) -> Self { unsafe { RcBlock::copy(self.ptr) } } } impl<A, R> Deref for RcBlock<A, R> { type Target = Block<A, R>; fn deref(&self) -> &Self::Target { unsafe { &*self.ptr } } } impl<A, R> Drop for RcBlock<A, R> { fn drop(&mut self) { unsafe { _Block_release(self.ptr as *const c_void); } } }然后再来完善 ConcreteBlock, 主要是把 Rust 的闭包转换成 ConcreteBlock, 在此之前先弄个把参数抽象出来, 先弄个单个参数的, 比较好处理pub trait BlockArguments: Sized { unsafe fn call_block<R>(self, block: *mut Block<Self, R>) -> R; } impl<A> BlockArguments for A { unsafe fn call_block<R>(self, block: *mut Block<Self, R>) -> R { let invoke: unsafe extern "C" fn(*mut Block<Self, R>, A) -> R = { let base = block as *mut BlockBase<Self, R>; mem::transmute((*base).invoke) }; let a = self; invoke(block, a) } }然后可以考虑一下多个参数的怎么处理, 没有参数的又怎么处理. 只要把上面的 A 改成元组包装一下, 再用元组处理多个参数的情况impl<A> BlockArguments for (A,) { unsafe fn call_block<R>(self, block: *mut Block<Self, R>) -> R { let invoke: unsafe extern "C" fn(*mut Block<Self, R>, A) -> R = { let base = block as *mut BlockBase<Self, R>; mem::transmute((*base).invoke) }; let (a,) = self; invoke(block, a) } } impl<A, B> BlockArguments for (A, B) { unsafe fn call_block<R>(self, block: *mut Block<Self, R>) -> R { let invoke: unsafe extern "C" fn(*mut Block<Self, R>, A, B) -> R = { let base = block as *mut BlockBase<Self, R>; mem::transmute((*base).invoke) }; let (a, b) = self; invoke(block, a, b) } }不过这样太无脑了, 假如有 12 个参数就要写 12 遍, 写个宏先macro_rules! block_args_impl { ($($a:ident : $t:ident), *) => ( impl<$($t),*> BlockArguments for ($($t,)*) { unsafe fn call_block<R>(self, block: *mut Block<Self, R>) -> R { let invoke: unsafe extern "C" fn(*mut Block<Self, R> $(, $t)*) -> R = { let base = block as *mut BlockBase<Self, R>; mem::transmute((*base).invoke) }; let ($($a,)*) = self; invoke(block $(, $a)*) } } ); } block_args_impl!(); block_args_impl!(a: A); block_args_impl!(a: A, b: B); block_args_impl!(a: A, b: B, c: C); block_args_impl!(a: A, b: B, c: C, d: D); block_args_impl!(a: A, b: B, c: C, d: D, e: E); block_args_impl!(a: A, b: B, c: C, d: D, e: E, f: F); block_args_impl!(a: A, b: B, c: C, d: D, e: E, f: F, g: G); block_args_impl!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H); block_args_impl!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I); block_args_impl!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J); block_args_impl!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K); block_args_impl!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L);现在来定义个 IntoConcreteBlock 的 trait, 主要是把 Rust 闭包转化成 ConcreteBlock, 因为有多个参数的情况, 所以又要一对一式地实现对应个数的, 顺便先把解引用, 克隆之类的 trait 实现一下, copy 函数让 RcBlock 持有 blockpub trait IntoConcreteBlock<A>: Sized where A: BlockArguments, { type ReturnType; fn into_concrete_block(self) -> ConcreteBlock<A, Self::ReturnType, Self>; } impl<A, R, F> ConcreteBlock<A, R, F> where A: BlockArguments, F: IntoConcreteBlock<A, ReturnType = R>, { pub fn new(closure: F) -> Self { closure.into_concrete_block() } } impl<A, R, F> ConcreteBlock<A, R, F> { unsafe fn with_invoke(invoke: unsafe extern "C" fn(*mut Self, ...) -> R, closure: F) -> Self { ConcreteBlock { base: BlockBase { isa: &_NSConcreteStackBlock, flags: 1 << 25, _reserved: 0, invoke: mem::transmute(invoke), }, descriptor: Box::new(BlockDescriptor::new()), closure, } } } impl<A, R, F> ConcreteBlock<A, R, F> where F: 'static, { pub fn copy(self) -> RcBlock<A, R> { unsafe { let mut block = self; let copied = RcBlock::copy(&mut *block); mem::forget(block); copied } } } impl<A, R, F> Deref for ConcreteBlock<A, R, F> { type Target = Block<A, R>; fn deref(&self) -> &Self::Target { unsafe { &*(&self.base as *const _ as *const Block<A, R>) } } } impl<A, R, F> DerefMut for ConcreteBlock<A, R, F> { fn deref_mut(&mut self) -> &mut Block<A, R> { unsafe { &mut *(&mut self.base as *mut _ as *mut Block<A, R>) } } } impl<A, R, F> Clone for ConcreteBlock<A, R, F> where F: Clone, { fn clone(&self) -> Self { unsafe { ConcreteBlock::with_invoke(mem::transmute(self.base.invoke), self.closure.clone()) } } }参数相关的, 先把一个的情况写出来impl<A, R, X> IntoConcreteBlock<(A,)> for X where X: Fn(A) -> R, { type ReturnType = R; fn into_concrete_block(self) -> ConcreteBlock<(A,), R, X> { unsafe extern "C" fn concrete_block_invoke_args1<A, R, X>( block_ptr: *mut ConcreteBlock<A, R, X>, a: A, ) -> R where X: Fn(A) -> R, { let block = &*block_ptr; (block.closure)(a) } let f: unsafe extern "C" fn(*mut ConcreteBlock<A, R, X>, a: A) -> R = concrete_block_invoke_args1; unsafe { ConcreteBlock::with_invoke(mem::transmute(f), self) } } }继续用宏处理macro_rules! concrete_block_impl { ($f:ident) => ( concrete_block_impl!($f,); ); ($f:ident, $($a:ident : $t:ident),*) => ( impl<$($t,)* R, X> IntoConcreteBlock<($($t,)*)> for X where X: Fn($($t,)*) -> R { type ReturnType = R; fn into_concrete_block(self) -> ConcreteBlock<($($t,)*), R, X> { unsafe extern fn $f<$($t,)* R, X>( block_ptr: *mut ConcreteBlock<($($t,)*), R, X> $(, $a: $t)*) -> R where X: Fn($($t,)*) -> R { let block = &*block_ptr; (block.closure)($($a),*) } let f: unsafe extern fn(*mut ConcreteBlock<($($t,)*), R, X> $(, $a: $t)*) -> R = $f; unsafe { ConcreteBlock::with_invoke(mem::transmute(f), self) } } } ); } concrete_block_impl!(concrete_block_invoke_args0); concrete_block_impl!(concrete_block_invoke_args0, a: A); concrete_block_impl!(concrete_block_invoke_args0, a: A, b: B); concrete_block_impl!(concrete_block_invoke_args0, a: A, b: B, c: C); concrete_block_impl!(concrete_block_invoke_args0, a: A, b: B, c: C, d: D); concrete_block_impl!(concrete_block_invoke_args0, a: A, b: B, c: C, d: D, e: E); concrete_block_impl!(concrete_block_invoke_args0, a: A, b: B, c: C, d: D, e: E, f: F); concrete_block_impl!(concrete_block_invoke_args0, a: A, b: B, c: C, d: D, e: E, f: F, g: G); concrete_block_impl!(concrete_block_invoke_args0, a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H); concrete_block_impl!(concrete_block_invoke_args0, a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I); concrete_block_impl!(concrete_block_invoke_args0, a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J); concrete_block_impl!(concrete_block_invoke_args0, a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K); concrete_block_impl!(concrete_block_invoke_args0, a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L);基本上已经用 Rust 把 Block 包装好了. 了解 objc Block 原理, 再配合上 Rust 的代码风格. 现在就是试试在 objc 端调用 Rust 的 Block 试试效果.在 objc 项目中试用先在 lib.rs 写上以下内容#[no_mangle] unsafe extern "C" fn sum(block: &Block<(i32, i32), i32>) -> i32 { block.call((1, 2)) + 1 }主要是调用 block 后加 1然后 Cargo.toml 加上[lib] name = "block" crate-type = ["staticlib", "cdylib"]后执行cargo build --release就能生成静态库, 为了简单起见, 直接写个 main.m 然后用 clang 编译同时链接静态库, 当然别忘了加上头文件, 内容如下// LLBlock.h #ifndef LLBlock_h #define LLBlock_h #import <Foundation/Foundation.h> int32_t sum(int32_t (^block)(int32_t, int32_t)); #endif /* LLBlock_h */// main.m #import "LLBlock.h" int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"%d", sum(^int32_t(int32_t a, int32_t b) { return a + b; })); } return 0; }然后用这个命令编译链接生成一个可执行文件cc ./main.m -framework Foundation ./libblock.a -o main && ./main只要是在 macOS 环境下, 应该能看到数字 4 的输出至此, 我们的任务完成了.
2022年04月25日
166 阅读
0 评论
0 点赞
2021-02-23
通过 cmake 混合构建 Rust & Cpp
Rust 提供了非常好用的 FFI, 可以方便我们将 Rust 代码跟 C/Cpp 之间互操. 在开始之前先弄个基本的种子例子, 我决定还是用 SDL2 来做演示场景. 后续有可能会尝试一下音视频之类的, SDL2 很适合拿来学习.准备工作先把 SDL2 装上, macOS 系统下就访问 brew 中文 官网, 可以找到 brew 的安装方法/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"如果已经安装了 brew, 那就直接安装 sdl2brew install sdl2然后再用 cmake 配置一个 Cpp 项目环境, 没有装 cmake 也可以用 brew 安装brew install cmake接下来就是mkdir rust_client && cd rust_client mkdir gui touch gui/main.cpp touch CMakeLists.txt我们现在开始编辑 CMakeLists.txtcmake_minimum_required(VERSION 3.15) set(SHORT_NAME Sdl2Player) project(${SHORT_NAME}) add_subdirectory(rs) add_subdirectory(gui)然后在 gui 目录下编辑 CMakeLists.txt, 具体 SDL2 目录以自己的环境为准set(CMAKE_CXX_STANDARD 14) include(FindSDL2.cmake) find_package(SDL2 REQUIRED) include_directories(${SDL2_INCLUDE_DIRS}) set(SOURCE main.cpp) add_executable(gui ${SOURCE}) get_target_property(CLIENT_DIR rs LOCATION) target_link_libraries(gui {SDL2_LIBRARIES}) target_link_libraries(gui{CLIENT_DIR}/librs.dylib) add_dependencies(gui rs)这里的 FindSDL2.cmake 是一个 github 上人家的配置文件, 解决找不到 SDL2 的头文件的问题. 可以访问该地址下载 SDL2Test.还有个问题 get_target_property(RS_DIR rs LOCATION) 是后面创建的 Rust 项目里 CMakeLists.txt 定义的, 现在先这样写.然后我们在 main.cpp 里用 SDL2 创建个窗口#include "SDL.h" enum { WINDOW_WIDTH = 960, WINDOW_HEIGHT = 544, SCREEN_CENTER = SDL_WINDOWPOS_CENTERED_MASK, }; int main(int argc, char **argv) { if (SDL_Init(SDL_INIT_VIDEO) < 0) return -1; SDL_Window *window = SDL_CreateWindow("Rust FFI Demo", SCREEN_CENTER, SCREEN_CENTER, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); if (window == nullptr) return -1; SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, 0); if (renderer == nullptr) return -1; SDL_Event event; while (true) { if (SDL_PollEvent(&event)) { if (SDL_QUIT == event.type) break; SDL_SetRenderDrawColor(renderer, 100, 0, 0, 255); SDL_RenderClear(renderer); SDL_RenderPresent(renderer); } } SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; }终于轮到 Rust 项目上场了, 直接在项目根目录下 cargo 走起cargo new --lib rs现在当前项目结构大概长这样$ tree -L 3 . ├── CMakeLists.txt ├── build ├── gui │ ├── CMakeLists.txt │ ├── FindSDL2.cmake │ └── main.cpp └── rs ├── CMakeLists.txt ├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── target └── debug打开 Cargo.toml 文件编辑一下[package] name = "rs" version = "0.1.0" authors = ["Author <xxx@example.com>"] edition = "2018" [dependencies] [lib] crate-type = ["cdylib"]我们主要关注点 [lib] 下的内容, 添加 crate-type ["cdylib"] 这里意思是说创建 C 动态库, 其实也可以创建静态库, 具体参数是 crate-type = ["cdylib", "staticlib"] 其实也可以指定编译出来的库名字, 譬如指定为 app, 就是添加 name = "app"重点来了, 创建 rs 目录下的 CMakeLists.txt, 添加如下内容if (CMAKE_BUILD_TYPE STREQUAL "Debug") set(CARGO_CMD cargo build) set(TARGET_DIR "debug") else () set(CARGO_CMD cargo build --release) set(TARGET_DIR "release") endif () set(RS_SO "{CMAKE_CURRENT_BINARY_DIR}/{TARGET_DIR}/librs.dylib") add_custom_target(rs ALL COMMENT "Compiling rs module" COMMAND CARGO_TARGET_DIR={CMAKE_CURRENT_BINARY_DIR}{CARGO_CMD} COMMAND cp {RS_SO}{CMAKE_CURRENT_BINARY_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) set_target_properties(rs PROPERTIES LOCATION ${CMAKE_CURRENT_BINARY_DIR}) add_test(NAME rs_test COMMAND cargo test WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})现在为了保证项目干净整洁, 我们可以在项目根目录创建个 build, 然后再用 cmake 生成 make filemkdir build && cd build cmake .. make ./gui/gui看看效果, 我看到的效果是一个暗红色空白窗口. 好了现在整个项目算了配置完成了.Cpp 调用 Rust通常我们学习当然是从 Hello World 开始的啦, 那就用控制台打印一下吧. 编辑 Rust src 下的 lib.rs 文件#[no_mangle] pub extern "C" fn hello_rust() { println!("Hello Rust!"); }日常 Hello Rust, no_mangle 保证函数签名不被混淆, 这一步很重要, 不然 Cpp 调用的时候就会找不到对应的函数, 然后我们用 Cpp 调用一下.// ... #include "rust.hpp" // ... int main(int argc, char **argv) { // ... bool quit = true; while (quit) { if (SDL_PollEvent(&event)) { if (SDL_QUIT == event.type) break; if (SDL_KEYDOWN == event.type) { switch (event.key.keysym.sym) { case SDLK_ESCAPE: quit = false; break; case SDLK_j: hello_rust(); break; } } // ... } } // ... return 0; }肯定注意到有个 #include "rust.hpp", 原来是把 hello_rust 函数 extern 出来// rust.hpp extern "C" { void hello_rust(); };然后在 SDL 主线程事件循环中通过键盘字母 j 触发调用, 也稍微改了点 退出的逻辑. 最后别忘了在 gui 目录下 CMakeLists.txt 中修改一下 SOURCE# ... set(SOURCE rust.hpp main.cpp) # ...最后重复构建操作cd build cmake .. make执行一下看看效果, 只要按了字母 j 就会在控制台打印 Hello Rust! 看样子简单的 Cpp 调用 Rust 完成了.项目地址https://github.com/limitLiu/cpp-with-rust.git
2021年02月23日
47 阅读
0 评论
0 点赞
1
2
3