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

目录
  • Rust语言调用C语言接口
  • Rust语言将字符串转换为整型
  • 编写build.rs自动化编译外部模块
  • 简单的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接口内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C语言在头文件中定义const变量详解

    C语言在头文件中定义const变量详解 在头文件中定义const不会有多变量的警告或错误,如果该头文件被大量包含会造成rom空间的浪费. 通过查看*.i文件的展开呢,可以发现每个.i文件都会有相应的变量展开. 查看*.map文件,能查看到该变量的多个地址分配. 在预编译的时候如果在头文件定义了const变量,每一个包含该头文件的c文件都会将其展开,而在编译的时候不会报错,因为这符合语法规则,每一个包含这个头文件的*.c文件都会编译一次这个变量,分配一个新的地址,然后在链接的时候也不会报错,因为每

  • C语言头文件<string.h>函数详解

    目录 1. strlen —— 求字符串长度 1.1 strlen 的声明与用处 1.2 strlen 的用法 1.3 strlen 的模拟实现 2. strcpy —— 字符串拷贝 2.1 strcpy 的声明与用处 2.2 strcpy 的用法 2.3 strcpy 的模拟实现 3. strcmp —— 字符串比较 3.1 strcmp 的声明与用处 3.2 strcmp 的用法 3.3 strcmp 的模拟实现 4. strcat —— 字符串追加 4.1 strcat 的声明与用处 4.

  • 仿写C语言string.h头文件检验字符串函数

    目录 c语言string.h头文件字符串检验函数仿写 strlen字符串求长度 strcmp / strncmp字符串比较 strchr / strrchr 字符串中查找字符ch 第一个出现的字符ch 最后一个出现的字符ch strstr 字符串匹配:src_str中查找des_str,返回匹配的起始位置,没有为NULL(BF算法) strcpy / strncpy字符串拷贝 strcat / strncat字符串的粘贴 strdup 字符串申请堆区空间存放字符串的副本 总结 c语言string

  • C语言中常用的几个头文件及库函数

    不完全统计,C语言标准库中的头文件有15个之多,所以我主要介绍常用的这四个头文件stdio.h,string.h,math.h,stdlib.h,以后用到其他的再做补充.下面上干货: 1.<stdio.h>:定义了输入输出函数.类型以及宏,函数几乎占了标准库的1/3. (1)文件访问. FILE *fopen("filename","mode"): 以mode模式打开地址为'filename'的文件,并返回文件指针. 访问模式主要是"r&quo

  • C语言编程入门之程序头文件的简要解析

    头文件是扩展名为.h的文件,其中包含C函数的声明和宏定义,也可以多个源文件之间共享.有两种类型的头文件:程序员编写的文件,和编译器中附带的文件. 要求使用头文件的程序,包括通过它,使用C语言预处理指令#include就像所看到的包含stdio.h头文件,它随着编译器自带. 包括一个头文件等于复制头文件的内容,但我们不这样做,因为这很容易出错,一个好主意是我们不复制头文件的内容,特别是包括多个程序的源文件. 在C或C++程序的简单做法是,我们把所有的常量,宏全系统全局变量和函数原型在头文件,其中包

  • 浅析C语言头文件和库的一些问题

    使用gcc的编译器 头文件没有包含stdlib.h,使用atoi函数(atoi函数在stdlib.h中才有声明),编译却没有出错 如果编译的时候加上-Wall选项,会有个警告,请问这是为什么?这是因为C语言一个非常傻的规定:一个函数如果没有声明函数原型,其返回值类型就是int(所谓的implicit declaration).由于atoi恰好真返回int,所以你即使不包含它的头文件也不报错.至于这个警告,是为了避免你由于忘记声明函数原型而出错. 编译器对于没有定义过的函数原型直接当作它返回int

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

    目录 Rust语言调用C语言接口 Rust语言将字符串转换为整型 编写build.rs自动化编译外部模块 简单的C语言头文件 Rust语言调用C语言接口 嵌入式系统层及应用层的软件开发,离不开C语言.笔者希望使用一种高效.稳定的开发语言,在一定程度上替代C语言,从而提高开发效率.降低嵌入式软件的扩展.维护成本,同时缩小研发团队规模.Rust编程语言很好地满足了高效.稳定这两个要求.不过需要在一定程度解决Rust调用外部C语言模块的问题:Rust语言已提供了完善的解决方案,笔者希望通过本文做一个必

  • 用C#把文件转换为XML的代码

    using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.IO; using System.Xml;  namespace MyWindows { /**//// <summary> /// 这个示例演示如何把Office文件编码为xml文件以及如何把生成的xml文件转换成Office文件

  • 易语言实现文件夹加密的代码

    加密锁上文件夹 此功能需要加载精易模块5.6 .版本 2 .支持库 shell .支持库 spec .支持库 dp1 .程序集 窗口程序集1 .程序集变量 密码, 文本型 .子程序 _按钮_浏览文件_被单击 编辑框_路径.内容 = 浏览文件夹 ("打开你需要加密的文件", ) .子程序 _按钮_加密_被单击 .局部变量 文件夹名称, 文本型 .判断开始 (寻找文本 (编辑框_路径.内容, "[已加密]", , 假) ≠ -1) 提示框 ("已加密"

  • PHP调用wsdl文件类型的接口代码分享

    复制代码 代码如下: <?php // 本类由系统自动生成,仅供测试用途 class IndexAction extends Action {     public function index(){         //#分销商订单提交.修改.取消.查询接口          $wsdl1='http://127.0.0.1:8080/ejfxs/services/order?wsdl';          //#分销商可销售产品接口地址         $wsdl='http://127.0

  • c++ 防止头文件重复引入的三种方法

    在之前我们详细介绍了 C 语言中如何使用宏定义(#ifndef / #define / #endif)来有效避免头文件被重复 #include,此方式在 C++ 多文件编程中也很常用. 举个例子,如下是一个 C++ 项目,其内部含有 school.h 和 student.h 这 2 个头文件以及 main.cpp 源文件,其各自包含的代码为: //student.h class Student {     //...... }; //school.h #include "student.h&qu

  • 浅谈iOS关于头文件的导入问题

    关于import和@class的使用问题 一.@class的出现可以适当减少编译时间,提高效率. 现在假设有两个类,一个是Teacher类,一个是Students类. Teacher类 #import <Foundation/Foundation.h> #import "Students.h" @interface Teacher : NSObject @property (copy,nonatomic)NSString *firstName; @property (cop

随机推荐