Rust 与不同语言调用

2021-09-18 language rust

简单介绍 Rust 和 C 之间的相互代码调用。

简介

首先简单介绍如何通过动态库、静态库进行相互调用,除了单独使用之外,还可以通过 Rust 的 build.rs 管理编译过程。

自动编译

Cargo 提供了 build.rs 机制,工程目录下存在 build.rs 文件时,则先编译并执行 build.rs,并将其输出的作为 rustc 编译参数,然后再编译 Rust 工程,这样就可以先编译 C 代码,并将相关参数传递给 rustc 编译。

简单示例可以使用 Makefile,复杂工程建议使用 CMake 类似管理,如下以 CMake 为例进行介绍。

CMake

对应的 CMakeLists.txt 文件为。

CMAKE_MINIMUM_REQUIRED(VERSION 3.9)
PROJECT(sysx)

SET(SRC_LIST src/math.c)

SET(LIB_STATIC_NAME ${PROJECT_NAME}-static)
SET(LIB_SHARED_NAME ${PROJECT_NAME}-shared)

ADD_LIBRARY(${LIB_STATIC_NAME} STATIC ${SRC_LIST})
SET_TARGET_PROPERTIES(${LIB_STATIC_NAME} PROPERTIES OUTPUT_NAME ${PROJECT_NAME})

ADD_LIBRARY(${LIB_SHARED_NAME} SHARED ${SRC_LIST})
SET_TARGET_PROPERTIES(${LIB_SHARED_NAME} PROPERTIES OUTPUT_NAME ${PROJECT_NAME})

INSTALL(TARGETS ${LIB_STATIC_NAME} DESTINATION .)
INSTALL(TARGETS ${LIB_SHARED_NAME} DESTINATION .)

项目会依赖 cmake-rs 三方库,可以通过 cargo add --build cmake 添加,同时需要确保在 CMakeLists.txt 文件中包含 install 目标,对应的 build.rs 代码如下,此时会在 target/debug 目录下生成对应二进制文件。

fn main() {
    // 通过 CMake 生成对应的库,可以时动态或者静态库
    use cmake::Config;
    let dst = Config::new("ansic").build();

    // 若 build.rs 有任何修改,则重新编译
    println!("cargo:rerun-if-changed=build.rs");
    // 搜索生成的函数库
    println!("cargo:rustc-link-search=native={}", dst.display());
    // 链接生成的函数库,可以是动态或者静态库,这里选择静态库
    println!("cargo:rustc-link-lib=static=sysx");
    //println!("cargo:rustc-link-lib=dylib=sysx");
}

如果不想引入依赖,那么就可以通过命令编译,对应的 build.rs 示例如下。

use std::process::Command;

fn main() {
    let libs = "libsysx";
    let target = "target/libsysx";

    std::fs::create_dir(target).unwrap();

    Command::new("cmake")
        .args(&["-S", libs, "-B", target])
        .status()
        .unwrap();
    Command::new("cmake")
        .args(&["--build", target])
        .status()
        .unwrap();

    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rustc-link-search=native={}", target);
    println!("cargo:rustc-link-lib=static=sysx");
}

注意,当前两种方式并不会检查 C 库的修改,所以,修改后需要先 cargo clean 然后再编译。

类型使用

大部分的基础类型都在 std::os::raw 中定义,其有很多是与 libc 包重叠的,如何选择详见如下的介绍,结构体调用则需要添加 #[repr(C)] 注解。

如果在结构体中定义了动态数组,也就是包含一个 num 确定数组大小,同时包含指针指向数组,其中指针的初始化可以通过 std::ptr::null() 或者 std::ptr::mut_null() 实现。

enum

在 C 语言中,对应的 enum 相当于整形,但是规范上有如下的描述。

Each enumerated type shall be compatible with char, a signed integer type, or an unsigned integer type. The choice of type is implementation-defined, but shall be capable of representing the values of all the members of the enumeration.

也就是说,根据不同编译器实现有所区别,而且编译时还可以通过 -fshort-enum 参数调整。

而在 Rust 中,在内存上来说,其开头还存在了 tag 信息,所以,有些场景下可以修改为如下的常量方式,例如作为数组的索引,可定义为 const F_IDX: usize = 0 这种方式。

常量

在 C 中可能会通过 #define F_ALL (1 << 1) 方式定义标志位常量,同样在 Rust 中可以修改为 const F_ALL: u32 = (1 << 1) 类似参数。

注意事项

不要使用 bool

stdbool.h 头文件中包含了定义,在 clang-16 内容如下。

#define bool _Bool
#define true 1
#define false 0

_Bool 是 C99 引入的 关键字,但是,Rust 中的 std::os::raw 只定义了常规的类型,包含了如下类型。

alias_core_ffi! {
    c_char c_schar c_uchar
    c_short c_ushort
    c_int c_uint
    c_long c_ulong
    c_longlong c_ulonglong
    c_float
    c_double
    c_void
}

调用实践

常见的一些场景就是参数传递、内存管理。

字符串

C 调用 Rust

C 语言中的字符串为 char * 类型,而且是 \0 结尾,而 Rust 中则保存了长度,所以,无法直接使用 String 或者 str 类型,而是需要通过 CStringCStr 进行转接。

其中 CString 用于 Rust->C 的转换,而 CStr 反之。另外,String::new() 会复制底层数据,而 CStr::new() 则不会。

如下是最简单的 C 调用 Rust 代码。

use std::ffi::CStr;
use std::os::raw::c_char;

#[no_mangle]
pub extern "C" fn hey(ptr: *const c_char) {
    let name = unsafe { CStr::from_ptr(ptr).to_str().expect("bad encoding") };
    println!("Hello {}", name); // NOTE: not owned now.

    let owned = name.to_owned();
    println!("Hello {}", owned); // Will auto drop.
}
#include <stdlib.h>
#include <string.h>

void hey(char *);

int main(void) {
    char *name = malloc(6);
    strcpy(name, "World");
    hey(name);
    free(name);
    return 0;
}

注意,这里使用的是 CStr 而不是 CString 类型,而且不要通过 CString::from_raw() 构造,该函数只接受 CString::into_raw() 的返回值。

特征调用

Rust 中的 trait object 采用的是胖指针,一般会在 Rust 中定义对应的特征,通过 C 实现对应的函数,如果需要,可以用 Rust 实现相关的结构对函数进行封装,例如 Rust Trait 中的示例。

另外,还有一种场景,在 Rust 中提供了函数,不过其入参是 dyn 类型,例如 Log 的实现,此时需要通过 Rust 的回调函数调用,这里就以此为例介绍如何处理。

异步调用

这一部分会比较复杂,单独拆出来,通过 python3 -m http.server 9090 命令,在本地启动一个静态文件服务器,如下是通过 tokioreqwest 简单读取某个文件内容。

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let body = reqwest::Client::new()
        .get("http://127.0.0.1:9090/test.md")
        .send()
        .await?
        .text()
        .await?;
    println!("{}", body);
    Ok(())
}

如下是简单实现。

use once_cell::sync::Lazy;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
use std::sync::{Arc, Mutex};
use tokio::runtime::Runtime;

static RUNTIME: Lazy<Runtime> = Lazy::new(|| Runtime::new().unwrap());

pub struct ReqContext {
    complete: bool,
    response: Option<String>,
}

#[no_mangle]
pub extern "C" fn request_send(ptr: *const c_char) -> *mut Arc<Mutex<ReqContext>> {
    let url = unsafe { CStr::from_ptr(ptr).to_str().expect("bad encoding") };
    let req_ctx_raw = Arc::new(Mutex::new(ReqContext {
        complete: false,
        response: None,
    }));

    println!("Rust handle send for {}", url);
    let req_ctx_clone = req_ctx_raw.clone();
    let future = async move {
        let body = reqwest::Client::new()
            .get(url)
            .send()
            .await
            .unwrap()
            .text()
            .await
            .unwrap();
        println!("Rust {}", body);
        let mut res = req_ctx_raw.lock().unwrap();
        res.complete = true;
        res.response = Some(body);
    };
    RUNTIME.spawn(future);

    Box::into_raw(Box::new(req_ctx_clone))
}

#[no_mangle]
pub extern "C" fn request_is_complete(ptr: *mut Arc<Mutex<ReqContext>>) -> bool {
    let ctx = unsafe {
        assert!(!ptr.is_null());
        &*ptr
    };
    let res = ctx.try_lock();
    if res.is_ok() {
        res.unwrap().complete
    } else {
        false
    }
}

#[no_mangle]
pub extern "C" fn request_get_result(ptr: *mut Arc<Mutex<ReqContext>>) -> *const c_char {
    let ctx = unsafe {
        assert!(!ptr.is_null());
        &*ptr
    };

    let res = ctx.try_lock();
    if res.is_ok() {
        if let Some(body) = res.unwrap().response.as_ref() {
            return CString::new(body.as_str()).unwrap().into_raw();
        }
    }
    ptr::null()
}

#[no_mangle]
pub extern "C" fn request_free_body(ptr: *mut c_char) {
    if ptr.is_null() {
        return;
    }
    unsafe { drop(CString::from_raw(ptr)) };
}

#[no_mangle]
pub extern "C" fn request_free(ptr: Box<Mutex<ReqContext>>) {
    drop(ptr);
}
#include <stdio.h>
#include <unistd.h>

struct reqwest *request_send(const char *);
int request_is_complete(struct reqwest *);
const char *request_get_result(struct reqwest *);
void request_free_body(const char *);
void request_free(struct reqwest *);

int main(void) {
    struct reqwest *req = request_send("http://127.0.0.1:9090/data.txt");
    while (!request_is_complete(req)) {
        printf("Waiting for fetching data.\n");
        sleep(1);
    }

    const char *body = request_get_result(req);
    if (body != NULL) {
        printf("Got data: %s\n", body);
        request_free_body(body);
    } else {
        printf("Got empty data\n");
    }
    request_free(req);
    return 0;
}

编译过程中建议使用静态库,同时添加 OpenSSL 的依赖 -lssl -lcrypto -lm 库。

其它

libc

libc 是对各个平台库的原始 FFI 绑定,用于与最基础的 C 库交互,支持 std/no-std 场景,提供了 C 相关的类型、常量、函数、宏等内容,使用时需要通过 unsafe 方式调用,

在使用 FFI 时,在 std::os::*::raw 中也提供了很多类似的类型定义等,可以用于一些简单的 C 代码交互,只要不涉及底层 API 的调用就不需要 libc 库。另外,类似 std::os::unix::raw 模块已经 Deprecated 了,使用时需要注意。

Rust 封装库

因为历史原因,有很多优秀的 C 库实现,可以参考 LMDB-RS 的实现。

总结

内存谁申请谁释放。不要假设使用何种内存分配器,可能是 tcmalloc jemalloc 等,所以,不要将 malloc 申请的转换为 Box 然后 drop 掉,反之,通过 Box::into_raw() 获取的指针也不要通过 free 释放。

所有权管理,常见的 Box Arc CStr CString 类型提供了 as_ptr() into_raw() from_raw() 几种与原始指针相关的函数,前者仍然持有对象,会按照 Rust 的规则执行 Drop 操作,而后者,则将所有权进行了转移。

参考

  • Foreign Function Interface Rust 死灵书中的相关介绍。
  • Quiche 一个 QUIC/HTTP3 协议库,同时提供了 C 接口,不过均采用同步调用,没有异步接口实现。