相关:C语言调用Rust
¶ 场景
在C++中有一个类想用Rust的std::collections::BinaryHeap
维护:
class A {
public:
(const char* data, size_t size) : data_(data), size_(size) {}
Aconst char* data() const { return data_; }
size_t size() const { return size_; }
bool operator < (const A& rhs) const {
if (size() < rhs.size()) {
return true;
}
if (size() > rhs.size()) {
return false;
}
return memcmp(data(), rhs.data(), size()) < 0;
}
private:
const char* data_;
size_t size_;
};
并且提供这些接口:
extern void* Create();
extern void Push(void* heap, A a);
extern A Pop(void* heap);
extern void Destroy(void* heap);
¶ 思路
Rust只能提供C语言的接口,结构体也只认C语言的结构体。
因此首先专门另写一份C++代码,调用Rust提供的C接口实现所需的C++接口,并且将C++提供的操作封装成C语言接口给Rust调用。
¶ 代码
¶ 结构
├── heap
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── cpp
│ │ ├── cpp_to_rust.cpp
│ │ ├── c_struct.h
│ │ └── rust_to_cpp.cpp
│ ├── include
│ │ └── heap.h
│ ├── Makefile
│ ├── src
│ │ └── lib.rs
│ └── target
├── test.cpp
└── test.h
¶ C++使用端
test.h
:
#ifndef TEST_H_
#define TEST_H_
#include <cstddef>
#include <cstring>
class A {
public:
(const char* data, size_t size) : data_(data), size_(size) {}
Aconst char* data() const { return data_; }
size_t size() const { return size_; }
bool operator < (const A& rhs) const {
if (size() < rhs.size()) {
return true;
}
if (size() > rhs.size()) {
return false;
}
return memcmp(data(), rhs.data(), size()) < 0;
}
private:
const char* data_;
size_t size_;
};
#endif // TEST_H_
test.cpp
:
#include <cstdio>
#include <cstring>
#include "test.h"
#include "heap.h"
using namespace std;
int main() {
void* heap = Create();
(heap, A("233", 3));
Push(heap, A("2333", 4));
Push(heap, A("332", 3));
Push(Pop(heap).data());
puts(Pop(heap).data());
puts(Pop(heap).data());
puts(heap);
Destroy
return 0;
}
¶ 新建Rust项目
cargo new heap --lib
在heap/Cargo.toml
中加入:
[lib]
crate-type = ["staticlib"]
heap/src/lib.rs
:
use std::collections::BinaryHeap;
use std::cmp::Ordering;
use std::os::raw::c_char;
extern "C" {
fn compare(a: *const C_A, b: *const C_A) -> bool;
}
#[repr(C)]
#[derive(Eq)]
pub struct C_A {
: *const c_char,
data: usize,
size}
impl Ord for C_A {
fn cmp(&self, rhs: &C_A) -> Ordering {
unsafe {
let a: *const C_A = self;
let b: *const C_A = rhs;
if compare(a, b) {
Ordering::Less
} else if compare(b, a) {
Ordering::Greater
} else {
Ordering::Equal
}
}
}
}
impl PartialOrd for C_A {
fn partial_cmp(&self, rhs: &C_A) -> Option<Ordering> {
Some(self.cmp(rhs))
}
}
impl PartialEq for C_A {
fn eq(&self, rhs: &C_A) -> bool {
self.cmp(rhs) == Ordering::Equal
}
}
#[no_mangle]
pub extern "C" fn __rust_create() -> *mut BinaryHeap<C_A> {
Box::into_raw(Box::new(BinaryHeap::<C_A>::new()))
}
#[no_mangle]
pub extern "C" fn __rust_push(heap: *mut BinaryHeap<C_A>, a: C_A) {
let heap = unsafe { &mut *heap };
.push(a);
heap}
#[no_mangle]
pub extern "C" fn __rust_pop(heap: *mut BinaryHeap<C_A>) -> C_A {
let heap = unsafe { &mut *heap };
// Use unwrap_unchecked to avoid panic. C++ does not know how to panic anyway.
unsafe { heap.pop().unwrap_unchecked() }
}
#[no_mangle]
pub extern "C" fn __rust_destroy(heap: *mut BinaryHeap<C_A>) {
unsafe { Box::from_raw(heap) };
}
#[repr(C)]
:
repr的意思是representation。repr(C)
表示用C语言的方式来组织这个结构体,这样这个结构体就可以在C语言和Rust之间传递了。
参考:https://stackoverflow.com/questions/24105186/can-i-call-c-or-c-functions-from-rust-code
¶ C语言结构体
Rust只兼容C语言结构体,所以要先把C++结构体变成C语言结构体,才能传给Rust用。同样,Rust传出来的结构体也只能是C语言结构体,要转成对应的C++结构体。
heap/cpp/c_struct.h
:
#ifndef C_STRUCT_H_
#define c_STRUCT_H_
#include <cstddef>
extern "C" {
struct CA {
const char* data;
size_t size;
};
}
#endif // C_STRUCT_H_
这里的struct CA
就是Rust里的struct C_A
。这里特定用了不同的名字,说明两者名字不一定要相同。
参考:https://stackoverflow.com/questions/62126501/how-to-pass-a-c-struct-to-rust
¶ 将C++操作封装成C语言接口
BinaryHeap
需要用到比较操作。但是A
的比较操作是operator <
,Rust显然不能直接用。因此需要将这个操作封装成C语言结构,Rust才能用。
heap/cpp/cpp_to_rust.cpp
:
#include "test.h"
#include "c_struct.h"
extern "C" {
bool compare(const CA* ca, const CA* cb);
}
bool compare(const CA* ca, const CA* cb) {
(ca->data, ca->size);
A a(cb->data, cb->size);
A breturn a < b;
}
¶ 使用Rust接口实现所需的C++接口
heap/cpp/rust_to_cpp.cpp
:
#include "test.h"
#include "c_struct.h"
#include <cstddef>
extern "C" {
extern void* __rust_create();
extern void __rust_push(void* heap, CA a);
extern CA __rust_pop(void* heap);
extern void __rust_destroy(void* heap);
}
void* Create() {
return __rust_create();
}
void Push(void* heap, A a) {
struct CA ca{
.data = a.data(),
.size = a.size(),
};
(heap, ca);
__rust_push}
(void* heap) {
A Pop= __rust_pop(heap);
CA ca return A(ca.data, ca.size);
}
void Destroy(void* heap) {
(heap);
__rust_destroy}
¶ 将实现的C++接口放进头文件
include/heap.h
:
#ifndef HEAP_H_
#define HEAP_H_
#include "test.h"
extern void* Create();
extern void Push(void* heap, A a);
extern A Pop(void* heap);
extern void Destroy(void* heap);
#endif // HEAP_H_
¶ Makefile
heap/Makefile
:
ifndef INCLUDE_DIR
# 这里不能缩进。
$(error INCLUDE_DIR is undefined)
endif
target/cpp_to_rust.o: cpp/cpp_to_rust.cpp cpp/c_struct.h $(INCLUDE_DIR)/test.h
$(INCLUDE_DIR) cpp/cpp_to_rust.cpp -c -o $@
g++ -I
target/rust_to_cpp.o: cpp/rust_to_cpp.cpp cpp/c_struct.h $(INCLUDE_DIR)/test.h
$(INCLUDE_DIR) cpp/rust_to_cpp.cpp -c -o $@
g++ -fPIC -I
target/debug/libheap.a: $(shell find src -type f) Cargo.toml Cargo.lock
cargo build
target/debug/libheap.o: target/cpp_to_rust.o target/rust_to_cpp.o target/debug/libheap.a
$@ --whole-archive target/rust_to_cpp.o --no-whole-archive target/cpp_to_rust.o target/debug/libheap.a
ld -r -o
obj_debug: target/debug/libheap.o
target/debug/libheap.so: obj_debug
$^ -ldl -o $@
gcc -shared
shared_lib_debug: target/debug/libheap.so
.PHONY: debug shared_lib_debug
其中ld
的-r
表示relocatable
。按照我的理解,由于要将多个.o
文件合并起来,所以不可避免地要进行relocation
。
--whole-archive
:将后面的文件里的所有符号都加入到目标文件中。--no-whole-archive
是取消前面的--whole-archive
的影响,使得后面的文件里只有用到了的符号才加入到目标文件。这里rust_to_cpp.o
里存储了需要给使用端用的C++接口,因此里面的符号应该全部加入到目标文件中。而其他的选择性加入即可。
然后make obj_debug INCLUDE_DIR=..
即可编译出target/debug/libheap.o
,里面有我们需要的C++接口。make shared_lib_debug INCLUDE_DIR=..
即可编译出动态库target/debug/libheap.so
。
参考:
https://stackoverflow.com/questions/3821916/how-to-merge-two-ar-static-libraries-into-one
https://stackoverflow.com/questions/29391965/what-is-partial-linking-in-gnu-linker
https://stackoverflow.com/questions/4728810/how-to-ensure-makefile-variable-is-set-as-a-prerequisite
缩进后的语句一律视为编译目标的一部分,作为shell语句解释。
:https://anclark.github.io/2021/02/09/Programming_Tips/Makefile_%E8%B8%A9%E5%9D%91%E8%AE%B0/
https://stackoverflow.com/questions/2826029/passing-additional-variables-from-command-line-to-make
-ldl
: [编译错误undefined reference to
dlsym'](https://blog.csdn.net/shareyao/article/details/5362642)。不过这里好像不加
-ldl`也不会报这个错误。
¶ 使用
用obj文件:
gcc -I heap/include/ -I. heap/target/debug/libheap.o test.cpp -o test
./test
用动态库:
gcc -I heap/include -I. -L heap/target/debug/ -lheap test.cpp -o test
LD_LIBRARY_PATH=heap/target/debug/ ./test
输出:
2333
332
233
¶ 注意事项
由于Rust编译出来的目标文件里会自带很多符号,因此本博客提供的方法无法实现C++代码调用多个Rust工程提供的接口,需要将所有Rust接口的实现都塞进一个Rust工程。暂时不清楚解决方案。