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 然后再编译。

调用实践

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

基础类型参数

C 调用 Rust

最常见的是基础类型,例如 int bool 等,只需要注意两种不同类型语言之间的差异即可。

#[no_mangle]
pub extern "C" fn add(left: u64, right: u64) -> u64 {
    left + right
}
#include <stdio.h>
#include <stdint.h>

uint64_t add(uint64_t, uint64_t);

int main(void) {
	printf("%ld\n", add(10, 20));
	return 0;
}

另外,在 Rust 的 std::os::raw 中包含很多 c_xxx 的类型声明,其对应了 C 中的类型,例如 c_int 就是 C 语言中的 int 类型。

数组参数

C 调用 Rust

这里只展示 Rust 的代码。

#[no_mangle]
pub extern fn sum(arr: *const c_int, length: c_int) -> c_int {
    let nums = unsafe {
        assert!(!arr.is_null());
        slice::from_raw_parts(arr, length as usize)
    }
    let sum = nums.iter().fold(0, |acc, v| acc + v);
    sum as c_int
}

结构体参数

C 调用 Rust

在 Rust 需要指定与 C 相同的布局方式,或者 #[repr(C, packed)] 不对齐。

#[repr(C)]
pub struct Rectangle {
    width: u64,
    height: u64,
}

#[no_mangle]
pub extern "C" fn area(rect: Rectangle) -> u64 {
    rect.width * rect.height
}
#include <stdint.h>
#include <stdio.h>

struct Rectangle {
    uint64_t width, height;
};

uint64_t area(struct Rectangle);

int main(void) {
    struct Rectangle rect = {10, 20};
    printf("%ld\n", area(rect));
    return 0;
}

另外,如果使用的是引用,而 C 语言中不存在,可以通过 Rust 中的原始指针,包括了 *const T *mut T 两种,分别对应了 &T&mut T 两种类型。在 Rust 中的引用会进行一系列的检查,而 raw pointer 则会忽略,使用时需要 unsafe 关键字。

示例如下。

#[repr(C)]
pub struct Rectangle {
    width: u64,
    height: u64,
}

#[no_mangle]
pub extern "C" fn area(rect: *const Rectangle) -> u64 {
    if rect.is_null() {
        return 0;
    }
    //unsafe { (*rect).width * (*rect).height }

    let rec: &Rectangle = unsafe { &(*rect) };
    rec.height * rec.width
}
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

struct Rectangle {
    uint64_t width, height;
};

uint64_t area(struct Rectangle *);

int main(void) {
    struct Rectangle *rect = malloc(sizeof(struct Rectangle));
    rect->width = 10;
    rect->height = 20;
    printf("%ld\n", area(rect));
    free(rect);
    return 0;
}

在上述的 Rust 代码中,可以直接将 *const 修改为 *mut 然后进行修改。

字符串

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 接口,不过均采用同步调用,没有异步接口实现。