C/C++ 与Rust的混合编译

主要参考自build-script-examplescc

一、考虑build.rs

假设有C源文件src/hello.c,其内容如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
const char *hello_str = "Hello World from C Code!";

char *str_from_c(char * rust_str) {
    char *leak_str = (char *)malloc(strlen(hello_str) + 1);
    memcpy(leak_str, hello_str, strlen(hello_str) + 1);
    leak_str[strlen(hello_str) + 1] = 0;
    printf("print rust_str in C: %s\n", rust_str);
    return leak_str;
}

int int_from_c(int rust_int) { 
    printf("print rust_int in C: %d\n", rust_int);
    return 12345;
}

在这里有两个函数,一个str_from_c,一个int_from_c。接下来考虑如何在Rust中调用这两个函数。

假设有Rust源文件src/main.rs,首先在Rust中声明这两个函数:

extern "C" {
    pub fn str_from_c(rust_str : *const::std::os::raw::c_char) -> *mut ::std::os::raw::c_char;
}
extern "C" {
    pub fn int_from_c(rust_int : i32) -> std::os::raw::c_int;
}

因为这两个函数是C的函数,所以在调用的时候,需要使用unsafe关键字来包裹。

fn main (){
    let rust_str = String::from("Hello World from Rust Code!");
    unsafe {
        let c_str = str_from_c(rust_str.as_ptr() as *const::std::os::raw::c_char);
        let c_str_rust_version = std::ffi::CString::from_raw(c_str);
        println!("{:?}", c_str_rust_version); 
    }
    let rust_int = 54321;
    unsafe{
        let c_int = int_from_c(rust_int);
        println!("{}", c_int);
    }
}

将上述Rust源文件内容写入src/main.rs之后,便可以开始考虑编写build.rs.

build.rs可以看作一个单独的Rust项目,在编译当前package之前,cargo会编译并运行build.rs以构建非Rust代码并获取链接地点。

考虑gcc指令语句:

std::process::Command::new("gcc")
    .args(&["-c", "-fPIC", "src/hello.c", "-o"])
    .arg(format!("{}/hello.o", out_dir))
    .status()
    .unwrap();

src/hello.c源文件编译成目标文件。

再使用ar指令语句:

std::process::Command::new("ar")
    .args(&["rcus", "libhello.a", "hello.o"])
    .current_dir(&std::path::Path::new(&out_dir))
    .status()
    .unwrap();

将目标文件编译成静态库。

目前静态库的所在路径为${out_dir}/libhello.aout_dir其实可以随意制定,但是cargo的构建模式里对每个package设定了中间产物存放路径,在这里变量out_dir来自环境变量OUT_DIR为:

let out_dir = std::env::var("OUT_DIR").unwrap();

最后调用println!打印数据告诉cargo要链接的库以及库的路径:

println!("cargo::rustc-link-search=native={}", out_dir);
println!("cargo::rustc-link-lib=static=hello");

将上述代码组合,得到以下build.rs代码。

fn main() {
    let out_dir = std::env::var("OUT_DIR").unwrap();
    std::process::Command::new("gcc")
        .args(&["-c", "-fPIC", "src/hello.c", "-o"])
        .arg(format!("{}/hello.o", out_dir))
        .status()
        .unwrap();
    std::process::Command::new("ar")
        .args(&["rcus", "libhello.a", "hello.o"])
        .current_dir(&std::path::Path::new(&out_dir))
        .status()
        .unwrap();
    println!("cargo::rustc-link-search=native={}", out_dir);
    println!("cargo::rustc-link-lib=static=hello");
    println!("cargo::rerun-if-changed=src/hello.c");
}

运行cargo run得到以下输出:

$ cargo run
print rust_str in C: Hello World from Rust Code!
"Hello World from C Code!"
print rust_int in C: 54321
12345

二、如果是C++

假设有如下src/hello.cpp代码:

#include <iostream>
#include <cstring>
const char *hello_str = "Hello World from C++ Code!";

char *str_from_c(char *rust_str) {
    char *leak_str = new char[strlen(hello_str) + 1];
    memcpy(leak_str, hello_str, strlen(hello_str) + 1);
    leak_str[strlen(hello_str) + 1] = 0;
    std::cout << "print rust_str in C++:" << rust_str << std::endl;
    return leak_str;
}

int int_from_c(int rust_int) {
    std::cout << "print rust_int in C++: %d\n" << rust_int << std::endl;
    return 12345;
}

在这里有一个问题,C++代码在编译成二进制文件之后,函数名会被mangle,在Rust中简单地指明str_fromint_from_c是找不到对应的符号的,要使用#[link_name = ???]来标明编译mangle之后的符号名。

想要获得mangle之后的符号,据笔者目前所知没有针对单条声明语句的处理方法,需要将整个函数声明以及声明所依赖的内容扔进前端处理之后再获取。

为了将问题简化,因此使用现成的工具bindgen

该工具的命令行版本可通过:

cargo install bindgen-cli

安装。

针对上述src/hello.cpp编写一个声明头文件src/hello.hpp

char *str_from_c(char *rust_str);

int int_from_c(int rust_int);

采用bindgen将其翻译:

bindgen src/hello.hpp -o lib.rs

可以得到:

/* automatically generated by rust-bindgen 0.70.1 */

extern "C" {
    #[link_name = "\u{1}_Z10str_from_cPc"]
    pub fn str_from_c(rust_str: *mut ::std::os::raw::c_char) -> *mut ::std::os::raw::c_char;
}
extern "C" {
    #[link_name = "\u{1}_Z10int_from_ci"]
    pub fn int_from_c(rust_int: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
}

src/main.rs中使用这两个函数:

fn main() {
    let rust_str = String::from("Hello World from Rust Code!");
    unsafe {
        let c_str = cxxtt::str_from_c(rust_str.as_ptr() as *mut::std::os::raw::c_char);
        let c_str_rust_version = std::ffi::CString::from_raw(c_str);
        println!("{:?}", c_str_rust_version); 
    }
    let rust_int = 54321;
    unsafe{
        let c_int = cxxtt::int_from_c(rust_int);
        println!("{}", c_int);
    }
}

注意,cxxtt当前crate的名字。

至于build.rs的编写,其内容大同小异:

fn main() {
    let out_dir = std::env::var("OUT_DIR").unwrap();
    std::process::Command::new("g++")
        .args(&["-c", "-fPIC", "src/hello.cpp", "-o"])
        .arg(format!("{}/hello.o", out_dir))
        .status()
        .unwrap();
    std::process::Command::new("ar")
        .args(&["rcus", "libhello.a", "hello.o"])
        .current_dir(&std::path::Path::new(&out_dir))
        .status()
        .unwrap();
    
    println!("cargo::rustc-link-search=native={}", out_dir);
    println!("cargo::rustc-link-lib=dylib=stdc++");
    println!("cargo::rustc-link-lib=static=hello");
    println!("cargo::rerun-if-changed=src/hello.cpp");
}

关键不同在于语句:

println!("cargo::rustc-link-lib=dylib=stdc++");

当前的Rust程序会有对libc.so的依赖,但是没有对libstdc++.so的依赖,如果不指明,将无法使用C++的标准函数。

运行cargo run得到以下输出:

print rust_str in C++:Hello World from Rust Code!
"Hello World from C++ Code!"
print rust_int in C++: %d
54321
12345