如何如何使用bindgen将C语言头文件转换为Rust接口代码

Rust语言调用C语言接口 嵌入式系统层及应用层的软件开发,离不开C语言。笔者希望使用一种高效、稳定的开发语言,在一定程度上替代C语言,从而提高开发效率、降低嵌

Rust语言调用C语言接口

嵌入式系统层及应用层的软件开发,离不开C语言。笔者希望使用一种高效、稳定的开发语言,在一定程度上替代C语言,从而提高开发效率、降低嵌入式软件的扩展、维护成本,同时缩小研发团队规模。Rust编程语言很好地满足了高效、稳定这两个要求。不过需要在一定程度解决Rust调用外部C语言模块的问题:Rust语言已提供了完善的解决方案,笔者希望通过本文做一个必要的记录。

笔者在之前一篇文章中简要介绍了Rust语言调用C语言动态库提供的函数的一般方法。不过随着Rust工程依赖的外部的C语言模块越来越来复杂,手工将C语言头文件定义的调用接口转换为Rust接口代码变得不具可操作性。幸运的是,一个名为bindgen的开源项目很好地解决了这个问题,它通过clang编译器库对C语言的头文件进行预处理,并生成相应的Rust接口代码;本文参考了其官方文档,结合笔者的开发需要作简要的使用说明。

Rust语言将字符串转换为整型

笔者在实际开发过程,需要将Rust的一个字符串类型转换为整型,Rust柡准库已经提供了相应的转换函数parse

pub fn parse<F>(&self) -> Result<F, <F as FromStr>::Err>where
    F: FromStr,
...
let four: u32 = "4".parse().unwrap();
assert_eq!(4, four);

不过,parse函数的缺陷是,它要求输入的字符串是十进制的,对于"0x1234"之类的非十六进制数,则不能正确处理。然而,柡准库也提供了另一个函数from_str_radix,可指定任意仍意进制的字符串到整型:

pub fn from_str_radix(src: &str, radix: u32) -> Result<i64, ParseIntError>
...
assert_eq!(i64::from_str_radix("A", 16), Ok(10));

结合这两个柡准库提供的函数,就可以编写一个纯粹的Rust函数,根据字符串的前缀决定调用哪一个转换函数了。不过笔者是怀旧的,希望继续调用C语言柡准库提供的函数strtoll/strtoull,这两个函数可以自动判断字符串的进制(尽管仅限于几个进制)。

笔者为Rust工程编写的代码如下(完整代码可参考此处):

/* extmodule/extmodule.h */
#ifndef RUST_EXTMODULE_H
#define RUST_EXTMODULE_H 1

int extm_strtol(const char * strp, long long * valp, int base);
int extm_strtoul(const char * strp, unsigned long long * valp, int base);

#endif

/* extmodule/extmodule.c */
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

int extm_strtol(const char * strp, long long * valp, int base)
{
    long long ret;
    int error = EINVAL;
    char * strend = NULL;

    if (strp == NULL)
        return error;

    errno = 0;
    ret = strtoll(strp, &strend, base);
    error = errno;
    if (error || strend == strp)
        return (error > 0) ? error : EINVAL;

    if (valp != NULL)
        *valp = ret;
    return 0;
}

int extm_strtoul(const char * strp, unsigned long long * valp, int base)
{
    int error = EINVAL;
    char * strend = NULL;
    unsigned long long ret;

    if (strp == NULL)
        return error;

    errno = 0;
    ret = strtoull(strp, &strend, base);
    error = errno;
    if (error || strend == strp)
        return (error > 0) ? error : EINVAL;

    if (valp != NULL)
        *valp = ret;
    return 0;
}

之后,笔者对这两个函数extm_strtol/extm_strtoul进一步封装:

// src/lib.rs
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
#![allow(non_upper_case_globals)]

use std::os::raw::c_int;
use std::os::raw::c_longlong;
use std::os::raw::c_ulonglong;

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

pub fn strtol(x: &str, base: i32) -> Result<i64, std::io::Error> {
    let mut res: c_longlong = 0;
    let y: Vec<u8> = x.as_bytes().iter().cloned().collect();
    let error = unsafe {
        let z = std::ffi::CString::from_vec_unchecked(y);
        extm_strtol(z.as_ptr(), &mut res as *mut c_longlong, base as c_int)
    };
    match error {
        0 => Ok(res as i64),
        _ => Err(std::io::Error::from_raw_os_error(error as i32)),
    }
}

pub fn strtoul(x: &str, base: i32) -> Result<u64, std::io::Error> {
    let mut res: c_ulonglong = 0;
    let y: Vec<u8> = x.as_bytes().iter().cloned().collect();
    let error = unsafe {
        let z = std::ffi::CString::from_vec_unchecked(y);
        extm_strtoul(z.as_ptr(), &mut res as *mut c_ulonglong, base as c_int)
    };
    match error {
        0 => Ok(res as u64),
        _ => Err(std::io::Error::from_raw_os_error(error as i32)),
    }
}

这样,笔者就得到了两个Rust语言版本的字符串到整型的转换函数,strtol/strtoul。接下来就要解决编译的问题,即将extmodule/extmodule.h头文件转换为src/lib.rs包含的接口文件,bindings.rs

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

此外还要求在为嵌入式设备编译构建时,也能够交叉编译extmodule模块。

编写build.rs自动化编译外部模块

笔者参考了bindgen的相关文档,调用相关的binding接口,将extmodule/extmodule.h转换为$(OUT_DIR)目录下的bindings.rs接口代码;之后又调用了make命令行工具,实现extmodule的(交叉)编译,生成libextm.so动态库:

extern crate bindgen;
use std::process::Command;

fn main() {
    // generate binding.rs for extmodule
    let bindings = bindgen::Builder::default()
        .header("extmodule/extmodule.h")
        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
        .generate()
        .expect("Unable to generate bindings");
    let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
    bindings.write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");

    // invoke make to build external C module
    let cc = format!("CC={}", std::env::var("TARGET_CC")
        .unwrap_or_else(|_| "cc".to_string()));
    let cflags = format!("CFLAGS={}", std::env::var("TARGET_CFLAGS")
        .unwrap_or_else(|_| "-Wall -fPIC -D_GNU_SOURCE -Os -ggdb".to_string()));
    let okay = Command::new("make")
        .arg(AsRef::<std::ffi::OsStr>::as_ref(&cc))
        .arg(AsRef::<std::ffi::OsStr>::as_ref(&cflags))
        .args(&["-C", "./extmodule", "-j1", "clean", "all"])
        .spawn()
        .expect("Failed to invoke make utility")
        .wait()
        .expect("Failed to wait make utility")
        .success();
    if !okay {
        eprintln!("Error, make for external C module has failed!");
        std::process::exit(1);
    }

    println!("cargo:rustc-link-lib=extm");
    println!("cargo:rustc-link-search=./extmodule");
    println!("cargo:rerun-if-changed=./extmodule/extmodule.h");
    println!("cargo:rustc-link-arg-bins=-Wl,-rpath=$ORIGIN");
}

在编译之前,需要为系统安装clang相关的依赖,这是bindgen需要的:

sudo apt install clang-14 libclang-14-dev # for ubuntu-22.04

笔者编译、运行bindings工程的输出结果如下:

yejq@ubuntu:~/program/bindings$ cargo build --release
   Compiling bindings v0.1.0 (/home/yejq/program/bindings)
    Finished release [optimized] target(s) in 0.73s
yejq@ubuntu:~/program/bindings$ cp -v ./extmodule/libextm.so ./target/release/
'./extmodule/libextm.so' -> './target/release/libextm.so'
yejq@ubuntu:~/program/bindings$ ./target/release/bindings 2099 0x2030
arg0: 2099, arg1: 0x2030
System uptime: 86910
total 0
lrwx------ 1 yejq yejq 64  1月 25 11:35 0 -> /dev/pts/11
lrwx------ 1 yejq yejq 64  1月 25 11:35 1 -> /dev/pts/11
lrwx------ 1 yejq yejq 64  1月 25 11:35 2 -> /dev/pts/11
lr-x------ 1 yejq yejq 64  1月 25 11:35 3 -> /dev/null
lr-x------ 1 yejq yejq 64  1月 25 11:35 4 -> /proc/17644/fd
total 0
lrwx------ 1 yejq yejq 64  1月 25 11:35 0 -> /dev/pts/11
lrwx------ 1 yejq yejq 64  1月 25 11:35 1 -> /dev/pts/11
lrwx------ 1 yejq yejq 64  1月 25 11:35 2 -> /dev/pts/11
lr-x------ 1 yejq yejq 64  1月 25 11:35 3 -> /proc/17645/fd

可以看到,使用extmodule外部模块,可以很好地解决十六进制字符串0x2030转换为整型的问题。

使用bindgen命令行工具转换接口文件

Rust语言、工具链开发者选择Rust作为自定义编译构建的语言,相应的代码为工作根目录下的build.rs。该代码依赖了bindgen库,将extmodule/extmodule.h转化为Rust编程语言的接口文件,这一依赖在Cargo.toml需要指明:

[dependencies]

[build-dependencies]
bindgen = "0.62.0"

有人可能会提议,使用build.rs作为自定义编译构建代码,可能不太方便,因为某些工程不产生以上依赖,而是使用bindgen命令行工具实现以上C语言头文件到bindings.rs接口的转换,那么使用shell脚本就更合理。例如build.rs.sh脚本实现了目前的build.rs所有功能:

#!/bin/bash

# Created by yejq.jiaqiang@gmail.com
# Simple build script for bindtest
# 2023/01/24

# generate bindings.rs source file in `$(OUT_DIR) directory
generate_bindings() {
    if [ ! -d "${OUT_DIR}" ] ; then
        echo "Error, \`\${OUT_DIR} not found." 1>&2
        return 1
    fi
    bindgen -o "${OUT_DIR}/bindings.rs" 'extmodule/extmodule.h'
    return $?
}

compile_extmodule() {
    local COMPILER="${TARGET_CC:-gcc}"
    local C_FLAGS="${TARGET_CFLAGS:--Wall -fPIC -Os -D_GNU_SOURCE -ggdb}"
    make "CC=${COMPILER}" "CFLAGS=${C_FLAGS}" -C extmodule -j1 clean all
    return $?
}

define_rustc_flags() {
    echo "cargo:rustc-link-lib=extm"
    echo "cargo:rustc-link-search=./extmodule"
    echo "cargo:rerun-if-changed=./build.rs.sh"
    echo "cargo:rerun-if-changed=./extmodule/extmodule.h"
    echo "cargo:rustc-link-arg-bins=-Wl,-rpath=\$ORIGIN"
    return 0
}

generate_bindings || exit $?
compile_extmodule || exit $?
define_rustc_flags ; exit 0

该脚本,即简洁,又具备很强的扩展性,修改起来又比build.rs方便很多;确实是这样。那么可以修改build.rs脚本,实现对该脚本的一劳永逸的调用:

diff --git a/Cargo.toml b/Cargo.toml
index a57c279..3b947ae 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,4 +8,4 @@ edition = "2021"
 [dependencies]
 
 [build-dependencies]
-bindgen = "0.62.0"
+libc = { version = "0.2.139" }
diff --git a/build.rs b/build.rs
index 6a2fff1..dddf1f8 100644
--- a/build.rs
+++ b/build.rs
@@ -1,38 +1,17 @@
-extern crate bindgen;
-use std::process::Command;
+use std::ffi::CString;
+use libc::{c_char, execv};
+use std::collections::VecDeque;
 
 fn main() {
-    // generate binding.rs for extmodule
-    let bindings = bindgen::Builder::default()
-        .header("extmodule/extmodule.h")
-        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
-        .generate()
-        .expect("Unable to generate bindings");
-    let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
-    bindings.write_to_file(out_path.join("bindings.rs"))
-        .expect("Couldn't write bindings!");
-
-    // invoke make to build external C module
-    let cc = format!("CC={}", std::env::var("TARGET_CC")
-        .unwrap_or_else(|_| "cc".to_string()));
-    let cflags = format!("CFLAGS={}", std::env::var("TARGET_CFLAGS")
-        .unwrap_or_else(|_| "-Wall -fPIC -D_GNU_SOURCE -Os -ggdb".to_string()));
-    let okay = Command::new("make")
-        .arg(AsRef::<std::ffi::OsStr>::as_ref(&cc))
-        .arg(AsRef::<std::ffi::OsStr>::as_ref(&cflags))
-        .args(&["-C", "./extmodule", "-j1", "clean", "all"])
-        .spawn()
-        .expect("Failed to invoke make utility")
-        .wait()
-        .expect("Failed to wait make utility")
-        .success();
-    if !okay {
-        eprintln!("Error, make for external C module has failed!");
-        std::process::exit(1);
-    }
-
-    println!("cargo:rustc-link-lib=extm");
-    println!("cargo:rustc-link-search=./extmodule");
-    println!("cargo:rerun-if-changed=./extmodule/extmodule.h");
-    println!("cargo:rustc-link-arg-bins=-Wl,-rpath=$ORIGIN");
+    // invoke build.rs.sh script instead
+    let argv: Vec<String> = std::env::args().skip(1).collect();
+    let mut argw: VecDeque<CString> = argv.iter()
+        .map(|x| CString::new(x.as_bytes()).unwrap()).collect();
+    argw.push_front(CString::new("./build.rs.sh").unwrap());
+    let mut argx: Vec<*const c_char> = argw.iter().map(|y| y.as_ptr()).collect();
+    argx.push(std::ptr::null());
+    unsafe { execv(argx[0], argx.as_mut_ptr()) };
+    eprintln!("Error, failed to invoke ./build.rs.sh: {:?}",
+        std::io::Error::last_os_error());
+    std::process::exit(1);
 }

简单的C语言头文件

以上的编译构建,考虑到了对嵌入式设备支持。主要是在build.rs(或build.rs.sh)访问TARGET_CC/TARGET_CFLAGS两个与交叉编译相关的环境变量。不过,值得说明的是,对于简单的C语言头文件(例如笔者编写的extmodule/extmodule.h)可以这样转换,但对于复杂的开源库,交叉编译时,因其头文件比较复杂,这种基于bindgen的接口转换常常是不可用的。举个例子,对于开源的paho.mqtt.rust软件,因其依赖了paho.mqtt.c库,在交叉编译时,就会使用该工程自己维护的bindings接口代码,而不是使用bindgen来转换:

yejq@ubuntu:~/program/paho.mqtt.rust/paho-mqtt-sys/bindings$ ls -lh
total 1.7M
-rw-rw-r-- 1 ubuntu ubuntu 267K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-aarch64-unknown-linux-gnu.rs
-rw-rw-r-- 1 ubuntu ubuntu 216K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-armv7-unknown-linux-gnueabihf.rs
-rw-rw-r-- 1 ubuntu ubuntu 216K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-default-32.rs
-rw-rw-r-- 1 ubuntu ubuntu 265K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-default-64.rs
-rw-rw-r-- 1 ubuntu ubuntu 281K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-x86_64-apple-darwin.rs
-rw-rw-r-- 1 ubuntu ubuntu 211K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-x86_64-pc-windows-msvc.rs
-rw-rw-r-- 1 ubuntu ubuntu 265K Jan 25 11:36 bindings_paho_mqtt_c_1.3.12-x86_64-unknown-linux-gnu.rs
drwxrwxr-x 2 ubuntu ubuntu 4.0K Jan 25 11:36 old

虽然如此,我们在嵌入式软件开发时,可以编写易于转换的C语言头文件,这就需要我们在实际开发中不断调整头文件的编写。

到此这篇关于使用bindgen将C语言头文件转换为Rust接口代码的文章就介绍到这了,更多相关C语言头文件转换为Rust接口内容请搜索好代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持好代码网!