通过阅读本文,你可以了解有关 FFI 编程的部分细节,了解通过 Python 和 Free Pascal 调用 cdecl 动态链接库。
一、C 语言支持回调函数
思想是在 C 语言中一切都是指针,函数也不例外。这样,可以将某个函数的指针作为参数,然后在函数主体中调用,例如:
void call(void * ctx, void (*cb)(void * ctx));
第二个入参 cb 就是函数指针,由于我对 C 语言指针了解甚少,这里就不过度解释。
现在,我在 Rust 中导出包含回调函数的 C 接口。首先,导入 safer-ffi 框架。
#[ffi_export]
fn call(
ctx: u64,
cb: extern "C" fn(ctx: *const c_char),
) {
let corr = match ctx {
0 => "Hello",
1 => "Hi",
_ => "Goodbye"
};
let corr = CString::new(corr).unwrap();
cb(corr.as_ptr());
// println!("{}", corr.to_string_lossy());
}
这个函数入参 cb 导出 C 接口后就是函数指针。这里,为了方便管理内存,我没将 corr 的所有权移出函数,这样离开作用域后,corr 会被自动析构。
在 Python 中,用下面的形式来使用回调函数:
C_CALLBACK = CFUNCTYPE(None, c_char_p)
def show(s: c_char_p):
# s = s.decode()
# print(s)
pass
callback = C_CALLBACK(show)
loaded_library.call(0, callback)
首先,你得声明回调函数的类型 C_CALLBACK,第一个参数是返回值,后面的参数是回调函数的入参。然后,声明 callback 对象(一切皆对象?),注意,这个 callback 对象要确保在调用时存活,即确保不被垃圾回收器清理。
在 Pascal 中调用,图简单可以遵循下面的形式:
type TDllCallback = procedure (message: PChar);
procedure call(signal: Integer; callback: TDllCallback); cdecl; external 'xxx.dll';
这样写就完成了绑定。使用时,你需要写一个符合 TDllCallback 的过程,比如 CLabel,然后加上 @CLabel 作为参数传入:
call(0, @CLabel);
二、使用 lazy_static 库新建动态链接库中全局可用的静态变量
比如,新建一个静态变量用以管理向子线程发送信号:
use tokio::sync::mpsc::{channel, Sender};
pub struct Core {
inner: Sender<u64>
}
impl Core {
pub fn new() -> Self {
let (s, mut r) = channel(16);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
std::thread::spawn(move || {
rt.block_on(async move {
while let Some(message) = r.recv().await {
tokio::spawn(async move {
let duration = Duration::from_secs(message);
println!("start working! duration: {}", duration.as_secs());
tokio::time::sleep(duration).await;
println!("finished! duration: {}", duration.as_secs());
});
}
});
});
Self { inner: s }
}
pub fn send_work(&self, dur: u64) {
self.inner.blocking_send(dur).unwrap();
}
}
使用 lazy_static 新建变量,再使用 safer_ffi 导出接口:
lazy_static::lazy_static! {
pub static ref CORE: Core = Core::new();
}
#[ffi_export]
fn execute(dur: u64) {
CORE.send_work(dur);
}
这样,在其他编程语言中调用起来就像是同步代码,实际业务逻辑上是在异步运行时中执行。结合一中的回调函数,就能实现一个形似异步的接口,实际是不是我不知道。
三、线程安全 - Rust 中全局静态变量线程安全 | Lazarus 用户界面线程安全
在 Rust 中,可以使用 Arc<Mutex
lazy_static::lazy_static! {
static ref CORE: Arc<Mutex<u64>> = Default::default();
}
#[ffi_export]
fn get_current() -> u64 {
let result = *CORE.lock().unwrap();
result
}
#[ffi_export]
fn set_current(i: u64) {
*CORE.lock().unwrap() = i;
}
在 Lazarus 中引入多线程后,更新界面时需要注意线程安全。图方便可以通过调用 Application.QueueAsyncCall 来更新界面。这个函数的实现是线程安全的,意味着我们在其他线程中调用这个函数来更新界面是安全的。
TDataEvent = procedure (Data: PtrInt) of object;
procedure QueueAsyncCall(AMethod: TDataEvent; Data: PtrInt);
这里解释一下 PtrInt 类型,PtrInt 就是指针所对应的类型(整形?),需要取指针再转换后传递,TDataEvent 是回调函数,注意调用时需要过程名前加上 @ 符号。
type PtrString = ^String;
procedure TForm1.ChangeLabel(message: PtrInt);
var
pStr: PtrString;
begin
pStr := PtrString(message);
Form1.Label1.Caption:= pStr^;
Dispose(pStr);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
i: Integer;
pStr: PtrString;
begin
for i:=0 to 1000 do begin
New(pStr);
pStr^ := 'index is ' + IntToStr(i);
Application.QueueAsyncCall(@ChangeLabel, PtrInt(pStr));
end;
end;
这里对某些符号进行巩固以便日后复习使用:
符号 | 说明 |
---|---|
^String | 表示 String 类型的指针 |
type PtrString = ^String; | 使用 type 声明类型,以便转换通用指针 |
pStr^ | 看作指针对应的变量使用 |
New(pStr) | 指针作为入参,手动申请内存 |
Dispose(pStr) | 指针作为入参,手动释放内存 |
四、使用 safer-ffi 导出字符串
下面的例子复制自库官方教程 string_concat - safer_ffi
User Guide
/// Concatenate two input UTF-8 (_e.g._, ASCII) strings.
///
/// \remark The returned string must be freed with `rust_free_string`
#[ffi_export]
fn concat(fst: char_p::Ref<'_>, snd: char_p::Ref<'_>) -> char_p::Box {
let fst = fst.to_str(); // : &'_ str
let snd = snd.to_str(); // : &'_ str
format!("{}{}", fst, snd) // -------+
.try_into() // |
.unwrap() // <- no inner nulls --+
}
/// Frees a Rust-allocated string.
#[ffi_export]
fn rust_free_string(string: char_p::Box) {
drop(string)
}
后记:心得体会(⏲10 分钟完成)
- 业务逻辑使用 Rust 完成,再使用 Rust 导出 C 接口,方便其他编程语言调用。
- Free Pascal 这门语言语法挺优美的,配上 Lazarus 写用户界面很不错。
- 使用 Python 的 ctypes 模块调用 C 动态链接库不是很困难。
- 日后尝试使用 LuaJIT FFI 扩展调用 C 接口,加上与之高度相似的 Python 库 CFFI。
- 日后尝试导出 Struct,目前可以使用 JSON 格式的字符串来表达复杂类型。
- 还需要进一步认识 safer-ffi 提供的 char_p 指针。
1 条评论
晦涩难懂