27. Rust与C/C++互操作实战指南

1.前言

由于rust诞生时间太短,目前生态不够完善,因此大量的功能库都需要依赖于C、C++语言的历史积累。

而本文将要介绍的便是如何实现rust与c乃至c++之间实现互相调用。

2.动态库调用

首先最方便的还是动态库形式,大量的C语言代码库可以被编译为平台动态库、导出相应的函数,rust直接加载对应的动态库完成调用,这也是目前使用最广的方式。

比如rust中的windows、fltk-rs等等,由于它们之前已经用C语言实现了大量的代码、十几年、乃至几十年的积淀,短时间内难以用rust重写一遍。

所以为了能让rust可以使用它们,常常就是采用动态库调用的方式。

比如在windows系统上,一个最简单常用的winapi:MessagBoxW,该函数用于弹出一个窗口。

此时如果我们想要在rust代码中调用这个函数(在没有官方提供的windows crate前提下),那么就可以先去官方文档看看它所在位置MessageBoxW

image.png

可以看到,官方文档中注明了它在User32.dll这个动态库中。

然后你可以下载everything等工具全局搜索这个动态库在哪里,找到它的路径(一般默认在C:\Windows\System32\user32.dll),然后通过一些工具查看它的导出函数,就可以看到它。

比如我用vs本身提供的一个叫做dumpbin的工具,查看这个动态库的所有导出函数:

dumpbin /EXPORTS C:\Windows\System32\user32.dll

注意如果你想要执行这条命令,你需要进入vs自带的那个控制台中才能找到dumpbin这个命令。

然后搜索一下你就会发现它确实在这个动态库中:

image.png

然后我们就可以来到下一步,在rust中调用这个函数。

如果你对C++的Windows编程比较熟悉的话,就知道一般调用动态库有两种方式,一种是静态加载、一种是动态加载。

其中静态加载一般意味着你想要结合静态库一起使用,这样的加载方式意味着你的exe程序只有找到了这个动态库才能运行,如果找不到,那么就无法运行、并且会报错找不到相关的dll文件。

而动态加载意味着你可以让exe先跑起来、再去动态加载相关的dll文件,即使找不到、我们也可以主动抛出错误、或者寻找替代解决方案。

首先我们先来试试动态加载方案。

2.1 动态加载

动态加载动态库是一个平台通用的方案,仅仅只是底层调用的系统api不同而已,因此我们不需要再自己去rust中导出一遍各个平台的api用于打开动态库、转换相应的函数。

直接使用别人封装好的crate即可:

libloading = "0.8.6"

然后我们就可以在代码中完成下面这样的函数定义与加载:

use libloading::{Library, Symbol};
use std::ffi::{c_uint, c_void};
// 定义函数指针类型
type MessageBoxWFn = unsafe extern "system" fn(
    hWnd: *mut c_void,
    lpText: *const u16,
    lpCaption: *const u16,
    uType: c_uint,
) -> c_uint;

fn main() {
    // 加载User32.dll
    unsafe {
        let lib = Library::new(r#"C:\Windows\System32\user32.dll"#).unwrap();
        // 获取函数指针
        let func: Symbol<MessageBoxWFn> = lib.get(b"MessageBoxW").unwrap();

        // 准备参数(使用UTF-16编码)
        let title: Vec<u16> = "kucoding.com\0".encode_utf16().collect();
        let message: Vec<u16> = "Hello from Rust!\0".encode_utf16().collect();

        // 调用函数
        let result = func(
            std::ptr::null_mut(), // hWnd
            message.as_ptr(),     // lpText
            title.as_ptr(),       // lpCaption
            0x00000040,           // MB_ICONASTERISK
        );

        println!("MessageBox 返回值: {}", result);
    }
}

上面是一个完整的动态库与其内函数定义与加载的示例。

首先是函数定义:

type MessageBoxWFn = unsafe extern "system" fn(
    hWnd: *mut c_void,
    lpText: *const u16,
    lpCaption: *const u16,
    uType: c_uint,
) -> c_uint;

这里的函数定义看起来很长很怪,所以你需要将其分开来看。

首先type关键字的作用是定义别名,这里的意思就是将后面这个函数的定义取一个别名为MessageBoxWFn,所以它并不重要,只是为了好看。

因此真正重要的是后面的:

unsafe extern "system" fn(
    hWnd: *mut c_void,
    lpText: *const u16,
    lpCaption: *const u16,
    uType: c_uint,
) -> c_uint;

任何从外部动态库导入的函数都是不被rust信任的,所以前面需要加上unsafe关键字,代表这是一个不安全的函数。

extern “system”代表这个函数是从系统库导入的,会采用系统默认函数调用约定,如果是想要导出三方C语言库,那么最好使用extern "C",这会告诉编译器采用C的函数调用约定。

至于这个函数的参数,那么你就需要参考其原函数的定义了:

image.png

常见的c语言类型,比如这里的INTUINTvoid,你都可以在use std::ffi下找到相应的rust类型定义。

而宽字节字符串,在rust中的标识则没有官方定义、而是直接使用u16数组:宽字节字符串本质上就是一串u16数字,每个数字映射一个字符。

但直接看官方文档其实并不容易看出来这个类型的本质含义,最推荐的做法是进入vs中写下该函数、进入函数定义、查看每个类型的本质是什么:

image.png

比如看起来很抽象的HWND类型,实际上就是一个8字节大小的指针(指针大小仅与当前程序类型有关,比如x64程序指针就是8位)。

作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux