本篇文章是受Databend团队邀请,作为《Rust培养提高计划》公开课程的讲稿而书写的。 课程相关录像可以在Databend的Bilibili账号【Databend】找到 讲师本人的微信公众号是:【极客幼稚园】 讲师本人的个人博客是: http://blog.ideawand.com 讲师本人的B站账号是: 【爆米花胡了】(注意胡是胡萝卜的胡) https://space.bilibili.com/500416539
特别提示: 该文长 26000+ 字, 阅读时间可能需要超过 60 分钟。 建义先保存收藏观看。 本次分享只是 Rust FFI 中的一部分,其中目录中标灰的内容还没讲到,还会有后续的更新。 可以关注 Databend 公众号了解最新动态。
课程 Demo 代码: https://github.com/myrfy001/rust_golang_ffi_demo
Rust 培养提高计划回放 : http://t.cn/A6M4JIOx
分享背景:
减弱大家在生产环境中引入Rust的恐惧感,如果你还在犹豫、观望,不妨get your hands dirty first, 然后大家就可以放开了去搞了。 了解Rust为什么比一些常用的语言(如Golang、Java等)更适合开发通用二进制库 了解一下Rust开发C-FFI库的方法,并介绍一些个人实际使用中遇到的问题(也可能是我太菜) 了解一下Golang中Cgo的使用 有很多朋友在纠结Golang和Rust选哪个,通过本次分享,加深对两个语言的了解,为大家做选择多一些参考
分享目录
Get hands dirty Rust在开发二进制库上的优势 为什么选择Golang作为调用示例 配置一个Rust项目,使之能够编译出满足C-FFI的动态链接库 开发一个Go项目,调用Rust编写的库 Case By Case,介绍常见的应用场景 字符串的传递 结构体以及函数方法 回调函数的使用 FFI接口处的并发安全问题 错误处理 性能测试:用Rust重写Go模块,真的会更快吗?
目标听众
对C语言有大致了解,大学C语言基础就够了 熟悉Go、Python、Java这类具有GC的语言,但对C这样需要手工管理内存的语言不熟悉的同学 了解Rust,并希望将Rust与其他语言配合使用的同学 熟悉Go语言,但不了解cgo使用方法的同学 如果你已经熟练掌握的Rust、C/C++、Go,那么这个分享可能过于简单,建议直接Clone Databend的代码,开始为Databend贡献代码~逃~~
Rust在开发二进制库上的优势
各种数据结构在内存中是如何布局存储的 函数调用时,参数如何传递,返回值如何传递(例如,是用寄存器来传递,还是用栈来传递?) 去哪里申请内存空间(使用的内存分配器是什么?),申请使用的内存空间,何时释放,由谁来释放(有没有GC?)?
NN * (N-1)ABIFFI
A类:是没有虚拟机、运行时,需要开发者自己管理内存的语言,例如C、C++、Rust等。 B类:是具有虚拟机、具有运行时等协助管理内存的组件的语言,例如Java、Golang、Python、PHP等。
from ctypes import cdll, c_char, c_char_p
libc = cdll.LoadLibrary('libc.so.6')
strchr = libc.strchr
strchr.argtypes = [c_char_p, c_char]
strchr.restype = c_char_p
substr = strchr('abcdef', 'd')
if substr:
print(substr)
ctypes
char *charchar *charstr
1+1
-buildmode=c-shared
A类中的语言相互调用,往往是最原生、最高效的。 B类中的语言相互调用,往往伴随着巨大的额外开销,甚至开销过大以至于失去折腾这些跨语言调用的价值。 每种语言都可能处于调用者和被调用者两种角色,而一门语言扮演两种角色时,其支持程度可能是不一样的: B类语言通常都支持调用A类语言开发的库函数,而且开销相对较小,因为B类语言通常比较新,为了适配老生态,他们都会去主动靠拢支持与C语言开发的库的调用。 A类语言如果想调用B类语言开发的函数,就要看B语言的诚意,因为A类语言是前辈,它们对新的语言特性、实现方式一无所知,后辈们如果希望得到前辈的重用,那就要自己提供出满足前辈价值观的接口。 通常,B类调用A类会容易实现一些(只需要做类型转换),而A类调用B类则会困难一些(需要初始化B类语言的运行时、解释器等)。多数语言对B类调A类的支持会优于A类调B类。
为什么选择Golang作为调用示例
Golang 目前应用非常广泛,熟悉Golang的小伙伴们也比较多,这样可能会有更多的受众。 最近使用Rust或Go开发的云原生项目增长非常快,因为这两种语言的应用场景在云原生方面有重合,所以研究一下二者的融合就很有意思。 Golang 具有自己的运行时、自己的栈结构、自己的内存分配器,因此相比较于Python这类胶水语言,Golang调用CFFI函数库更有难度,我们喜欢做一些有挑战的事情。 正因为其略微复杂,我们才可以更好的思考一些问题,更好的领会跨语言调用的核心思想。
配置一个Rust项目,使之能够编译出满足C-ABI的动态链接库
https://github.com/myrfy001/rust_golang_ffi_demoexample_1
rustgolangrust
.
├── Cargo.lock
├── Cargo.toml
└── src
├── ffi.rs
├── lib.rs
└── my_app.rs
cargo new --lib rustffi.rsmy_app.rs
Cargo.toml
[lib]
crate-type = ["cdylib"]
crate-type = ["cdylib"]cdylibdylibdylib
my_app.rsmy_app.rsffi.rslib.rsmodmy_app.rsffi.rs
my_app.rs
pub fn my_app_simple_rust_func_called_from_go(arg1: u8, arg2: u16, arg3: u32) -> usize {
arg1 as usize + arg2 as usize + arg3 as usize
}
ffi.rs
use crate::my_app;
#[no_mangle]
fn simple_rust_func_called_from_go(arg1: u8, arg2: u16, arg3: u32) -> usize {
my_app::my_app_simple_rust_func_called_from_go(arg1, arg2, arg3) as usize
}
#[no_mangle]
cargo buildtarget/debug/librust.solibrust.dyliblibrust.dyliblibrustCargo.tomlname = "rust"
开发一个Go项目,调用Rust编写的库
本篇文章虽然是介绍在Go中调用Rust编写的函数库,但其中以C-FFI为界,在Golang视角中,其看到的只是一个符合C-FFI标准的函数库,本篇文章介绍的与cgo有关的知识点,可以应用在其他任何语言所开发的符合C-FFI标准的软件库上,不仅限于Rust开发的软件库。
example_1golang
.
├── ffi_demo.h
├── go.mod
├── main.go
└── main_test.go
go.modgo mod initmain.gomain_test.goffi_demo.h
ffi_demo.h
ffi_demo.h
#include "stdint.h"
uintptr_t simple_rust_func_called_from_go(uint8_t arg1, uint16_t arg2, uint32_t arg3);
cbindgen
stdint.huintptr_t
#[no_mangle]
ffi_demo.h/usr/local/includegolang
ffi_demo.hmain.go
package main
/*
#cgo CFLAGS: -I.
#cgo LDFLAGS: -L../rust/target/debug -lrust
#include "ffi_demo.h"
*/
import "C"
func SimpleRustFuncCalledFromGo() {
arg1 := 123
arg2 := 1234
arg3 := 1234567
cArg1 := C.uint8_t(arg1)
cArg2 := C.uint16_t(arg2);
cArg3 := C.uint32_t(arg3);
ret := C.simple_rust_func_called_from_go(cArg1, cArg2, cArg3)
if int(ret) != arg1 + arg2 + arg3 {
panic("SimpleRustFuncCalledFromGo Error")
}
}
import "C"import "C"
#cgo CFLAGS: -I.-I..golangffi_demo.hffi_demo.h#cgo LDFLAGS: -L../rust/target/debug -lrust-L../rust/target/debug-lrustrustlibrust.so#include "ffi_demo.h"ffi_demo.h
import "C"Cmain.goC.xxxx
SimpleRustFuncCalledFromGo
arg1arg2arg3ffi_demo.hcArg1cArg2cArg3C.uint8_tC.uint16_tC.uint32_tC.simple_rust_func_called_from_gosimple_rust_func_called_from_goCsimple_rust_func_called_from_goffi_demo.hsimple_rust_func_called_from_goffi_demo.hlibrust.sosimple_rust_func_called_from_golibrust.solibrust.sosimple_rust_func_called_from_go#[no_mangle]C.ulongC.ulongffi_demo.huintptr_tC.ulongffi_demo.huintptr_tC.ulongif int(ret) != arg1 + arg2 + arg3 {if ret != arg1 + arg2 + arg3 {C._Ctype_ulongintC.ulong
arg1arg2arg3uint8uint16uint32
CC
C.ucharC.shortC.int
CC
main_test.go
字符串的传递
预防针 在接下来的内容中,会遇到一些非常规的做法,这里之所以要介绍这些非常规的做法,目的不是为了鼓励大家在代码中去这么使用,而是给大家展示各种各样的情况,让大家了解到无论上层接口再怎么变,使用方法再怎么稀奇古怪,其底层核心不变的就是内存的分配与释放。借助unsafe rust,大家可以获得像C/C++一样对内存的完全掌控能力。实际使用中FFI边界上的情况非常灵活多变,但只要内存管理能搞清楚,其他的事情都不是大问题。
了解库函数的核心功能逻辑
example_2rust/src/my_app.rs
pub fn my_app_receive_string_and_return_string(s: String) -> String {}
pub fn my_app_receive_str_and_return_string(s: &str) -> String {}
pub fn my_app_receive_str_and_return_str(s: &str) -> &str {}
pub unsafe fn my_app_receive_string_and_return_str<'a>(s: String) -> (&'a str, *const u8, usize, usize) {}
String&str
当输入字符串的长度小于15个byte的时候,返回完整的字符串,而超过15个byte的时候返回前15个byte
String&str
接收String,返回String
pub fn my_app_receive_string_and_return_string(s: String) -> String {
if s.len() > 15 {
// this path has new memory alloc on heap
s[0..15].to_string()
} else {
// this path doesn't have new memory alloc on heap
s
}
}
to_string()
old:
┌───┬───┬───┐
│ptr│cap│len│
├───┼───┼───┤
│ │ 19│ 19│
└─┬─┴───┴───┘
new: │
┌───┬───┬───┐ │
│ptr│cap│len│ │
├───┼───┼───┤ ┌─┴┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│ │ 15│ 15│ │0 │1 │2 │3 │4 │5 │6 │7 │8 │9 │A │B │C │D │E │F │X │Y │Z │
└─┬─┴───┴───┘ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
│
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
└───────────────►│0 │1 │2 │3 │4 │5 │6 │7 │8 │9 │A │B │C │D │E │
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
old:
┌───┬───┬───┐
new: │ptr│cap│len│
┌───┬───┬───┐ ├───┼───┼───┤
│ptr│cap│len│ │ │ 15│ 15│
├───┼───┼───┤ └─┬─┴───┴───┘
│ │ 15│ 15│ │
└─┬─┴───┴───┘ │
│ ▼
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
└─────────────────►│0 │1 │2 │3 │4 │5 │6 │7 │8 │9 │A │B │C │D │E │
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
出于篇幅的限制,后面不能每个case都画图来说明。在继续后面的介绍之前,大家一定要理解这个字符串在内存中的表示形式,并且在看到后面的代码时,能想想出对应的内存布局。
接收&str,返回String
pub fn my_app_receive_str_and_return_string(s: &str) -> String {
// both path alloc new memory
if s.len() > 15 {
s[0..15].to_string()
} else {
s.to_string()
}
}
&strStringString&str&strString
String: &str:
┌───┬───┬───┐ ┌───┬───┐
│ptr│cap│len│ │ptr│len│
├───┼───┼───┤ ├───┼───┤
│ │ 15│ 15│ │ │ 3 │
└─┬─┴───┴───┘ └─┬─┴───┘
│ │
│ │
▼ ▼
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│0 │1 │2 │3 │4 │5 │6 │7 │8 │9 │A │B │C │D │E │
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
接收&str,返回&str
pub fn my_app_receive_str_and_return_str(s: &str) -> &str {
// neither path alloc new memory
if s.len() > 15 {
&s[0..15]
} else {
s
}
}
接收String,返回&str
pub unsafe fn my_app_receive_string_and_return_str<'a>(s: String) -> (&'a str, *const u8, usize, usize) {
// this function is only used as an example to show that we can use unsafe
// rust to turn an owned type to a reference, you should not write such code
// in production code. It's a very ugly api design.
// neither path alloc new memory
let my_slice = if s.len() > 15 {
&*(&s[0..15] as &str as *const str)
} else {
&*(&s as &str as *const str)
};
// you can replace the following two lines using s.into_raw_parts()
// s.into_raw_parts() internally use ManuallyDrop too
// I use ManuallyDrop explicit here to show you how memory is managed
// The reason why we need to return the ptr, len and cap is that we need them
// to rebuild the String header, we need to rebuild the string header to
// free memory.
let s = ManuallyDrop::new(s);
(my_slice, s.as_ptr(), s.len(), s.capacity())
}
my_app.rs
包装字符串传递的FFI接口
rust/src/ffi.rs
#[no_mangle]
pub fn receive_str_and_return_string(s: *const c_char) -> *const c_char{}
#[no_mangle]
pub fn receive_string_and_return_string(s: *const c_char) -> *const c_char{}
#[no_mangle]
pub fn receive_str_and_return_str(s: *const c_char) -> *const c_char{}
#[no_mangle]
pub fn receive_string_and_return_str(s: *const c_char, new_ptr: *mut *const c_char, c_origin_ptr: *mut *const c_char, len: *mut usize, cap: *mut usize){}
#[no_mangle]
pub unsafe fn free_string_alloc_by_rust_by_raw_parts(s: *mut c_char, len: usize, cap: usize){}
#[no_mangle]
pub unsafe fn free_cstring_alloc_by_rust(s: *mut c_char){}
#[no_mangle]
pub fn receive_str_and_return_str_no_copy(s: *const c_char, new_ptr: *mut *const c_char, len: *mut usize){}
my_app.rsString&str*const c_char
┌────────────────────┐ ┌────────────────────┐
│s1="0123456789A" │ │s3="CDEF" │
└┬───────────────────┘ └─┬──────────────────┘
│ │
│ ┌────────────────────┐ │ ┌───────┐
│ │s2="456789A" │ │ │s4="" │
│ └┬───────────────────┘ │ └┬──────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│0 │1 │2 │3 │4 │5 │6 │7 │8 │9 │A │\0│C │D │E │F │\0│Y │Z │
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
String&strchar*
String&strchar*String&strchar*String&strchar*String&strchar*
char*CStringCStr
CStringStringCStringCStrstr&CStrCStr
receive_str_and_return_string:
#[no_mangle]
pub fn receive_str_and_return_string(s: *const c_char) -> *const c_char {
let cstr = {
// 永远要记得先检查传入的指针是不是空指针
assert!(!s.is_null());
// 下面这一行内部实现会遍历s,寻找第一个Null出现的位置,说白了就是做计数,数出来字符串的长度
// &CStr 相比于 *const c_char而言,内部直接记录了长度信息
// 由于我们要读裸指针,所以需要一个unsafe块
unsafe{CStr::from_ptr(s)}
};
// 下面这一行会再一次遍历底层的字节序列来检查一下这个字节序列是否满足UTF-8编码,如果检查通过,这个
// 函数会返回一个&str类型的指针,同样指向原来的地址。这一步相当于做了一个认证,把`&CStr`转换为`&str`
// 就可以告诉类型系统:这块内存满足我们Rust对字符串的要求,可以在Safe Rust代码里放心使用了。
// 这个操作会复用底层的字节数组,不会发生内存分配和数据拷贝。
// 由于`&str`是一个引用,它引用的是外部系统(例如Golang)所持有的内存,所以,外部系统必须有责任保证这
// 块内存不会在使用中被释放
let rstr = cstr.to_str().expect("not valid utf-8 string");
// 上面的两行代码没有做内存分配,但是却从头到尾遍历了两次字符串。如果这个FFI接口在你的Hot Path上,那么
// 你需要认真考虑一下性能开销的影响。
// 现在,我们就可以在Safe Rust的世界来使用外部的字符串了
let ret = my_app::my_app_receive_str_and_return_string(rstr);
// 现在,我们得到了一个`String`,这也就意味着上面的函数调用过程中发生了Rust这一侧的内存分配。
// 我们将要返回给调用者一个指针,但返回的数据必须遵循C的字符串规范才能够穿越FFI的边界,也就是我们
// 要返回一个裸指针,裸指针本身没有字符串的长度信息,并且指向一块以Null结尾的内存区域。
// 而我们知道,Rust的String不一定能保证字符串结尾的后面一定有一个Null存在。
// 这时我们就要借助`CString`这个数据类型了,不幸的是,这里又会引入额外的开销:
// * 首先,它会再次检查整个底层字节数组是否在中间包含Null,因此它会再次遍历字符串。
// * 其次,它将尝试将 Null字节追加到底层数组的后面,这一步是否会有额外开销取决于底层的缓冲区
// 是否还有空闲空间,也就是String的len是否小于cap。如果有空间,那么追加一个Null字节几乎
// 没有什么开销,但如果没有空间,那么就要进行扩容操作,这会导致一次内存分配以及一次数据拷贝
let c_ret = CString::new(ret).expect("null byte in the middle");
// 终于到了这里,我们可以通过into_raw()的方法返回一个指向堆内存的指针了。这个方法会消耗掉
// c_ret,让编译器暂时“忘掉”这块堆内存。
// 但是,这块内存迟早要有人来释放,应该怎么释放呢?我们后面再介绍。
c_ret.into_raw()
// 重要提示:
// 看完上面的代码,你必须要掌握的一个核心点:
// * 这个ffi包装函数的入参所对应的内存是由调用者拥有的(比如Golang),因此也必须由外部系统
// 来决定何时释放
// * 返回值是由Rust申请的,那么将来也一定是由Rust来执行释放
}
通过人为的约定,减少一些不必要的检查 通过调整API接口的设计,显式传递字符串长度 通过调整API接口设计,将频繁的调用转化为少量批次调用,以减小开销 避免在Hot Path上使用转换开销大的接口 关于提升性能,我们会在文章后后面进一步讨论,通过做性能测试的方法,来对比不同方案的性能提升。
receive_string_and_return_string:
#[no_mangle]
pub fn receive_string_and_return_string(s: *const c_char) -> *const c_char {
let cstr = {
assert!(!s.is_null());
unsafe{CStr::from_ptr(s)}
};
// to_str() 会遍历一次String来检查是否满足UTF-8,to_string() 会分配一次内存并且做一次拷贝。所以下面这行代码总计会有两次长度为N的遍历以及一次内存分配
let r_string = cstr.to_str().expect("not valid utf-8 string").to_string();
// 下面这一行调用,根据传入的字符串长度,可能进行内存分配,也可能不进行分配。
let ret = my_app::my_app_receive_string_and_return_string(r_string);
let c_ret = CString::new(ret).expect("null byte in the middle");
c_ret.into_raw()
// 重要提示:
// 和上一个函数一样,输入数据的内存是被Golang持有的,返回值是被Rust持有的
// 返回的指针指向c_ret申请的的堆内存,该段内存因为into_raw()方法,暂时被编译器忘掉,函数返回时不会被释放
// r_string 和 ret 都是临时变量, 最终返回的字符指针指向的堆内存地址是c_ret变量持有的,
// r_string 和 ret 所分配的堆内存都会在函数返回时被回收。
}
receive_str_and_return_str
#[no_mangle]
pub fn receive_str_and_return_str(s: *const c_char) -> *const c_char {
let cstr = {
assert!(!s.is_null());
unsafe{CStr::from_ptr(s)}
};
let rstr = cstr.to_str().expect("not valid utf-8 string");
// 下面这个函数调用并不会进行内存分配,从Rust角度来看,&str是要复用底层的字符缓冲区,
// 这样看起来貌似很好,没啥问题
let ret = my_app::my_app_receive_str_and_return_str(rstr);
// 但是,问题来了,如果上面的函数调用返回的是一个长字符串的子切片,那么这个子切片就不会是以Null结尾的了,
// 因为&str是一个Rust概念里的胖指针,里面存了长度,所以这对Rust不是个问题。但在FFI的边界上,我们要
// 使用C语言规范来交流,所以这就成了一个大问题。因此,我们还是需要再创建一个CString,于是又引入了
// 内存分配和拷贝。
let c_ret = CString::new(ret).expect("null byte in the middle");
c_ret.into_raw()
// 重要提示:
// 这是一个用来演示Rust在FFI边界上处理字符串引用所引入的开销的例子。在纯Rust中,
// `my_app::my_app_receive_str_and_return_str(str)`这个函数的参数和返回值都是引用
// 类型,所以我们可以避免数据的复制。
// 但是在FFI的边界上,根据FFI包装函数实现方式的不同,返回值可能可以复用内存,避免拷贝,
// 也可能需要重新分配内存并拷贝。
// 在我们当前的这个例子的实现中,我们就无法避免拷贝。如果你想避免拷贝,那么就要重新设计FFI
// 接口的API样式。(我们会在后面的例子中给出例子)
// 作为这个函数库的作者,你有责任编写一个清晰的使用手册来告诉用户,输入数据与输出数据的内存
// 是怎样被使用的。
// 最后,和上一个函数一样,输入数据的内存是被Golang持有的,返回值是被Rust持有的
// 返回的指针指向c_ret申请的的堆内存,该段内存因为into_raw()方法,暂时被编译器忘掉,函数返回时不会被释放
}
receive_string_and_return_str
定义一个结构体来保存多个返回值的内容,然后返回指向这个结构体的指针 通过传入指针来修改调用者的内存数据,从而将要返回的值写入到调用者给定的变量中 这里我们的二阶指针就是使用了第二种方法。
#[no_mangle]
pub fn receive_string_and_return_str(s: *const c_char, new_ptr: *mut *const c_char, c_origin_ptr: *mut *const c_char, len: *mut usize, cap: *mut usize) {
let cstr = {
assert!(!s.is_null());
unsafe{CStr::from_ptr(s)}
};
let r_string = cstr.to_str().expect("not valid utf-8 string").to_string();
let (ret, t_c_origin_ptr, t_len, t_cap) = unsafe{my_app::my_app_receive_string_and_return_str(r_string)};
let c_ret = CString::new(ret).expect("null byte in the middle");
unsafe {
*new_ptr = c_ret.into_raw();
*c_origin_ptr = t_c_origin_ptr as *const i8;
*len = t_len;
*cap = t_cap;
}
// 重要提示:
// 和上一个函数一样,输入数据的内存是被Golang持有的,返回值是被Rust持有的
// 返回的指针指向c_ret申请的的堆内存,该段内存因为into_raw()方法,暂时被编译器忘掉,函数返回时不会被释放
// ret是一个临时变量
// ret 和 t_c_origin_ptr 都指向r_string申请到的堆内存
// 由于在`my_app::my_app_receive_string_and_return_str()`这个函数内部,我们也让编译器忘掉了r_string申请
// 的堆内存,所以在函数返回时,r_string申请的内存不会被释放,c_origin_ptr所指向的内存就还是有效可用的内存
}
内存释放的相关函数
into_raw()
#[no_mangle]
pub unsafe fn free_string_alloc_by_rust_by_raw_parts(s: *mut c_char, len: usize, cap: usize) {
String::from_raw_parts(s as *mut u8, len, cap);
}
#[no_mangle]
pub unsafe fn free_cstring_alloc_by_rust(s: *mut c_char) {
// 这个方法会再次遍历指针所指向的内存,直到找到Null,从而计算出字符串的长度
CString::from_raw(s);
}
一个零拷贝的FFI接口示例
my_app::my_app_receive_str_and_return_str()
#[no_mangle]
pub fn receive_str_and_return_str_no_copy(s: *const c_char, new_ptr: *mut *const c_char, len: *mut usize) {
let cstr = {
assert!(!s.is_null());
unsafe{CStr::from_ptr(s)}
};
let rstr = cstr.to_str().expect("not valid utf-8 string");
let ret = my_app::my_app_receive_str_and_return_str(rstr);
let c_ret = ret.as_ptr();
unsafe {
*new_ptr = c_ret as *const i8;
*len = ret.len();
}
// 重要提示:
// 在这段代码中没有出现任何`to_owned()` 或 `to_string()`,因此这个API接口不会申请任何新的堆内存
// 我们显示返回了字符串的长度,这样就解决了截取字符串末尾没有Null的问题。
// 返回的指针所指向的内存,是外部系统分配的内存空间,不是Rust分配的!!!
// 如果你设计了一个这样的API接口,那么你应该在说明文档中清楚地写出来,返回的内存指针是指向了调用者提供的内存
}
在Golang中调用
golang/ffi_demo.h
#include "stdint.h"
uintptr_t simple_rust_func_called_from_go(uint8_t arg1, uint16_t arg2, uint32_t arg3);
char* receive_str_and_return_string(char*);
char* receive_string_and_return_string(char*);
char* receive_str_and_return_str(char*);
// the follow line is a very ugly api design, only used as example, never use in real code.
char* receive_string_and_return_str(char*, char**, char**, uintptr_t*, uintptr_t*);
void free_string_alloc_by_rust_by_raw_parts(char*, uintptr_t, uintptr_t);
void free_cstring_alloc_by_rust(char*);
void receive_str_and_return_str_no_copy(char*, char**, uintptr_t*);
*const c_charchar*
golang/main.goimport "C"#include "stdlib.h"stdlib.hC.free()
PassStringBySinglePointer()
testProc := func(f int, x, y string) {
goStr := x
// Memory Alloc in OS allocator And String Copy
// cStr is not managed by go GC
cStr := C.CString(goStr)
defer C.free(unsafe.Pointer(cStr))
var cStrRet *C.char
switch f{
case 1:
cStrRet = C.receive_str_and_return_string(cStr)
case 2:
cStrRet = C.receive_string_and_return_string(cStr)
case 3:
cStrRet = C.receive_str_and_return_str(cStr)
}
// Memory Alloc in Go runtime And String Copy
// goStrRet is managed by go GC
goStrRet := C.GoString(cStrRet)
C.free_cstring_alloc_by_rust(cStrRet)
if goStrRet != y {
panic(fmt.Sprintf("Error, expected %s, got %s", y, goStrRet))
}
}
C.CString()C.GoString()C.free()
testProc(1, "极客幼稚园是一个不错的微信公众号", "极客幼稚园")
testProc(1, "Datafuse Lab", "Datafuse Lab")
testProc(2, "极客幼稚园是一个不错的微信公众号", "极客幼稚园")
testProc(2, "Datafuse Lab", "Datafuse Lab")
testProc(3, "极客幼稚园是一个不错的微信公众号", "极客幼稚园")
testProc(3, "Datafuse Lab", "Datafuse Lab")
testProc := func(x, y string) {
goStr := x
cStr := C.CString(goStr) // Memory Alloc And String Copy
defer C.free(unsafe.Pointer(cStr))
var cStrRet *C.char
var cRawStr *C.char
retCap := C.ulong(0)
retLen := C.ulong(0)
C.receive_string_and_return_str(cStr, &cStrRet, &cRawStr, &retLen, &retCap)
goStrRet := C.GoString(cStrRet) // Memory Alloc And String Copy
C.free_string_alloc_by_rust_by_raw_parts(cStrRet, retLen, retCap)
if goStrRet != y {
panic(fmt.Sprintf("Error, expected %s, got %s", y, goStrRet))
}
}
cStrRetcRawStrretCapretLen
testProc := func(x, y string) {
goStr := x
cStr := C.CString(goStr) // Memory Alloc And String Copy
defer C.free(unsafe.Pointer(cStr))
var cStrRet *C.char
retLen := C.ulong(0)
C.receive_str_and_return_str_no_copy(cStr, &cStrRet, &retLen)
goStrRet := C.GoString(cStrRet) // Memory Alloc And String Copy
// 注意:不同于前面的Go代码,我们在创建完GoString以后,不能在这里调用Rust提供的内存释放接口了
// 因为cStrRet所指向的内存就是上面cStr所分配的内存,这段内存会被上面的defer语句在函数返回时释放
// 如果我们在这里释放,就会导致内存的二次释放问题。
// 因为是复用的同一块内存,所以重构出来的Go字符串会和输入的字符串一模一样。因为我们并不能向字符串中间插入Null
if goStrRet != x {
panic(fmt.Sprintf("Error, expected %s, got %s", x, goStrRet))
}
// 但是,通过Rust函数返回的长度信息,我们截取出来我们要的数据
goStrRetWithLengthLimit := goStrRet[:retLen]
if goStrRetWithLengthLimit != y {
panic(fmt.Sprintf("Error, expected %s, got %s", y, goStrRetWithLengthLimit))
}
// 我们的演示案例中,由于没有修改字符串的起始位置,只是调整了它的结尾,所以API返回新的字符串指针其实是有些多余的,使用原始的传入字符串
// 当然也可以打到效果,毕竟我们只需要知道一个长度而已。但是如果这个函数的功能是从字符串的中间截取一段出来的话,那么返回一个子字符串的起
// 始指针就很重要了。
if goStr[:retLen] != y {
panic(fmt.Sprintf("Error, expected %s, got %s", y, goStrRetWithLengthLimit))
}
}