通过阅读本文,你可以了解有关 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> 来包裹某个类型来实现线程安全。引入多线程锁 Mutex 后,在读写操作时需要调用 lock 函数,得益于 Rust 特性,离开作用域自动析构解锁。下面这个例子将在动态链接库中维护一个 u64 整数:

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 分钟完成)

  1. 业务逻辑使用 Rust 完成,再使用 Rust 导出 C 接口,方便其他编程语言调用。
  2. Free Pascal 这门语言语法挺优美的,配上 Lazarus 写用户界面很不错。
  3. 使用 Python 的 ctypes 模块调用 C 动态链接库不是很困难。
  4. 日后尝试使用 LuaJIT FFI 扩展调用 C 接口,加上与之高度相似的 Python 库 CFFI。
  5. 日后尝试导出 Struct,目前可以使用 JSON 格式的字符串来表达复杂类型。
  6. 还需要进一步认识 safer-ffi 提供的 char_p 指针。
最后修改:2024 年 03 月 02 日
如果觉得我的文章对你有用,请随意赞赏