likes
comments
collection
share

Rust手动绑定C++库(新中新二代证读卡器dll)

作者站长头像
站长
· 阅读数 13

背景

两个月前的一个项目,接新中新二代身份证读卡器(型号F200)的c++动态库,当时网上Rust绑定C++的资料不是很多,给的demo也只有c#和c++的,那没办法了只能硬上。其实主要卡点在于两点,一是接口文档确实老旧了,新接口没写不说,怎么把数据以正确的方式解析也没有讲;二是第一次用rust接c++,很多类型转化关系不是很清楚,现在有时间就稍微做下总结。

案例

让我们先看看接口文档其中一个接口

**Syn_ReadBaseMsg** 读取身份证内基本信息区域信息。

    int  Syn_ReadBaseMsg(
    int iPort,
    unsigned char* pucCHMsg,
    unsigned int * puiCHMsgLen,
    unsigned char * pucPHMsg,
    unsigned int * puiPHMsgLen, 
    int iIfOpen
    );

    **参数说明:**
    *iPort*
    [in] 整数,表示端口号。参见Syn_ResetSAM。
    *pucCHMsg*
    [out] 无符号字符指针,指向读到的文字信息。
    *puiCHMsgLen*
    [out] 无符号整型数指针,指向读到的文字信息长度。
    *pucPHMsg*
    [out] 无符号字符指针,指向读到的照片信息。
    *puiPHMsgLen*
    [out] 无符号整型数指针,指向读到的照片信息长度。
    *iIfOpen*
    [in] 整数,参见Syn_ResetSAM。
    **返回值:**
    0 读基本信息成功
    其他 读基本信息失败(具体含义参见返回码表)

rust加载dll呢,我使用libloading这个crate,还用到encoding_rs,主要用来解析切片数据

[dependencies]
    libloading = "0.7"
    encoding_rs = "0.8.31"

rust代码

    let func: libloading::Symbol<
            unsafe extern "C" fn(
                port: i32,
                pucCHMsg: *mut u8,
                puiCHMsgLen: *mut u32,
                pucPHMsg: *mut u8,
                puiPHMsgLen: *mut u32,
                if_open: i32,
            ) -> i32,
        > = lib.get("Syn_ReadBaseMsg".as_bytes())?;

我们来看看c++和rust的参数是怎么对应起来的

第一个参数port:

c++和rust都是int类型

第二个参数pucCHMsg:

c++是unsigned char*,rust是*mut u8。这里的*mut表示指向变量的指针,c++通过接收这个指针,修改这个变量。后面跟着u8,因为c++里char*是字符指针,指向了一个字符数组,既然是数组,那数组通常就以第一个元素的地址作为整个数组的地址,unsigned char实际上就是u8。同理有指向变量的指针,也就有指向常量的指针,比如c++里用const char*这个类型,rust就用*const u8。

第三个参数puiCHMsgLen:

c++是unsigned int*,rust是*mut u32。这里的*mut同上,u32即unsigned int,也就是无符号整型。

剩下几个参数大同小异。

生成参数

绑定成功后,要如何调用。

先说参数

    let chmsg: *mut u8 = [0u8; 1024].as_mut_ptr();
    let mut chmsg_len: u32 = 0u32;
    let phmsg: *mut u8 = [0u8; 1024].as_mut_ptr();
    let mut phmsg_len: u32 = 0u32;

这里第二第四个参数整型自然和数组无关,所以直接给他一个0u32即可,但是第一第三个参数我们初始化了一个u8数组,容量1024,并且调用了as_mut_ptr这个方法。 我们来看下它的说明

Returns an unsafe mutable pointer to the slice's buffer.

The caller must ensure that the slice outlives the pointer this function returns, or else it will end up pointing to garbage.

Modifying the container referenced by this slice may cause its buffer to be reallocated, which would also make any pointers to it invalid

大意就是获取字节切片缓冲区的一个不安全的可变指针。而且我们要保证这个指针的生命周期要始终小于等于缓冲区。

传参

    let ret = func(port, chmsg, &mut chmsg_len, phmsg, &mut phmsg_len, if_open);

传参我想没什么好说的

处理返回值或者指针

    let chmsg_res: &[u8] = std::slice::from_raw_parts(chmsg, chmsg_len.try_into().unwrap());
    let (res, ..) = encoding_rs::UTF_16LE.decode(chmsg_res);
    let phmsg_res: &[u8] = std::slice::from_raw_parts(phmsg, phmsg_len.try_into().unwrap());

由于我们传入的是变量的原始指针,在传入C后,进行了修改,现在我们想要把指针里的数据读出来,就要通过from_raw_parts这个方法。

Forms a slice from a pointer and a length.
The `len` argument is the number of **elements**, not the number of bytes.

翻译过来就是根据指针和长度形成切片。len是元素的数量,而不是字节的数量。

这里查看二代证机读信息说明文档

Rust手动绑定C++库(新中新二代证读卡器dll)

发现文字信息采用的是UCS-2进行存储,UCS-2使用固定长度的16位编码,每个字符都由2字节Unicode代码表示,最高有效字节在前,也就是小端存储。所以这里使用encoding_rs::UTF_16LE.decode解析出信息。

最后由于调用c++动态库方法在rust看来是不安全操作,所以我们需要把以上所有的代码用unsafe关键字包一下。

完整代码

    pub(crate) fn read_base_msg(
        lib: &libloading::Library,
        port: i32,
        if_open: i32,
    ) -> Result<ReadBaseMsgRes> {
        unsafe {
            let func: libloading::Symbol<
                unsafe extern "C" fn(
                    port: i32,
                    pucCHMsg: *mut u8,
                    puiCHMsgLen: *mut u32,
                    pucPHMsg: *mut u8,
                    puiPHMsgLen: *mut u32,
                    if_open: i32,
                ) -> i32,
            > = lib.get("Syn_ReadBaseMsg".as_bytes())?;
            let pucCHMsg = [0u8; 1024].as_mut_ptr();
            let mut puiCHMsgLen = 0u32;
            let pucPHMsg = [0u8; 1024].as_mut_ptr();
            let mut puiPHMsgLen = 0u32;
            // 传参
            let ret = func(port, pucCHMsg, &mut puiCHMsgLen, pucPHMsg, &mut puiPHMsgLen, if_open);
            let chmsg_res = std::slice::from_raw_parts(pucCHMsg, puiCHMsgLen.try_into().unwrap());
            let (res, ..) = encoding_rs::UTF_16LE.decode(chmsg_res);
            let phmsg_res = std::slice::from_raw_parts(pucPHMsg, puiPHMsgLen.try_into().unwrap());
            let mut buf = std::fs::File::create("photo.WLT")?;
            use std::io::Write;
            buf.write_all(phmsg_res)?;
            buf.flush()?;
            Ok(ReadBaseMsgRes {
                ret,
                msg: res.to_string().into(),
            })
        }
    }

技巧

我在接动态库接口的时候,发现照着文档怎么也接不上(其实是接口文档写错了)。于是我直接去看demo,把c++demo里的接口复制出来,用bindgen这个crate来自动绑定

    [build-dependencies]
    bindgen = "0.64"

比如我想要绑定以下的c++结构体和方法

wrapper.h

    typedef struct tagIDCardData {
        char Name[32];      //姓名      
        char Sex[6];        //性别
        char Nation[64];    //名族
        char Born[18];      //出生日期
        char Address[72];   //住址
        char IDCardNo[38];  //身份证号
        char GrantDept[32]; //发证机关
        char UserLifeBegin[18]; // 有效开始日期
        char UserLifeEnd[18];   // 有效截止日期
        char PassID[20];        // 通行证号码
        char IssuesTimes[6];    //签发次数
        char reserved[12];      // 保留
        char PhotoFileName[255];// 照片路径
        char CardType[4];    //证件类型
        char EngName[122];   //英文名
        char CertVol[6];     //证件版本号
    }IDCardData;

    int _stdcall Syn_ReadMsg(int iPort, int iIfOpen, IDCardData *pIDCardData);

build.rs如下写法

    use std::{env, path::PathBuf};

    fn main() {
        let bindings = bindgen::Builder::default()
            //头文件路径
            .header("wrapper.h")
            .parse_callbacks(Box::new(bindgen::CargoCallbacks))
            .generate()
            .expect("Unable to generate bindings");
        let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
        bindings
            .write_to_file(out_path.join("bindings.rs"))
            .expect("Couldn't write bindings!");
    }

编译执行后,在target->debug->build下找到生成的rs文件

    #[repr(C)]
    #[derive(Debug, Copy, Clone)]
    pub struct tagIDCardData {
        pub Name: [u8; 32usize],
        pub Sex: [u8; 6usize],
        pub Nation: [u8; 64usize],
        pub Born: [u8; 18usize],
        pub Address: [u8; 72usize],
        pub IDCardNo: [u8; 38usize],
        pub GrantDept: [u8; 32usize],
        pub UserLifeBegin: [u8; 18usize],
        pub UserLifeEnd: [u8; 18usize],
        pub PassID: [u8; 20usize],
        pub IssuesTimes: [u8; 6usize],
        pub reserved: [u8; 12usize],
        pub PhotoFileName: [u8; 255usize],
        pub CardType: [u8; 4usize],
        pub EngName: [u8; 122usize],
        pub CertVol: [u8; 6usize],
    }

    pub type IDCardData = tagIDCardData;
    extern "C" {
        pub fn Syn_ReadMsg(
            iPort: ::std::os::raw::c_int,
            iIfOpen: ::std::os::raw::c_int,
            pIDCardData: *mut IDCardData,
        ) -> ::std::os::raw::c_int;
    }

这样不用手动绑定,就可以直接拿来用了,大家可以对照一下自己手动绑定和工具绑定的代码,看看有什么不同,然后试试把u8换成i8,看看会发生什么。其实char对应i8和u8在这里没区别,都是8位大小的空间,只不过用u8的话方便String和Vec操作。

引用文章:

C++与Rust数据类型对应关系

Rust FFI 编程 - 手动绑定 C 库入门 01

ucs2字符集(UCS-2 Unicode编码)