在前篇中我们已经了解了 Rust 如何调用 C 的代码和给定接口的库。现在让我们更任性一点: 我们现在需要定义一个动态链接库接口,协作者只要发布他的实现接口的动态链接库二进制文件,我们就可以 对代码不做任何修改,直接动态调用它。

由于 dlopenLoadLibrary 的存在,这肯定是可以实现的。他们只能解析返回 静态变量和函数。因此我们实现的一切都需要使用函数和静态变量去传递。

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++ 解决不了任何问题” 大概指的就是东西好是好,可惜用不了!