在前篇中我们已经了解了 Rust 如何调用 C 的代码和给定接口的库。现在让我们更任性一点: 我们现在需要定义一个动态链接库接口,协作者只要发布他的实现接口的动态链接库二进制文件,我们就可以 对代码不做任何修改,直接动态调用它。
由于 dlopen
与 LoadLibrary
的存在,这肯定是可以实现的。他们只能解析返回
静态变量和函数。因此我们实现的一切都需要使用函数和静态变量去传递。
Rust 调用
我们使用 libloading = "0.8"
简化调用过程。
我们期望实现这样一个插件接口
#[repr(C)]
struct MyPlugin{
name: &'static i8, // CStr
new: extern "C" fn(i32)->&'static (),
destroy: unsafe extern "C" fn(&()),
add: extern "C" fn(&(), x:i32)->i32,
sub: extern "C" fn(&(), x:i32)->i32,
}
从而能够这样被调用
let p=std::env::current_dir().expect("unable to access library directory");
#[cfg(unix)]
let name="libmylib.so";
#[cfg(windows)]
let name="mylib.dll";
unsafe {
let lib = libloading::Library::new(p.join(name)).unwrap(); // load from absolute path
let plgn = lib.get::<&MyPlugin>(b"impls").unwrap();
println!("load plugin {:?}", core::ffi::CStr::from_ptr(plgn.name));
let x = (plgn.new)(1);
assert_eq!((plgn.add)(x,2), 3);
assert_eq!((plgn.sub)(x,2), -1);
(plgn.destroy)(x);
}
C library
一个最简单的插件库如下:
#include <stdlib.h>
typedef struct {int x;} calc_t;
calc_t* new(int x){
calc_t* p=(calc_t*)malloc(sizeof(calc_t));
p->x=x;
return p;
}
void destroy(calc_t* p){free(p);}
int add(calc_t* x, int y){return (x->x)+y;}
int sub(calc_t* x, int y){return (x->x)-y;}
typedef const void* plugin_t[5];
#ifdef _MSC_VER
__declspec(dllexport)
#endif
plugin_t impls={ //
"c impl",
(const void*)new,
(const void*)destroy,
(const void*)add,
(const void*)sub
};
通过 gcc -shared -fPIC -o libmylib.so lib.c
编译出动态链接库,就能用啦!
需要注意的是, Cygwin 上 libc 调用有神秘 Bug。建议用 Visual C 的 cl /LD mylib.c
编译。
Rust
Rust 对 C api 的兼容性令人惊艳。
// avoid raw pointer sync check
#[repr(transparent)]
pub struct Share<T>(*const T);
unsafe impl<T> Sync for Share<T>{}
#[no_mangle]
pub static impls:[Share<()>; 5] = [
Share(b"rust impl\0".as_ptr() as *const ()),
Share(Calculator::new as *const ()),
Share(Calculator::destroy as *const ()),
Share(Calculator::add as *const ()),
Share(Calculator::sub as *const ()),
];
pub struct Calculator(i32);
impl Calculator{
pub extern "C" fn new(x:i32)->&'static mut Self{Box::leak(Box::new(Self(x)))}
pub extern "C" fn destroy(&mut self){drop(unsafe{Box::from_raw(self)});}
pub extern "C" fn add(&self, y:i32)->i32{self.0+y}
pub extern "C" fn sub(&self, y:i32)->i32{self.0-y}
}
可以用我们最喜欢的 struct-impl
风格写!只需要把导出的函数用 extern "C"
标注一下就好了。
而且可以料想的是,随着项目规模增大,Rust 的组织结构先天优势会带来更好的体验!
需要注意的是,Rust 编译出的导出静态变量都多加了一层指针。例如对于 impls: *mut T
,
实际导出的是一个 T**
类型;对于定长数组 impls: [*mut T;5]
导出的是 T**
C++
Rust 都可以,那么 C++ 呢?
依葫芦画瓢我们可以写出
struct Calc{
int x;
Calc(int x):x(x){}
int add(int y){return x+y;}
int sub(int y){return x-y;}
};
Calc* construct(int x){return new Calc(x);}
void destruct(Calc* p){delete p;}
extern "C" {
#ifdef _MSC_VER
__declspec(dllexport)
#endif
const void* impls[]={
"cpp impl",
(const void*)construct,
(const void*)destruct,
(const void*)Calc::add,
(const void*)Calc::sub,
};
}
然而
cannot convert 'int (MyClass::*)(int, int)' to 'void*' in initialization
由于缺少 this
,它无法调用。当然,我们可以写成友元函数
struct Calc{
int x;
Calc(int x):x(x){}
friend int add(Calc*, int);
friend int sub(Calc*, int);
};
int add(Calc* x, int y){return x->x+y;}
int sub(Calc* x, int y){return x->x-y;}
Calc* construct(int x){return new Calc(x);}
void destruct(Calc* p){delete p;}
// export
extern "C" {
#ifdef _MSC_VER
__declspec(dllexport)
#endif
const void* impls[]={
"cpp impl",
(const void*)construct,
(const void*)destruct,
(const void*)add,
(const void*)sub,
};
}
但这又和 C 没啥区别了。不如 C。
这大概就是 Linus 对 C++ 十分厌恶的原因吧——它的类模型完全不兼容 C-FFI,导致你想与它交互只能使用 C++,这简直是一种垄断。要兼容其他语言的交互,你不得不写出比 C 还丑的东西。“C++ 解决不了任何问题” 大概指的就是东西好是好,可惜用不了!