Rust 调用 C 使用示例

2024-06-24 language rust c/cpp

简单介绍 Rust 调用 C 常用的示例代码。

简介

使用的时候可以在 Rust 工程中,或者是独立的 C 库提供了 Rust 的封装。如下通过简单示例介绍,提供一个 libsysx 的系统库,通过 C 语言对底层的系统调用进行封装,然后由 Rust 调用。

目录结构如下。

├── Cargo.toml
├── README.md
├── libsysx/
│   ├── CMakeLists.txt
│   ├── include/
│   │   └── math.h
│   └── src/
│       └── math.c
├── build.rs
└── src/
    └── main.rs

手动编译

对应内容为。

// math.h
#ifndef __LIBSYSX_H__
#define __LIBSYSX_H__

int add(int left, int right);

#endif

// math.c
int add(int left, int right) {
	return left + right;
}

然后手动生成动态库和静态库,并复制到 target/{debug,release}/deps 目录下。

$ gcc -shared -fPIC -o libsysx.so -I include src/math.c

$ gcc -c -o math.o -I include src/math.c
$ ar rc libsysx.a math.o

对应的 Rust 代码如下,使用静态库链接,如果是动态库需要将路径添加到 LD_LIBRARY_PATH 环境变量中,然后通过 cargo run/build 运行即可。

//#[link(name = "sysx", kind = "dylib")]
#[link(name = "sysx", kind = "static")]
extern "C" {
    fn add(left: isize, right: isize) -> isize;
}

fn main() {
    println!("{}", unsafe { add(10, 20) });
}

CMake

可以通过 Makefile 管理,不过当前大部分的 C/CPP 工程会使用 CMake 管理,这里同时会生成动态和静态库,更多内容可以参考 CMake 使用简介

CMAKE_MINIMUM_REQUIRED(VERSION 3.9)
PROJECT(sysx)

SET(SRC_LIST src/math.c)

SET(CMAKE_C_FLAGS "-fPIC")

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

INCLUDE_DIRECTORIES("${PROJECT_SOURCE_DIR}/include")

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 .)

此时常规的可以通过如下命令生成。

cd libsysx && make build && cd build
cmake ..
make

不过建议通过如下命令在 target/libsysx 目录下生成,可以直接在工程目录下执行如下命令。

cmake  -S libsysx -B target/libsysx
cmake --build target/libsysx

上述命令也是后续 build.rs 中使用。

build.rs

如果是简单的,例如只有单个文件,那么可以参考 LMDB-RS 使用 cc 库即可。

这里直接使用命令行进行编译,对应代码如下。

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");
}

另外 *-sys 库是有特定含义的。

使用示例

基础类型参数

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

int add(int left, int right) {
	return left + right;
}
use std::os::raw::c_int;

//#[link(name = "sysx", kind = "dylib")]
#[link(name = "sysx", kind = "static")]
extern "C" {
    fn add(left: c_int, right: c_int) -> c_int;
}

fn main() {
    println!("add(10, 20) = {}", unsafe { add(10, 20) });
}

同之前一样,需要使用 Rust 中的 std::os::raw 类型。

字符串

可以参考 字符串 中的介绍,这里简单介绍如何使用,包括需要在 Rust 中释放内存的场景。

// math.h
#ifndef __LIBSYSX_H__
#define __LIBSYSX_H__

int hello(const char *name);
char *hey(const char *name);
void modify(char *name);
void free_string(char *name);

#endif

// math.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int hello(const char *name) {
	return printf("Hello %s\n", name);
}

char *hey(const char *name) {
	char *fmt = malloc(strlen(name) + 7); // including '\0'
	if (fmt == NULL)
		return NULL;
	sprintf(fmt, "Hello %s", name);
	return fmt;
}

void modify(char *name) {
	name[0] = 'W';
}

void free_string(char *name) {
	free(name);
}
use std::{
    ffi::{CStr, CString},
    os::raw::{c_char, c_int},
};

#[link(name = "sysx", kind = "static")]
extern "C" {
    fn hello(name: *const c_char) -> c_int;
    fn hey(name: *const c_char) -> *mut c_char;
    fn modify(name: *mut c_char);
    fn free_string(v: *mut c_char);
}

fn main() {
    let name = CString::new("world").unwrap();
    println!("{}", unsafe { hello(name.as_ptr()) });

    // transfers ownership
    let name = CString::new("world").unwrap().into_raw();
    unsafe {
        modify(name);
        let name = CString::from_raw(name); // Retakes ownership
        println!("{:?}", name.to_str().unwrap());
    };

    let name = CString::new("world").unwrap();
    unsafe {
        let ptr = hey(name.as_ptr());
        let msg = CStr::from_ptr(ptr);
        println!("{:?}", msg);
        free_string(ptr); // or libc::free
    };
}

简单来说,原则是谁申请谁释放。

数组参数

通过指针+整数方式传递,指针指定的真实数据,而整数则是数组的大小,这里仅列出相关的代码。

// math.c
int sum01(const int *arr, int length) {
	int total = 0;
	for (int i = 0; i < length; i++) {
		total += arr[i];
	}
	return total;
}

int sum02(const int arr[5]) {
	int total = 0;
	for (int i = 0; i < 5; i++) {
		total += arr[i];
	}
	return total;
}
// main.rs
use std::os::raw::c_int;

//#[link(name = "sysx", kind = "dylib")]
#[link(name = "sysx", kind = "static")]
extern "C" {
    fn sum01(arr: *const c_int, length: c_int) -> c_int;
    fn sum02(arr: &[c_int; 5]) -> c_int;
}

fn main() {
    let numbers: [c_int; 5] = [1, 2, 3, 4, 5];
    unsafe {
        let total = sum01(numbers.as_ptr(), numbers.len() as c_int);
        println!("sum(1, 2, 3, 4, 5) = {}", total);

        let total = sum02(&numbers);
        println!("sum(1, 2, 3, 4, 5) = {}", total);
    }
}

如果是可修改的,那么可以通过 as_mut_ptr() 替换原 as_ptr() 函数。还有一种场景是已经明确知道数组的长度,就可以通过 [c_int; 5] 类似的方式传入。

结构体参数

// math.c
#include <unistd.h>

struct Rectangle {
	int width, height;
};

int area(const struct Rectangle *rect) {
	if (rect == NULL) {
		return 0;
	}
	return rect->height * rect->width;
}
use std::os::raw::c_int;

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

//#[link(name = "sysx", kind = "dylib")]
#[link(name = "sysx", kind = "static")]
extern "C" {
    fn area(rect: &Rectangle) -> c_int;
}

fn main() {
    unsafe {
        let rect = Rectangle {
            width: 10,
            height: 20,
        };
        println!("area(Rectangle<10, 20>)={}", area(&rect));
    }
}

上述是不可变指针,如果是可变指针可以通过 &mut T 方式传入。

回调函数

在 C 中经常使用,这里介绍一个比较复杂的,依赖某个结构体,同时在 Rust 回调函数中会包含相关参数。

// math.h
#ifndef __LIBSYSX_H__
#define __LIBSYSX_H__

struct summer {
	int sum;
	struct {
		void *data;
		int (*func)(struct summer *, void *, int);
	} user;
};

void hello(struct summer *sum);
struct summer *summer_create(int (*func)(struct summer *, void *, int), void *data);

#endif

// math.c
#include <stdlib.h>

#include "math.h"

struct summer *summer_create(int (*func)(struct summer *, void *, int), void *data) {
	struct summer *sum = malloc(sizeof(struct summer));
	if (sum == NULL)
		return NULL;
	sum->sum = 5;
	sum->user.func = func;
	sum->user.data = data;
	return sum;
}

void hello(struct summer *sum) {
	sum->user.func(sum, sum->user.data, 10);
}
use std::os::raw::{c_int, c_void};

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct Summer {
    pub sum: c_int,
    data: *const c_void,
    func: *const c_void,
}

pub type SumHook = unsafe extern "C" fn(*mut Summer, *mut c_void, c_int) -> c_int;

#[link(name = "sysx", kind = "static")]
extern "C" {
    fn hello(sum: *mut Summer);
    fn summer_create(hook: SumHook, data: *mut c_void) -> *mut Summer;
}

pub unsafe extern "C" fn hook(s: *mut Summer, data: *mut c_void, val: c_int) -> c_int {
    let data = &mut *(data as *mut Hello);
    data.sum = val + (*s).sum;
    0
}

#[derive(Debug, Default)]
struct Hello {
    sum: c_int,
}

fn main() {
    let mut data = Hello::default();
    unsafe {
        let summer = summer_create(hook, &mut data as *mut Hello as *mut c_void);
        hello(summer);
        println!("{}", data.sum);
    }
}