首页
统计
留言板
关于
Search
1
Rust 包装 objc Block
86 阅读
2
02. Rust 内存管理 Copy & Clone(上)
85 阅读
3
Flutter 调用 Rust 生成的 共享/静态 库
79 阅读
4
纯 C 写个 iOS App(误)
67 阅读
5
用 Rust 开发 iOS 应用(粗糙版)
64 阅读
默认分类
Rust
Apple
iOS
Swift
Android
emulator
NES
登录
/
注册
Search
标签搜索
Rust
iOS
NES
Swift
Android
杂项
limit
累计撰写
18
篇文章
累计收到
0
条评论
首页
栏目
默认分类
Rust
Apple
iOS
Swift
Android
emulator
NES
页面
统计
留言板
关于
搜索到
11
篇与
的结果
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日
79 阅读
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日
34 阅读
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日
86 阅读
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日
28 阅读
0 评论
0 点赞
2021-02-23
Rust 数据结构-动手实现 Vec
正文讲屁话阶段Rust 的 Vec 其实是动态数组, 很多语言内置动态数组, 譬如 JavaScript Python这类, 像 Rust 这种具有内存控制能力的语言, 就选择了标准库内置动态数组.基本用法let mut v1 : Vec<i32> = vec![]; dbg!(v1.len(), v1.capacity()); for i in 1..10 { v1.push(i); } dbg!(&v1);布局现在直接写一下 Rust 动态数组直观实现pub struct MyVec<T> { ptr: *mut T, cap: usize, len: usize, }这样直接编译能通过, 但是 ptr 这个裸指针不能让 Drop check 正常工作, 因为直接在这里使用裸指针 Drop 检查器会认为你没有持有任何值, 因此我们可以用 Unique<T> 处理祼指针 *mut T, 通过它内部的 PhantomData 来帮助 Drop check 工作.Unique<T> 可以封装祼指针T 是可变的可以进行 drop 检查T 实现了 Send/Sync, 该指针也具备 Send/Sync 特性, 等于讲线程安全指针具备非空性既然如此自己实现一个吧, 解析源码自己写不会浪费什么时间, 还能体会别人的设计, 顺道实现一下 Send/Syncuse std::marker::PhantomData; struct MyUnique<T: ?Sized> { ptr: *const T, _marker: PhantomData<T>, } unsafe impl<T: Send + ?Sized> Send for MyUnique<T> {} unsafe impl<T: Sync + ?Sized> Sync for MyUnique<T> {} impl<T: ?Sized> MyUnique<T> { #[inline] pub fn new(ptr: *mut T) -> Option<Self> { if !ptr.is_null() { Some(unsafe { MyUnique { ptr: ptr as _, _marker: PhantomData, } }) } else { None } } #[inline] pub const fn as_ptr(&self) -> *mut T { self.ptr as *mut T } }我们看到有个 ?Sized 的 trait bound 这玩意其实是指定一下泛型的特性, 意思是指编译时确定大小, 默认情况下直接写 T 就是 Sized 了, 加了个问号就会放宽约束的范围, 编译时不确定大小的也被接受. 加上 #[inline] 这个属性就表示内联函数, 因为这几个函数可能会经常使用到, 内联会带点加速效果.内存分配之后我们要考虑初始化容器了, 如果容器放了东西肯定会开辟内存空间, 但是初始化的情况容器应该是空的, 既然是空的肯定不会分配内存, 那就用 MyUnique 建个空的东西imp<T: ?Sized> MyUnique<T> { #[inline] pub const unsafe fn new_unchecked(ptr: *mut T) -> Self { MyUnique { ptr: ptr as _, _marker: PhantomData, } } } impl<T> MyUnique<T> { pub const fn empty() -> Self { unsafe { MyUnique::new_unchecked(mem::align_of::<T>() as *mut T) } } } pub struct MyVec<T> { ptr: MyUnique<T>, cap: usize, len: usize, } impl<T> MyVec<T> { fn new() -> Self { assert_ne!(mem::size_of::<T>(), 0, "We're not ready to handle ZSTs"); MyVec { ptr: MyUnique::empty(), len: 0, cap: 0, } } }接下来写一下内存分配相关的代码, 既然我们需要分配内存, 这块其实没什么东西, 就是读一下内存分配的文档然后使用起来, 当然也要考虑对齐. 其实这是个扩容处理use std::alloc::{handle_alloc_error, realloc, Layout}; use std::mem; impl<T> MyVec<T> { fn grow(&self) -> Self { unsafe { let (new_cap, ptr) = if self.cap == 0 { let ptr = alloc(Layout::array::<T>(1).unwrap()); (1, ptr) } else { let new_cap = self.cap * 2; let layout = Layout::array::<T>(self.cap).unwrap(); let ptr = realloc(self.ptr.as_ptr() as *mut _, layout, layout.size()); if ptr.is_null() { handle_alloc_error(Layout::from_size_align_unchecked( new_cap * elem_size, mem::align_of::<T>(), )); } (new_cap, ptr) }; Self { ptr: MyUnique::new_unchecked(ptr as *mut _), cap: new_cap, len: self.len, } } } }push & pop上面已经做到能分配内存了, 接下来自然是实现基本的功能.先分析一波 push 的行为, push 就是往动态数组添加元素, 如果满了就需要重新分配内存, 其实就是调用 grow, 每次添加元素后长度也要相应 +1, 还需要对相应地址写元素. 写入行为用 std::ptr 的 write 函数来处理, 相应的 pop 也很好理解, 只要把最后一个读取出来同时把长度 -1use std::{mem, ptr}; impl<T> MyVec<T> { pub fn ptr(&self) -> *mut T { self.ptr.as_ptr() } pub fn push(&mut self, element: T) { if self.len == self.cap { self.grow(); } unsafe { ptr::write(self.ptr().add(self.len), element); } self.len += 1; } pub fn pop(&mut self) -> Option<T> { if self.len == 0 { None } else { self.len -= 1; unsafe { Some(ptr::read(self.ptr().add(self.len))) } } } }回收资源Rust 的机制让我们处理这个问题非常简单, 只需要实现一下 trait Dropimpl<T> Drop for MyVec<T> { fn drop(&mut self) { let elem_size = mem::size_of::<T>(); if elem_size != 0 { while let Some(_) = self.pop() {} unsafe { dealloc(self.ptr() as *mut _, Layout::array::<T>(self.cap).unwrap()); } } } }解引用至此, 已经实现了一个简单的数据结构, 但是我们还没法跟 slice 相通, 真实的 Vec 不是这样的, 所以当下我们应该实现一下自动解引用, 只要实现了下面这些东西, 我们就可以用 slice 的提供的接口了use std::ops::{Deref, DerefMut}; impl<T> Deref for MyVec<T> { type Target = [T]; fn deref(&self) -> &[T] { unsafe { std::slice::from_raw_parts(self.ptr(), self.len) } } } impl<T> DerefMut for MyVec<T> { fn deref_mut(&mut self) -> &mut [T] { unsafe { std::slice::from_raw_parts_mut(self.ptr(), self.len) } } }插入跟删除插入插入的行为其实就是把当前要插入的位置之后的所有元素分别向右移动一位, 譬如一个数组 [1, 2, 3], 我要把 10 插入 索引 1 位置(就是元素 2), 那么 10 的下标就是 1, 同时元素 2 跟 3 的下标就是 2 跟 3, 最后就变成了 [1, 10, 2, 3].impl<T> MyVec<T> { // ... pub fn insert(&mut self, index: usize, element: T) { if self.cap == self.len { self.grow(); } unsafe { if index < self.len { ptr::copy(self.ptr().add(index), self.ptr().add(index + 1), self.len - index); } ptr::write(self.ptr().add(index), element); self.len += 1; } } }删除删除的行为也很好理解, 跟插入反着来就可以了, 只要把要删除的位置之后所有的下标向左移动一位, 譬如现在把之前插入后的数组 [1, 10, 2, 3] 10 给删除掉, 10 所在的下标是 1, 后面的元素的下标分别是 2 跟 3, 后面的 -1 就完成了.impl<T> MyVec<T> { // ... pub fn remove(&mut self, index: usize) -> T { assert!(index < self.len, "index out of bounds"); unsafe { self.len -= 1; let result = ptr::read(self.ptr().add(index)); ptr::copy(self.ptr().add(index + 1), self.ptr().add(index), self.len - index); result } } }IntoIter这趟开始处理一下 Vec 才有的迭代器, 其实只要实现了自动解引用的 trait, 就可以使用 slice 的 iter 还有 iter_mut, 但是 slice 是没有 into_iter 的, 所以我们得实现一下.现在有个问题, 既然已经有了 slice 的迭代器功能了, 我们为什么要实现这个 IntoIter?我们可以看到一个 Vec 可以直接用 for 进行循环遍历, 原因就是只要一个自定义的类型实现了 IntoIter 就具备能被 for 迭代的能力let v = vec![1, 2, 3]; for i in v { dbg!(i); }现在可以用两个指针来处理迭代器的操作, 一个在开头, 一个在结尾后面那一个, 只要开头的指针跟结尾后一个的指针地址相同, 就表明迭代结束了.[1, 2, 3, 4, 5, sth] ^ ^ start end现在来建立个迭代器结构, 大概长这样struct IntoIter<T> { start: *const T, end: *const T, }当然后续还要处理内存相关的, 所以我们应该把 Vec 分配的空间讯息保存一下, 当然还要把 MyVec 转化成 IntoIter 类型struct IntoIter<T> { start: *const T, end: *const T, buf: MyUniuqe<T>, cap: usize, } impl<T> MyVec<T> { fn into_iter(self) -> IntoIter<T> { let MyVec { ptr, cap, len } = self; mem::forget(self); unsafe { IntoIter { buf: ptr, cap, start: ptr.as_ptr(), end: if cap == 0 { ptr.as_ptr() } else { ptr.as_ptr().add(len) }, } } } }还要实现一下迭代器, size_hint 是仿写标准库的, 主要作用是表达剩余可迭代元素数量上下界, 下面是 next 相关的操作 impl<T> Iterator for IntoIter<T> { type Item = T; fn next(&mut self) -> Option<T> { if self.start == self.end { None } else { unsafe { let result = ptr::read(self.start); self.start = self.start.offset(1); Some(result) } } } fn size_hint(&self) -> (usize, Option<usize>) { let len = (self.end as usize - self.start as usize) / mem::size_of::<T>(); (len, Some(len)) } }还有 next_back 的操作 impl<T> DoubleEndedIterator for IntoIter<T> { fn next_back(&mut self) -> Option<T> { if self.start == self.end { None } else { unsafe { self.end = self.end.offset(-1); Some(ptr::read(self.end)) } } } }为了处理内存相关的, 我们要给 IntoIter 实现一下 Drop traitimpl<T> Drop for IntoIter<T> { fn drop(&mut self) { if self.cap != 0 { for _ in &mut *self {} unsafe { dealloc(self.buf.as_ptr() as *mut _, Layout::array::<T>(self.cap).unwrap()); } } } }RawVec现在继续重构代码, 因为我们分别给 IntoIter 跟 MyVec 实现了一遍 Drop, 所以重构一下代码是有必要的pub struct MyVec<T> { buf: RawVec<T>, len: usize, } impl<T> MyVec<T> { pub fn push(&mut self, element: T) { if self.len == self.cap() { self.buf.grow(); } unsafe { ptr::write(self.ptr().add(self.len), element); } self.len += 1; } pub fn pop(&mut self) -> Option<T> { if self.len == 0 { None } else { self.len -= 1; unsafe { Some(ptr::read(self.ptr().add(self.len))) } } } pub fn insert(&mut self, index: usize, element: T) { assert!(index <= self.len, "index out of bounds"); if self.cap() == self.len { self.buf.grow(); } unsafe { if index < self.len { ptr::copy(self.ptr().add(index), self.ptr().add(index + 1), self.len - index); } ptr::write(self.ptr().add(index), element); self.len += 1; } } pub fn remove(&mut self, index: usize) -> T { assert!(index < self.len, "index out of bounds"); unsafe { self.len -= 1; let result = ptr::read(self.ptr().add(index)); ptr::copy(self.ptr().add(index + 1), self.ptr().add(index), self.len - index); result } } } impl<T> Drop for MyVec<T> { fn drop(&mut self) { while let Some(_) = self.pop() {} } } struct IntoIter<T> { start: *const T, end: *const T, _buf: RawVec<T>, } impl<T> Iterator for IntoIter<T> { type Item = T; fn next(&mut self) -> Option<T> { if self.start == self.end { None } else { unsafe { let result = ptr::read(self.start); self.start = self.start.offset(1); Some(result) } } } fn size_hint(&self) -> (usize, Option<usize>) { let len = (self.end as usize - self.start as usize) / mem::size_of::<T>(); (len, Some(len)) } } impl<T> DoubleEndedIterator for IntoIter<T> { fn next_back(&mut self) -> Option<T> { if self.start == self.end { None } else { unsafe { self.end = self.end.offset(-1); Some(ptr::read(self.end)) } } } } impl<T> Drop for IntoIter<T> { fn drop(&mut self) { for _ in &mut *self {} } } struct RawVec<T> { ptr: MyUnique<T>, cap: usize, } impl<T> RawVec<T> { fn new() -> Self { RawVec { ptr: MyUnique::empty(), cap: 0, } } fn grow(&self) -> Self { unsafe { let (new_cap, ptr) = if self.cap == 0 { let ptr = alloc(Layout::array::<T>(1).unwrap()); (1, ptr) } else { let new_cap = self.cap * 2; let layout = Layout::array::<T>(self.cap).unwrap(); let ptr = realloc(self.ptr.as_ptr() as *mut _, layout, layout.size()); if ptr.is_null() { handle_alloc_error(Layout::from_size_align_unchecked( new_cap * mem::size_of::<T>(), mem::align_of::<T>(), )); } (new_cap, ptr) }; Self { ptr: MyUnique::new_unchecked(ptr as *mut _), cap: new_cap, } } } } impl<T> Drop for RawVec<T> { fn drop(&mut self) { if self.cap != 0 { unsafe { dealloc(self.ptr.as_ptr() as *mut _, Layout::array::<T>(self.cap).unwrap()); } } } }抽取迭代操作现在我们基本的 Vec 结构已经做出来了, 现在仿照之前的 RawVec 做一份封装.struct RawValIter<T> { start: *const T, end: *const T, } impl<T> RawValIter<T> { unsafe fn new(slice: &[T]) -> Self { RawValIter { start: slice.as_ptr(), end: if slice.len() == 0 { slice.as_ptr() } else { slice.as_ptr().offset(slice.len() as isize) }, } } } impl<T> Iterator for RawValIter<T> { type Item = T; fn next(&mut self) -> Option<T> { if self.start == self.end { None } else { unsafe { let result = ptr::read(self.start); self.start = self.start.offset(1); Some(result) } } } fn size_hint(&self) -> (usize, Option<usize>) { let len = (self.end as usize - self.start as usize) / mem::size_of::<T>(); (len, Some(len)) } } impl<T> DoubleEndedIterator for RawValIter<T> { fn next_back(&mut self) -> Option<T> { if self.start == self.end { None } else { unsafe { self.end = self.end.offset(-1); Some(ptr::read(self.end)) } } } }然后改造一下迭代器, 现在只要在各函数内部调用 RawValIter 的实现就行了struct IntoIter<T> { _buf: RawVec<T>, iter: RawValIter<T>, } impl<T> Drop for MyVec<T> { fn drop(&mut self) { while let Some(_) = self.pop() {} } } impl<T> Iterator for IntoIter<T> { type Item = T; fn next(&mut self) -> Option<T> { self.iter.next() } fn size_hint(&self) -> (usize, Option<usize>) { self.iter.size_hint() } } impl<T> DoubleEndedIterator for IntoIter<T> { fn next_back(&mut self) -> Option<T> { self.iter.next_back() } } impl<T> Drop for IntoIter<T> { fn drop(&mut self) { for _ in &mut self.iter {} } }处理 Zero-Sized Types通常情况下, Rust 是不需要处理 Zero-Sized Types 的, 但是现在我们的代码中有大量关于裸指针的操作, 假如给分配器传递 zst, 会导致未定义行为, 对 zst 裸指针进行 offset 是一个 no-op 行为.先把 new 函数的 cap 处理一下, 如果是 size_of 处理出来的 T 是 0 的情况, 就给 0 按位取反(usize::MAX), 因为 T 的 size_of 为 0 其实不需要开辟内存, 反正你存进来的都是 0, 逻辑上不会占用内存.impl<T> RawVec<T> { fn new() -> Self { let cap = if mem::size_of::<T>() == 0 { !0 } else { 0 }; RawVec { ptr: MyUnique::empty(), cap, } } }然后就是 grow 函数也处理一下impl<T> RawVec<T> { // ... fn grow(&self) -> Self { unsafe { let elem_size = mem::size_of::<T>(); assert_ne!(elem_size, 0, "capacity overflow"); let (new_cap, ptr) = if self.cap == 0 { let ptr = alloc(Layout::array::<T>(1).unwrap()); (1, ptr) } else { let new_cap = self.cap * 2; let layout = Layout::array::<T>(self.cap).unwrap(); let ptr = realloc(self.ptr.as_ptr() as *mut _, layout, layout.size()); (new_cap, ptr) }; if ptr.is_null() { handle_alloc_error(Layout::from_size_align_unchecked( new_cap * elem_size, mem::align_of::<T>(), )); } Self { ptr: MyUnique::new_unchecked(ptr as *mut _), cap: new_cap, } } } }RawVec 的 Drop 也需要处理, 其实之前的实现也可以, 但是假装对齐一下吧!impl<T> Drop for RawVec<T> { fn drop(&mut self) { let elem_size = mem::size_of::<T>(); if self.cap != 0 && elem_size != 0 { unsafe { dealloc( self.ptr.as_ptr() as *mut _, Layout::from_size_align_unchecked(self.cap * elem_size, mem::align_of::<T>()), ); } } } }然后是 RawValIter 的 zst 处理impl<T> RawValIter<T> { unsafe fn new(slice: &[T]) -> Self { RawValIter { start: slice.as_ptr(), end: if mem::size_of::<T>() == 0 { ((slice.as_ptr() as usize) + slice.len()) as *const _ } else if slice.len() == 0 { slice.as_ptr() } else { slice.as_ptr().offset(slice.len() as isize) }, } } }迭代器也处理一下, size_hint 除数为 0 的情况需要处理impl<T> Iterator for RawValIter<T> { type Item = T; fn next(&mut self) -> Option<T> { if self.start == self.end { None } else { unsafe { let result = ptr::read(self.start); self.start = if mem::size_of::<T>() == 0 { (self.start as usize + 1) as *const _ } else { self.start.offset(1) }; Some(result) } } } fn size_hint(&self) -> (usize, Option<usize>) { let elem_size = mem::size_of::<T>(); let len = (self.end as usize - self.start as usize) / (if elem_size == 0 { 1 } else { elem_size }); (len, Some(len)) } } impl<T> DoubleEndedIterator for RawValIter<T> { fn next_back(&mut self) -> Option<T> { if self.start == self.end { None } else { unsafe { self.end = if mem::size_of::<T>() == 0 { (self.end as usize - 1) as *const _ } else { self.end.offset(-1) }; Some(ptr::read(self.end)) } } } }with_capacity这个实现一下差不多算结束了, 先把 MyVec 改一下impl<T> MyVec<T> { // ... pub fn with_capacity(capacity: usize) -> MyVec<T> { MyVec { buf: RawVec::with_capacity(capacity), len: 0 } } }然后给 RawVec 添加个接口impl<T> RawVec<T> { fn new() -> Self { let cap = if mem::size_of::<T>() == 0 { !0 } else { 0 }; RawVec { ptr: MyUnique::empty(), cap, } } pub fn with_capacity(cap: usize) -> Self { RawVec::allocate_in(cap, None) } fn allocate_in(cap: usize, p: Option<MyUnique<T>>) -> Self { unsafe { let elem_size = mem::size_of::<T>(); assert_ne!(elem_size, 0, "capacity overflow"); let (new_cap, ptr) = if cap == 0 { let ptr = alloc(Layout::array::<T>(1).unwrap()); (1, ptr) } else { if let Some(some_p) = p { let new_cap = cap * 2; let layout = Layout::array::<T>(cap).unwrap(); let ptr = realloc(some_p.as_ptr() as *mut _, layout, layout.size()); (new_cap, ptr) } else { let ptr = alloc(Layout::array::<T>(cap).unwrap()); (cap, ptr) } }; if ptr.is_null() { handle_alloc_error(Layout::from_size_align_unchecked( new_cap * elem_size, mem::align_of::<T>(), )); } Self { ptr: MyUnique::new_unchecked(ptr as *mut _), cap: new_cap, } } } fn grow(&self) -> Self { RawVec::allocate_in(self.cap, Some(self.ptr)) } }现在已经把 grow 抽取出来, 同时给 with_capacity 调用Drain目前待定吧, 这个稍微麻烦点, 还没想好怎么讲.Git 托管最后把代码放上来https://e.coding.net/limitLiu/algorithms-with-rust.git
2021年02月23日
8 阅读
0 评论
0 点赞
1
2
3