Rust 2026 经验谈 - FFI 实战:C 调用 Rust

2964 字
15 分钟
Rust 2026 经验谈 - FFI 实战:C 调用 Rust

上一篇我们讨论了 Rust 调用 C——用 bindgen 生成绑定、处理布局和回调。本文反过来:让 C 调用 Rust。这不是”把 Rust 代码抄一遍”那么简单——你需要设计 C 友好的 API、用 cbindgen 导出头文件、管理 opaque 类型的生命周期、把 cargo 构建嵌入 CMake 系统。实战中踩坑极多,本文系统总结。

cbindgen 导出 C 头文件#

为什么需要 cbindgen#

当 Rust 库要被 C 调用时,C 侧需要一个头文件声明导出的函数和类型。手写头文件极易与 Rust 侧脱节——改了 Rust 签名忘了改 .h,编译通过但运行时 UB。cbindgen 从 Rust 源码自动生成 C 头文件,保证同步。

基本用法#

Cargo.toml
[package]
name = "my_lib"
version = "0.1.0"
edition = "2024"
lib = { crate-type = ["cdylib", "staticlib"] }
[build-dependencies]
cbindgen = "0.27"
build.rs
fn main() {
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
cbindgen::Builder::new()
.with_crate(crate_dir)
.with_language(cbindgen::Language::C)
.generate()
.expect("Unable to generate bindings")
.write_to_file("include/my_lib.h");
}
src/lib.rs
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_add(a: i32, b: i32) -> i32 {
a + b
}
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_greet(name: *const i8) {
if name.is_null() {
return;
}
let c_str = unsafe { std::ffi::CStr::from_ptr(name) };
let rust_str = c_str.to_string_lossy();
println!("Hello, {}!", rust_str);
}

cbindgen 生成的头文件:

include/my_lib.h
#ifndef MY_LIB_H
#define MY_LIB_H
#include <stdint.h>
#include <stdbool.h>
int32_t my_lib_add(int32_t a, int32_t b);
void my_lib_greet(const char *name);
#endif /* MY_LIB_H */

cbindgen 配置#

cbindgen 支持 cbindgen.toml 配置文件,放在 crate 根目录:

cbindgen.toml
language = "C"
include_guard = "MY_LIB_H"
autogen_warning = "/* Warning: this file is auto-generated by cbindgen. Do not modify. */"
no_include = true
sys_includes = ["stdint.h", "stdbool.h", "stddef.h"]
includes = ["my_lib_types.h"]
tab_width = 4
style = "both"
[defines]
"feature = logging" = "MY_LIB_LOGGING"
[export]
prefix = "MyLib"
include = ["MyContext", "MyConfig"]
exclude = ["InternalState"]

在 build.rs 中使用配置文件:

fn main() {
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let config = cbindgen::Config::from_file("cbindgen.toml")
.expect("Unable to read cbindgen.toml");
cbindgen::Builder::new()
.with_crate(crate_dir)
.with_config(config)
.generate()
.expect("Unable to generate bindings")
.write_to_file("include/my_lib.h");
}

踩坑:cbindgen 不看条件编译#

// cbindgen 会忽略 #[cfg],总是生成所有符号的声明
#[cfg(feature = "experimental")]
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_experimental() {}
// 生成的 .h 中总是包含 my_lib_experimental 声明
// 但如果 feature 没开,链接时找不到符号——C 侧编译失败
// 变通:用 cbindgen.toml 的 [defines] 或在 build.rs 中动态配置

#[unsafe(no_mangle)] + extern “C” 导出函数约定#

基本规则#

#[unsafe(no_mangle)]
pub extern "C" fn exported_function(x: i32) -> i32 {
x * 2
}
  • #[unsafe(no_mangle)]:阻止 Rust 的 name mangling,让链接器看到 exported_function 而非 _ZN11my_lib17exported_functionE 之类的符号。Rust 2024 Edition 要求把 no_mangle 写成 unsafe attribute,因为开发者必须保证导出符号名不会和其他库冲突。
  • pub extern "C":使用 C 调用约定(cdecl),保证 C 侧能正确调用
  • pub:符号必须对链接器可见

可用类型#

extern "C" 函数的参数和返回值只能是 C 兼容类型:

Rust 类型C 类型说明
i32 / u32int32_t / uint32_t固定宽度
i64 / u64int64_t / uint64_t固定宽度
f32 / f64float / double浮点
*mut T / *const TT* / const T*原始指针
boolbool(C23)注意 ABI 兼容性
c_int, c_longint, long平台相关

不兼容的类型(编译器会报错或产生 UB):

// 错误!String 不是 C 兼容类型
// #[unsafe(no_mangle)]
// pub extern "C" fn bad(s: String) {}
// 错误!&str 不是 C 兼容类型
// #[unsafe(no_mangle)]
// pub extern "C" fn bad2(s: &str) {}
// 错误!泛型在 extern "C" 中不允许
// #[unsafe(no_mangle)]
// pub extern "C" fn bad3<T>(t: T) {}

踩坑:Rust bool vs C bool#

// Rust 的 bool 是 1 字节,值为 0 或 1
// C99 没有 bool,C11 的 _Bool 也是 1 字节
// 但很多 C 代码用 int 表示布尔值——任何非零为 true
// 危险!C 传 int 2 作为 bool,Rust 视为 true 但可能 UB
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_set_enabled(enabled: bool) {
// 如果 C 侧传了 2 作为 bool:
// Rust 保证 bool 必须是 0 或 1
// 传其他值是 UB!
if enabled {
println!("enabled");
}
}
// 安全做法:C 侧用 c_int,Rust 侧转换
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_set_enabled_safe(enabled: std::os::raw::c_int) {
let enabled = enabled != 0;
if enabled {
println!("enabled");
}
}

导出 opaque 类型:Box 透传为 void*#

opaque 模式#

当 C 侧只需持有指针而不需访问内部字段时,用 opaque 类型。Rust 侧用 Box<T> 管理内存,C 侧只看到 void*

pub struct MyContext {
data: Vec<u8>,
config: Config,
connected: bool,
}
pub struct Config {
max_size: usize,
timeout_ms: u64,
}
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_context_create(max_size: usize, timeout_ms: u64) -> *mut MyContext {
let ctx = Box::new(MyContext {
data: Vec::with_capacity(max_size),
config: Config { max_size, timeout_ms },
connected: false,
});
Box::into_raw(ctx)
}
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_context_destroy(ctx: *mut MyContext) {
if !ctx.is_null() {
unsafe { drop(Box::from_raw(ctx)); }
}
}
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_context_connect(ctx: *mut MyContext) -> i32 {
if ctx.is_null() {
return -1;
}
let ctx = unsafe { &mut *ctx };
ctx.connected = true;
0
}
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_context_write(ctx: *mut MyContext, data: *const u8, len: usize) -> i32 {
if ctx.is_null() || data.is_null() {
return -1;
}
let ctx = unsafe { &mut *ctx };
let slice = unsafe { std::slice::from_raw_parts(data, len) };
if slice.len() > ctx.config.max_size {
return -2;
}
ctx.data.extend_from_slice(slice);
0
}

cbindgen 生成的头文件:

typedef struct MyContext MyContext;
MyContext *my_lib_context_create(size_t max_size, uint64_t timeout_ms);
void my_lib_context_destroy(MyContext *ctx);
int32_t my_lib_context_connect(MyContext *ctx);
int32_t my_lib_context_write(MyContext *ctx, const uint8_t *data, size_t len);

C 侧不透明——只知道 MyContext*,无法访问字段。

踩坑:Box::from_raw 必须用同一类型#

struct Inner { x: i32 }
struct Wrapper { inner: Inner, extra: i32 }
#[unsafe(no_mangle)]
pub extern "C" fn create() -> *mut Wrapper {
Box::into_raw(Box::new(Wrapper { inner: Inner { x: 0 }, extra: 0 }))
}
// 错误!用 Inner 释放 Wrapper
// #[unsafe(no_mangle)]
// pub extern "C" fn destroy_bad(ptr: *mut Inner) {
// unsafe { drop(Box::from_raw(ptr)); } // 类型不匹配——UB!
// }
// 正确:类型必须一致
#[unsafe(no_mangle)]
pub extern "C" fn destroy(ptr: *mut Wrapper) {
if !ptr.is_null() {
unsafe { drop(Box::from_raw(ptr)); }
}
}

踩坑:多线程访问 opaque 指针#

use std::sync::Mutex;
pub struct SharedContext {
inner: Mutex<ContextInner>,
}
struct ContextInner {
data: Vec<u8>,
count: u64,
}
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_shared_create() -> *mut SharedContext {
let ctx = Box::new(SharedContext {
inner: Mutex::new(ContextInner {
data: Vec::new(),
count: 0,
}),
});
Box::into_raw(ctx)
}
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_shared_increment(ctx: *mut SharedContext) -> u64 {
if ctx.is_null() {
return 0;
}
let ctx = unsafe { &*ctx };
let mut inner = ctx.inner.lock().unwrap();
inner.count += 1;
inner.count
}

关键&*ctx 获取 &SharedContext(共享引用),Mutex 提供内部可变性。不要用 &mut *ctx——多个 C 线程可能同时调用。

C API 设计模式#

错误码返回#

pub const MY_LIB_OK: i32 = 0;
pub const MY_LIB_ERR_NULL: i32 = -1;
pub const MY_LIB_ERR_OOM: i32 = -2;
pub const MY_LIB_ERR_INVALID: i32 = -3;
pub const MY_LIB_ERR_IO: i32 = -4;
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_process(ctx: *mut MyContext, input: *const u8, input_len: usize) -> i32 {
if ctx.is_null() {
return MY_LIB_ERR_NULL;
}
if input.is_null() && input_len > 0 {
return MY_LIB_ERR_INVALID;
}
let ctx = unsafe { &mut *ctx };
let data = if input.is_null() || input_len == 0 {
&[]
} else {
unsafe { std::slice::from_raw_parts(input, input_len) }
};
match ctx.process(data) {
Ok(()) => MY_LIB_OK,
Err(e) => match e {
MyError::OutOfMemory => MY_LIB_ERR_OOM,
MyError::InvalidInput => MY_LIB_ERR_INVALID,
MyError::Io(_) => MY_LIB_ERR_IO,
},
}
}

错误信息获取#

use std::ffi::CString;
use std::sync::atomic::{AtomicPtr, Ordering};
static LAST_ERROR: AtomicPtr<i8> = AtomicPtr::new(std::ptr::null_mut());
fn set_last_error(msg: &str) {
let c_string = CString::new(msg).unwrap_or_else(|_| CString::new("error contains null byte").unwrap());
let ptr = c_string.into_raw();
let old = LAST_ERROR.swap(ptr, Ordering::SeqCst);
if !old.is_null() {
unsafe { drop(CString::from_raw(old)); }
}
}
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_get_last_error() -> *const i8 {
LAST_ERROR.load(Ordering::SeqCst)
}
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_clear_error() {
let old = LAST_ERROR.swap(std::ptr::null_mut(), Ordering::SeqCst);
if !old.is_null() {
unsafe { drop(CString::from_raw(old)); }
}
}

init / destroy 生命周期#

pub struct MyLib {
contexts: Vec<*mut MyContext>,
}
static mut GLOBAL_INSTANCE: *mut MyLib = std::ptr::null_mut();
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_init() -> i32 {
let lib = Box::new(MyLib {
contexts: Vec::new(),
});
unsafe {
if !GLOBAL_INSTANCE.is_null() {
return -1; // already initialized
}
GLOBAL_INSTANCE = Box::into_raw(lib);
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_destroy() {
unsafe {
if GLOBAL_INSTANCE.is_null() {
return;
}
let lib = Box::from_raw(GLOBAL_INSTANCE);
for &ctx in &lib.contexts {
if !ctx.is_null() {
drop(Box::from_raw(ctx));
}
}
GLOBAL_INSTANCE = std::ptr::null_mut();
}
}

更安全的替代:用 Option<Box<...>>OnceLock

use std::sync::OnceLock;
static GLOBAL: OnceLock<MyLib> = OnceLock::new();
pub struct MyLib {
initialized: bool,
}
#[unsafe(no_mangle)]
pub extern "C" fn my_lib_init() -> i32 {
match GLOBAL.set(MyLib { initialized: true }) {
Ok(()) => 0,
Err(_) => -1,
}
}

输出参数模式#

#[unsafe(no_mangle)]
pub extern "C" fn my_lib_compute(ctx: *mut MyContext, result: *mut i32) -> i32 {
if ctx.is_null() || result.is_null() {
return MY_LIB_ERR_NULL;
}
let ctx = unsafe { &*ctx };
let value = ctx.compute();
unsafe { *result = value; }
MY_LIB_OK
}

C 侧使用:

int32_t result;
int rc = my_lib_compute(ctx, &result);
if (rc != MY_LIB_OK) {
const char *err = my_lib_get_last_error();
fprintf(stderr, "Error: %s\n", err);
}

跨语言构建系统集成:CMake 调用 cargo#

方案一:ExternalProject_Add#

CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(MyApp C)
include(ExternalProject)
set(RUST_LIB_DIR "${CMAKE_BINARY_DIR}/rust-target")
ExternalProject_Add(
my_rust_lib
SOURCE_DIR "${CMAKE_SOURCE_DIR}/rust/"
BUILD_IN_SOURCE 1
CONFIGURE_COMMAND ""
BUILD_COMMAND cargo build --release --target-dir ${RUST_LIB_DIR}
INSTALL_COMMAND ""
)
set(RUST_LIB_PATH "${RUST_LIB_DIR}/release/libmy_lib.a")
add_library(my_lib STATIC IMPORTED)
set_target_properties(my_lib PROPERTIES IMPORTED_LOCATION ${RUST_LIB_PATH})
add_dependencies(my_lib my_rust_lib)
add_executable(my_app main.c)
target_link_libraries(my_app my_lib)
target_include_directories(my_app PRIVATE "${CMAKE_SOURCE_DIR}/rust/include")

方案二:corrosion(推荐)#

corrosion 是专门的 CMake-Cargo 集成工具:

CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(MyApp C)
# Fetch corrosion
FetchContent_Declare(
corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
GIT_TAG v0.5
)
FetchContent_MakeAvailable(corrosion)
# 导入 Rust crate
corrosion_import_crate(
MANIFEST_PATH rust/Cargo.toml
CRATE_TYPES staticlib
)
add_executable(my_app main.c)
target_link_libraries(my_app my_lib)
target_include_directories(my_app PRIVATE "${CMAKE_SOURCE_DIR}/rust/include")

corrosion 的优势:

  • 自动处理 cargo 构建目录
  • 自动链接 Rust 静态库
  • 支持交叉编译
  • 支持 profile 和 feature 传递

方案三:cargo 构建脚本 + CMake 消费#

# 先在 CI 中构建 Rust 库
# cargo build --release -p my_lib
# CMakeLists.txt 只链接产物
cmake_minimum_required(VERSION 3.20)
project(MyApp C)
add_executable(my_app main.c)
# 假设 cargo 产物在 ${RUST_OUTPUT_DIR}
target_link_directories(my_app PRIVATE ${RUST_OUTPUT_DIR})
target_link_libraries(my_app my_lib pthread dl m)
target_include_directories(my_app PRIVATE include)

踩坑:链接 Rust 静态库需要系统库#

Rust 静态库依赖系统库(Linux 上是 pthread、dl、m),漏链会报 undefined reference:

if(UNIX AND NOT APPLE)
target_link_libraries(my_app my_lib pthread dl m)
elseif(APPLE)
target_link_libraries(my_app my_lib dl)
endif()

踩坑:Windows 上的 MSVC vs GNU#

# Rust 默认用 MSVC 工具链(windows-msvc)
# 如果 C 代码用 MinGW 编译——ABI 不兼容!
# 确保两侧使用同一工具链
# 检查 Rust 工具链
# rustup target list --installed
# rustup default stable-x86_64-pc-windows-msvc
# CMake 中强制 MSVC
if(MSVC)
# OK:匹配
else()
message(WARNING "C compiler is not MSVC, may not match Rust toolchain")
endif()

实战:将 Rust 库封装为 C SDK#

项目结构#

my-sdk/
├── Cargo.toml
├── cbindgen.toml
├── build.rs
├── src/
│ └── lib.rs
├── include/
│ └── my_sdk.h # cbindgen 生成
├── examples/
│ └── c/
│ ├── CMakeLists.txt
│ └── main.c
└── tests/
└── c_test.c

Rust 核心#

src/lib.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_int;
pub struct MySdk {
buffer: Vec<u8>,
capacity: usize,
}
#[repr(C)]
pub struct MySdkResult {
pub data: *const u8,
pub len: usize,
pub error_code: c_int,
}
const MY_SDK_OK: c_int = 0;
const MY_SDK_ERR_NULL: c_int = -1;
const MY_SDK_ERR_CAPACITY: c_int = -2;
const MY_SDK_ERR_INVALID: c_int = -3;
#[unsafe(no_mangle)]
pub extern "C" fn my_sdk_create(capacity: usize) -> *mut MySdk {
let sdk = Box::new(MySdk {
buffer: Vec::with_capacity(capacity),
capacity,
});
Box::into_raw(sdk)
}
#[unsafe(no_mangle)]
pub extern "C" fn my_sdk_destroy(sdk: *mut MySdk) {
if !sdk.is_null() {
unsafe { drop(Box::from_raw(sdk)); }
}
}
#[unsafe(no_mangle)]
pub extern "C" fn my_sdk_append(sdk: *mut MySdk, data: *const u8, len: usize) -> c_int {
if sdk.is_null() {
return MY_SDK_ERR_NULL;
}
if data.is_null() && len > 0 {
return MY_SDK_ERR_INVALID;
}
let sdk = unsafe { &mut *sdk };
let slice = if data.is_null() || len == 0 {
&[]
} else {
unsafe { std::slice::from_raw_parts(data, len) }
};
if sdk.buffer.len() + slice.len() > sdk.capacity {
return MY_SDK_ERR_CAPACITY;
}
sdk.buffer.extend_from_slice(slice);
MY_SDK_OK
}
#[unsafe(no_mangle)]
pub extern "C" fn my_sdk_get_result(sdk: *mut MySdk) -> MySdkResult {
if sdk.is_null() {
return MySdkResult {
data: std::ptr::null(),
len: 0,
error_code: MY_SDK_ERR_NULL,
};
}
let sdk = unsafe { &*sdk };
MySdkResult {
data: sdk.buffer.as_ptr(),
len: sdk.buffer.len(),
error_code: MY_SDK_OK,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn my_sdk_clear(sdk: *mut MySdk) -> c_int {
if sdk.is_null() {
return MY_SDK_ERR_NULL;
}
let sdk = unsafe { &mut *sdk };
sdk.buffer.clear();
MY_SDK_OK
}
#[unsafe(no_mangle)]
pub extern "C" fn my_sdk_version() -> *const i8 {
let v = CString::new(env!("CARGO_PKG_VERSION")).unwrap();
v.into_raw()
}
#[unsafe(no_mangle)]
pub extern "C" fn my_sdk_free_string(s: *mut i8) {
if !s.is_null() {
unsafe { drop(CString::from_raw(s)); }
}
}

C 测试#

tests/c_test.c
#include "my_sdk.h"
#include <stdio.h>
#include <assert.h>
#include <string.h>
int main(void) {
// 版本
const char *version = my_sdk_version();
printf("SDK version: %s\n", version);
my_sdk_free_string((char *)version);
// 创建
MySdk *sdk = my_sdk_create(1024);
assert(sdk != NULL);
// 追加数据
const char *hello = "Hello, ";
int rc = my_sdk_append(sdk, (const uint8_t *)hello, strlen(hello));
assert(rc == 0);
const char *world = "World!";
rc = my_sdk_append(sdk, (const uint8_t *)world, strlen(world));
assert(rc == 0);
// 获取结果
MySdkResult result = my_sdk_get_result(sdk);
assert(result.error_code == 0);
printf("Result (%zu bytes): %.*s\n", result.len, (int)result.len, result.data);
// 超容量测试
uint8_t big[2048];
memset(big, 'x', sizeof(big));
rc = my_sdk_append(sdk, big, sizeof(big));
assert(rc == -2); // ERR_CAPACITY
// 清空
rc = my_sdk_clear(sdk);
assert(rc == 0);
// 销毁
my_sdk_destroy(sdk);
printf("All tests passed!\n");
return 0;
}

CI 构建#

.github/workflows/c-sdk.yml
name: C SDK
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Build Rust library
run: cargo build --release
- name: Build C test
run: |
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build .
- name: Run C test
run: ./build/c_test

踩坑总结#

1. panic 不能跨越 FFI 边界#

#[unsafe(no_mangle)]
pub extern "C" fn my_sdk_risky_operation(sdk: *mut MySdk) -> c_int {
// 如果内部 panic,unwind 到 C 栈帧 = UB!
// 必须用 catch_unwind 包裹
std::panic::catch_unwind(|| {
let sdk = unsafe { &mut *sdk };
sdk.risky_op()
}).unwrap_or_else(|_| {
eprintln!("Rust panicked in FFI");
-999
})
}

2. 导出函数不能返回 Rust 分配的堆内存而不提供释放函数#

// 错误!C 侧 free() 释放 Rust 分配的内存——分配器不匹配
// #[unsafe(no_mangle)]
// pub extern "C" fn bad() -> *mut u8 {
// let v = vec![0u8; 100];
// Box::into_raw(v.into_boxed_slice()) as *mut u8
// }
// 正确:提供配对的释放函数
#[unsafe(no_mangle)]
pub extern "C" fn my_sdk_alloc(size: usize) -> *mut u8 {
let mut v = Vec::with_capacity(size);
let ptr = v.as_mut_ptr();
std::mem::forget(v);
ptr
}
#[unsafe(no_mangle)]
pub extern "C" fn my_sdk_free(ptr: *mut u8, size: usize) {
if !ptr.is_null() {
unsafe { drop(Vec::from_raw_parts(ptr, 0, size)); }
}
}

3. cdylib vs staticlib#

  • cdylib:生成动态链接库(.so/.dll/.dylib),只导出 #[unsafe(no_mangle)] pub extern "C" 的符号,适合作为 C SDK
  • staticlib:生成静态库(.a/.lib),包含所有符号包括 Rust 标准库,适合静态链接
  • lib(默认):Rust crate 格式,不能被 C 直接链接
[lib]
crate-type = ["cdylib"] # 只生成动态库
# crate-type = ["staticlib"] # 只生成静态库
# crate-type = ["cdylib", "staticlib"] # 两种都生成

4. C 结构体中包含 Rust 分配的指针#

#[repr(C)]
pub struct MySdkConfig {
pub max_connections: u32,
pub endpoint: *mut i8, // Rust CString
}
// C 侧设置 endpoint 后,必须用 my_sdk_free_string 释放
// 不能用 C 的 free()!

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
Rust 2026 经验谈 - FFI 实战:C 调用 Rust
https://tinyzzh.github.io/posts/rust-2026/2026-07-01-rust_2026_031_ffi_c_call_rust/
作者
TinyZ Zzh
发布于
2026-07-01
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
TinyZ Zzh
专注于高并发服务器、网络游戏相关(Java、PHP、Unity3D、Unreal Engine等)技术,热爱游戏事业, 正在努力实现自我价值当中。
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
245
分类
38
标签
288
总字数
407,675
运行时长
0
最后活动
0 天前

文章目录