1.前言
由于rust诞生时间太短,目前生态不够完善,因此大量的功能库都需要依赖于C、C++语言的历史积累。
而本文将要介绍的便是如何实现rust与c乃至c++之间实现互相调用。
2.动态库调用
首先最方便的还是动态库形式,大量的C语言代码库可以被编译为平台动态库、导出相应的函数,rust直接加载对应的动态库完成调用,这也是目前使用最广的方式。
比如rust中的windows、fltk-rs等等,由于它们之前已经用C语言实现了大量的代码、十几年、乃至几十年的积淀,短时间内难以用rust重写一遍。
所以为了能让rust可以使用它们,常常就是采用动态库调用的方式。
比如在windows系统上,一个最简单常用的winapi:MessagBoxW
,该函数用于弹出一个窗口。
此时如果我们想要在rust代码中调用这个函数(在没有官方提供的windows crate前提下),那么就可以先去官方文档看看它所在位置MessageBoxW:
可以看到,官方文档中注明了它在User32.dll
这个动态库中。
然后你可以下载everything等工具全局搜索这个动态库在哪里,找到它的路径(一般默认在C:\Windows\System32\user32.dll
),然后通过一些工具查看它的导出函数,就可以看到它。
比如我用vs本身提供的一个叫做dumpbin的工具,查看这个动态库的所有导出函数:
dumpbin /EXPORTS C:\Windows\System32\user32.dll
注意如果你想要执行这条命令,你需要进入vs自带的那个控制台中才能找到dumpbin这个命令。
然后搜索一下你就会发现它确实在这个动态库中:
然后我们就可以来到下一步,在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的函数调用约定。
至于这个函数的参数,那么你就需要参考其原函数的定义了:
常见的c语言类型,比如这里的INT
、UINT
、void
,你都可以在use std::ffi
下找到相应的rust类型定义。
而宽字节字符串,在rust中的标识则没有官方定义、而是直接使用u16数组:宽字节字符串本质上就是一串u16数字,每个数字映射一个字符。
但直接看官方文档其实并不容易看出来这个类型的本质含义,最推荐的做法是进入vs中写下该函数、进入函数定义、查看每个类型的本质是什么:
比如看起来很抽象的HWND
类型,实际上就是一个8字节大小的指针(指针大小仅与当前程序类型有关,比如x64程序指针就是8位)。