基础概念铺垫
1. 链接库是什么?
写代码时很多通用功能(加密、网络、数学计算)不用每次重写,把一堆函数、变量、类打包成独立二进制文件 ,这个文件就是链接库 。
程序编译时分两步:
- 编译 :源代码 → 机器码
目标文件.o / .obj - 链接 :把
目标文件+ 依赖库 合并成最终可执行程序
举栗子:
C语言直观演示
- 业务源码(两份业务代码)
main.c(程序入口)
c
#include "calc.h"
int main() {
int res = add(10, 20);
return res;
}
calc.c(业务工具函数)
c
int add(int a, int b) {
return a + b;
}
第一步:编译(源码 → 业务目标文件 .o)
bash
# 两份业务代码,分别编译成 .o
gcc -c main.c -o main.o
gcc -c calc.c -o calc.o
现在 main.o + calc.o = 完整业务逻辑的机器码。
第二步:链接(链接器合并 业务.o + 系统标准库libc)
bash
# 输入:main.o、calc.o(业务目标文件) + 默认链接系统C标准库shturl.
gcc main.o calc.o -o app
链接器做的事:
- 读取
main.o、calc.o里的业务机器码; - 去系统C标准库
libc补全printf、exit这类系统函数地址; - 把所有代码、数据、符号表整合,生成能直接运行的
app可执行程序。
如果引入 第三方 静态库
假设我们有自己封装的公共库 libmath.a,链接命令变成:
bash
gcc main.o calc.o -L. -lmath -o app
输入依旧是:业务.o文件 + 第三方库 + 系统标准库。
2. 两大库:静态库 vs 动态库
| 类型 | 核心原理 | 优缺点 | 系统后缀区分 |
|---|---|---|---|
| 静态库 Static Lib | 编译链接时,把库完整复制粘贴进最终程序 | 优点:运行不依赖外部文件,分发简单;缺点:程序体积巨大,更新库必须重新编译整个程序 | Windows:.lib Linux:.a MacOS:.a |
| 动态库 Shared Lib | 编译只记录引用,运行时操作系统加载库文件,多个程序共享同一份库 | 优点:程序体积小,单独替换库文件就能更新逻辑;缺点:分发必须附带对应库,缺失会直接崩溃 | Windows:.dll Linux:.so MacOS:.dylib |
3. 链接库 是不是专为 C/C++ 诞生?
是的,根源就是C语言
- 早年 汇编时 代没有
库概念 ,
C语言诞生, 后为了代码复用、模块化,设计了 编译 + 链接 模型,
静态库.a直接沿用Unix系统设计; - C++ 完全兼容C链接模型,扩展了类、重载,动态库引入符号导出机制;
- 后面所有 高级语言 (Rust/Go/Python/Java)的 库 交互,底层全部复用操作系统提供的C ABI二进制标准 ,也就是说:所有语言
跨语言调用库,本质都是走C兼容接口。
ABI (Application Bin Interface)
关键知识点:ABI ( 二进制应用接口 )
不同语言内存布局、函数调用规则不一样,但 C语言 ABI 是 全操作系统 统一标准。
所以 任何语言 想对外提供库、调用外部库,都必须封装一层 C兼容接口,不能直接用语言自身特色语法(Rust所有权、Go协程、Python对象都不能跨库传递)。
两个核心场景
每个语言分两块:
- 【场景A】本语言打包生成静态库/动态库(给其他语言调用)
- 【场景B】本语言调用外部C/C++静态/动态库
附带可直接运行的完整示例代码,环境:Linux(最通用,Windows/Mac标注差异)
前置:写一个基础C测试库(所有语言都会调用它)
创建 mylib.c(通用底层库)
c
#include <stdio.h>
// C兼容导出函数,计算两数相加
int add(int a, int b) {
return a + b;
}
// 打印字符串
void hello(const char* msg) {
printf("C lib print: %s\n", msg);
}
编译C 静态库 .a
bash
# 1. 编译 目标文件
gcc -c mylib.c -o mylib.o
# 2. 打包 静态库
ar rcs libmylib.a mylib.o
# 产物:libmylib.a 静态库
ar= archive 归档工具,是 Unix/Linux 自带老牌工具,本质是一个二进制压缩打包工具,专门用来把一堆 .o 目标文件打包成静态库 .a。.a 文件内部就是一堆 .o 的集合,只是套了一层归档索引,方便链接器快速查找函数符号。
编译C 动态库 .so
bash
# -fPIC 生成位置无关代码(动态库必须)
gcc -shared -fPIC mylib.c -o libmylib.so
# 产物:libmylib.so 动态库
第一部分:Rust 操作 静态/动态库
Rust 完全兼容 C ABI,支持:导出C库、调用C库
1. Rust 打包生成 动态库 / 静态库(给Python/Go/C调用)
步骤1:新建rust项目
bash
cargo new rust_lib && cd rust_lib
步骤2:修改 Cargo.toml 配置输出库
toml
[lib]
# 同时编译 静态库 + 动态库
crate-type = ["cdylib", "staticlib"]
# cdylib:C兼容动态库(输出.so/.dll/.dylib)
# staticlib:C兼容静态库(输出.a/.lib)
步骤3:src/lib.rs 代码(必须 extern "C" 导出C接口)
rust
// extern "C" 强制使用C调用ABI,跨语言必备
#[no_mangle] // 关闭Rust名字混淆,函数名和C一致
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn rust_hello(msg: *const u8) {
// 裸指针转rust字符串(unsafe操作跨语言裸指针)
unsafe {
let s = std::ffi::CStr::from_ptr(msg as *const i8);
println!("Rust lib output: {}", s.to_str().unwrap());
}
}
步骤4:编译
bash
cargo build --release
# target/release/产物
# Linux/Mac:
# librust_lib.so 动态库
# librust_lib.a 静态库
# Windows:rust_lib.dll + rust_lib.lib
2. Rust 调用 外部 C 静态库/动态库(上面的 libmylib)
前置准备(统一环境 Linux x86_64)
- 先编译出C动态库
libmylib.so,放在项目根目录
bash
# mylib.c
#include <stdio.h>
int add(int a, int b) { return a + b; }
void hello(const char* msg) { printf("C lib: %s\n", msg); }
编译生成so:
bash
gcc -shared -fPIC mylib.c -o libmylib.so
- 新建rust项目
bash
cargo new rust_call_c && cd rust_call_c
# 把 libmylib.so 复制到 rust_call_c 项目根目录
项目目录结构(两套方案通用)
rust_call_c/
├── libmylib.so # 我们自己的C动态库
├── Cargo.toml
├── src/
│ └── main.rs # Rust调用代码
└── build.rs # 方案1专用构建脚本
方案1:build.rs 构建脚本(工业标准,推荐,功能最强)
前置知识
基本介绍
-
build.rs 不是普通业务代码,是Cargo的构建通信子进程 指定的 文件。
build.rs 是固定专属文件名,不能修改,位置也有严格规则,必须在 项目根目录下, 和 Cargo.toml 同级。
-
Cargo 设计时约定:项目根目录 build.rs 作为默认 构建 前置 脚本。
-
Cargo 会单独编译 build.rs 为临时可执行文件,在编译你的项目源码之前运行,它和 src/main.rs 是完全隔离的两套代码,互不共享作用域。
-
一个包(项目)只能有一个构建脚本:要么根目录默认 build.rs,要么 Cargo.toml build= 指定的单个 rs文件,不能同时存在多个构建脚本。
-
阻塞执行: 如果 build.rs 中写一个 1 个小时的定时器, 那么 build.rs 文件的代码逻辑 执行不完, cargo run 后续的执行逻辑 需要一直等待
文本协议通信
Cargo 和 build.rs 之间没有复杂函数/结构体交互,约定了一套纯文本规则:
- 通信通道:子进程标准输出
stdout - 指令标识:
行开头必须是cargo: - 格式规范:
cargo:指令名=参数
rs
// build.rs
fn main() {
// 带 cargo: 前缀 → Cargo 捕获并解析为构建指令
println!("cargo:rustc-link-search=.");
println!("cargo:rustc-link-lib=mylib");
// 不带前缀 → 只是普通日志,Cargo忽略,仅打印到控制台
println!("正在配置C库链接参数");
}
build.rs 不需要引入任何特殊库、调用任何特殊函数,只需要向控制台打印 约定格式的字符串,就能和Cargo通信。
每次都执行吗?
一句话总结:
默认不会每次 cargo run 都执行 build.rs;只有源码、监听文件、编译环境变动,或清理缓存后,才会前置运行;无变动时直接复用缓存跳过。
-
只要满足下面 任意一条 ,执行
cargo run/build时就会完整运行 build.rs: -
下面的 全部条件 同时满足 才会跳过 不执行 build.rs, Cargo 直接复用上次 build.rs 输出的编译参数:
整体流程
- 你执行
cargo build / cargo run - Cargo 自动检测项目根目录存在
build.rs - Cargo 单独启动一个子进程,编译、运行这个
build.rs - Cargo 全程实时监听这个子进程的标准输出 stdout (就是
println!打印出来的文字) - 只要输出行匹配固定前缀
cargo:,Cargo 就解析这行指令,转化成编译参数传给rustc - 普通无
cargo:前缀的打印,只会作为日志输出,不会参与编译配置
步骤1:在项目根目录创建 build.rs
文件名必须固定 build.rs,Cargo会自动执行这个文件,专门用来处理编译链接前置逻辑。
rust
// build.rs
fn main() {
// 指令1:告诉rustc/链接器去哪里找库文件
// -L . 等价 gcc -L . :当前项目根目录搜索库
println!("cargo:rustc-link-search=.");
// 指令2:指定要链接的库名
// -lmylib 等价 gcc -lmylib :自动匹配 libmylib.so / libmylib.a
println!("cargo:rustc-link-lib=mylib");
// 指令3: 设置rpath,程序运行时自动在自身目录查找so库
println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN");
// 可选扩展:如果库不在根目录,比如 ./lib 文件夹
// println!("cargo:rustc-link-search=./lib");
}
逐行解释 build.rs 语法规则
所有 println!("cargo:xxx=yyy") 是Cargo内置特殊指令:
cargo:rustc-link-search=路径
作用:传递-L参数给链接器,告诉链接器「去这个文件夹找 .so/.a 库文件」cargo:rustc-link-lib=库名
作用:传递-l参数给链接器,链接libxxx.so只需要写xxx,不用加lib和后缀cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN
作用: 是给底层 ld 链接器传递自定义参数
免除了 在 命令行export LD_LIBRARY_PATH=.的这个命令的输入
步骤2:编写 src/main.rs 调用C库代码
rust
// src/main.rs
use std::ffi::{c_int, c_char};
// 声明外部C ABI函数,和C代码签名严格对应
extern "C" {
// C: int add(int a, int b);
fn add(a: c_int, b: c_int) -> c_int;
// C: void hello(const char* msg);
fn hello(msg: *const c_char);
}
fn main() {
// 跨语言FFI调用必须包裹unsafe块(裸指针、外部函数不安全)
unsafe {
let a: c_int = 10;
let b: c_int = 20;
let sum = add(a, b);
println!("调用C add(10,20) = {}", sum);
// C字符串必须以\0结尾,转成*const c_char指针
let msg = b"Hello Rust Call C\x00".as_ptr() as *const c_char;
hello(msg);
}
}
步骤3:运行、编译、打包完整命令
3.1 开发调试运行
Linux动态库特性:运行时操作系统需要找到 libmylib.so,只要 libmylib.so 在同一个文件夹,直接 就能跑
bash
# cargo自动执行build.rs,完成链接后运行程序
cargo run
输出结果:
调用C add(10,20) = 30
C lib: Hello Rust Call C
3.2 打包发布二进制(release正式产物)
bash
# 编译优化版发布程序
cargo build --release
# 产物路径 target/release/rust_call_c
发布分发注意(动态库依赖坑)
生成的 rust_call_c 程序只是记录了依赖 libmylib.so,不会把库打包进程序。
分发时必须同步附带 libmylib.so ,运行前依旧要设置 LD_LIBRARY_PATH=.,否则报错找不到共享库。
build.rs 额外扩展能力(生产常用)
- 库放在子文件夹
./libs
rust
println!("cargo:rustc-link-search=./libs");
- 强制静态链接 libmylib.a(如果同时存在.a和.so)
rust
println!("cargo:rustc-link-lib=static=mylib");
- 打印调试日志,看cargo执行过程
rust
println!("cargo:warning=正在链接本地libmylib.so");
方案2:仅 Cargo.toml 配置(极简场景,功能有限)
步骤1:删除/移除 build.rs,只用Cargo.toml配置链接规则
修改 Cargo.toml,新增 [links] 段配置
toml
[package]
name = "rust_call_c"
version = "0.1.0"
edition = "2021"
# 专门配置外部C库链接
[links.mylib]
# 等价 build.rs 的 cargo:rustc-link-search=.
search-path = ["."]
# 可选:如果需要额外链接参数
# args = ["-ldl"]
配置逐行解释
[links.mylib]
mylib= 库名,对应libmylib.so,和-lmylib完全对应search-path = ["."]
数组格式,可以填多个搜索目录,等价多个-L参数
search-path = [".", "./libs"]同时搜索根目录和libs文件夹
步骤2:src/main.rs 代码完全不变,和方案1通用
rust
use std::ffi::{c_int, c_char};
extern "C" {
fn add(a: c_int, b: c_int) -> c_int;
fn hello(msg: *const c_char);
}
fn main() {
unsafe {
let sum = add(10, 20);
println!("结果: {}", sum);
hello(b"Test Cargo.toml\x00".as_ptr() as *const c_char);
}
}
步骤3:运行、打包命令和方案1完全一致
Cargo.toml 的 links 所有配置:只影响编译阶段,传递 -L、-l、链接参数给 rustc/ld;
LD_LIBRARY_PATH:程序运行阶段由操作系统动态加载器读取,属于运行时环境变量,和编译配置完全无关;
Cargo.toml 没有任何配置项可以把环境变量永久写入。
所以要手动写一下
bash
export LD_LIBRARY_PATH=.
cargo run
# 发布打包
cargo build --release
两套方案核心对比
| 维度 | 方案1 build.rs | 方案2 Cargo.toml links |
|---|---|---|
| 适用场景 | 正式项目、跨平台、复杂库依赖 | 小型Demo、单一平台、无额外逻辑 |
| 自定义逻辑 | 支持if分支、文件复制、打印警告、调用外部脚本 | 仅静态配置路径,无运行时逻辑 |
| 静态/动态切换 | 支持 static= 强制静态链接 |
无法区分,链接器自动选 |
| 可读性 | 完整代码流程,链接逻辑集中 | 配置分散,复杂依赖难维护 |
LD_LIBRARY_PATH 作用(运行阶段,和编译无关)
- 编译阶段:
-L只是告诉链接器去哪里找库的符号信息 - 运行阶段:操作系统动态加载器需要找到真实
libmylib.so文件
export LD_LIBRARY_PATH=.= 临时告诉系统:当前目录优先搜索动态库
打包后分发两种解决方案(解决找不到so问题)
-
方案A:配套分发so文件(最简单)
发布包结构:dist/
├── rust_call_c # release二进制
└── libmylib.so # 依赖动态库
运行脚本 run.sh
bash
#!/bin/bash
export LD_LIBRARY_PATH=$(dirname "$0")
./rust_call_c
- 方案B:编译时写入rpath(永久固化库路径,不用手动export)
修改 build.rs,嵌入rpath,把库目录写进程序二进制内部
rust
fn main() {
println!("cargo:rustc-link-search=.");
println!("cargo:rustc-link-lib=mylib");
// rpath=$ORIGIN:程序运行时,在自身所在目录寻找so
println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN");
}
重新 cargo build --release,打包后直接运行,不需要手动设置LD_LIBRARY_PATH。
第二部分:Go 操作静态/动态库
Go 通过 cgo 实现和C库互通,也能导出C兼容动态库
1. Go 打包生成 动态库(给Python/Rust/C调用)
新建 go_lib.go
go
package main
import "C"
import "fmt"
//export go_add
func go_add(a, b C.int) C.int {
return a + b
}
//export go_hello
func go_hello(msg *C.char) {
fmt.Printf("Go lib print: %s\n", C.GoString(msg))
}
func main() {} // 导出库必须空main函数
编译生成 动态库 .so(Linux)
bash
# -buildmode=c-shared 生成C兼容动态库
go build -o libgolib.so -buildmode=c-shared go_lib.go
# 输出 libgolib.so 动态库
生成 静态库(极少用,Go静态库兼容性差,一般推荐动态库)
bash
go build -o libgolib.a -buildmode=c-archive go_lib.go
-
静态库会把Go整套运行机制塞进你的程序里
Go的协程、垃圾回收、内存管理是一整套独立系统。静态链接时,这套东西会直接合并到Rust/C主程序。如果你链接2个Go静态库,程序里会出现两套独立的垃圾回收、内存管理器,互相打架,直接崩溃。
动态库
.so是独立文件,每个库单独一套运行环境,互不干扰。 -
静态Go库会抢主程序的系统控制权
静态打包后,Go会霸占程序的内存分配、信号报错处理逻辑,和Rust/C本身的内存、报错机制冲突,经常出现内存错乱、程序卡死;
动态库只是运行时临时加载,不会篡改主程序底层全局逻辑。
-
静态库对编译环境要求极度苛刻
想用Go静态库,你的Rust/C编译工具、系统底层库、Go版本必须完全一致,换台机器、升级Go就大概率链接失败;
动态库只需要运行时存在对应
.so文件,编译阶段无强制绑定,随便分发。 -
官方定位:
c-archive静态库只是备用试验功能,官方推荐跨语言交互一律用c-shared动态库。
2. Go 调用外部C静态/动态库(libmylib)
新建 call_c.go,cgo通过 注释 写 C头文件 逻辑
go
package main
/*
#cgo LDFLAGS: -L. -lmylib // 链接libmylib库
#include "mylib.h" // 自建头文件声明add、hello
*/
import "C"
import "unsafe"
func main() {
res := C.add(3, 7)
println("Go调用C库 add(3,7) =", res)
msg := C.CString("Hello Go call C")
defer C.free(unsafe.Pointer(msg)) // 释放C字符串内存
C.hello(msg)
}
配套 mylib.h 头文件
c
int add(int a, int b);
void hello(const char* msg);
运行:
bash
export LD_LIBRARY_PATH=.
go run call_c.go
cgo通过 注释 写 C头文件 介绍
package main
/*
#cgo LDFLAGS: -L. -lmylib // 链接libmylib库
#include "mylib.h" // 自建头文件声明add、hello
*/
import "C"
......
这不是 普通注释,cgo 专用指令注释,能真实生效
1. 为什么长得像注释却能执行?
Go 的 cgo 有特殊规则:
import "C" 上方 紧邻的 多行注释块,会被 Go 编译器单独解析,交给内置的 C预处理器处理,不属于Go代码注释范畴 。
普通 //、/* */ 注释在Go里会被直接忽略,但紧贴 import "C" 的注释块是cgo专属配置区。
2. 两行指令分别干什么(大白话)
c
#cgo LDFLAGS: -L. -lmylib
#include "mylib.h"
① #cgo LDFLAGS: -L. -lmylib
#cgo:标识这是给cgo的配置指令;LDFLAGS:给底层C链接器传递参数;-L.:告诉链接器,当前目录找库文件;-lmylib:链接libmylib.so/libmylib.a;
等价Rust build.rs里println!("cargo:rustc-link-search=."); println!("cargo:rustc-link-lib=mylib");
② #include "mylib.h"
标准C头文件引入语法,作用是读取头文件,告诉cgo add、hello 两个C函数的签名,否则Go不知道这两个函数入参、返回值类型,编译报错。
3. 关键限制:位置不能乱
- 必须紧贴
import "C",中间不能有空行、不能有Go代码隔开; - 只能写在文件最顶部、
package main之后、import "C"之前; - 如果挪到别的地方,就变成普通注释,完全失效。
失效示例(中间空一行,指令作废)
go
package main
/*
#cgo LDFLAGS: -L. -lmylib
#include "mylib.h"
*/
// 空一行隔开,直接失效
import "C"
真正无作用的普通注释
如果是下面这种,就纯文本、完全没用:
go
package main
// 普通单行注释,随便写,cgo不会解析
// #cgo LDFLAGS: -L. -lmylib
import "C"
单行 // 注释不被cgo解析,只有紧贴import "C"的多行/* */块 内的 #cgo、#include 才会生效。
第三部分:Python 操作静态/动态库
Python 不能生成静态库 (Python解释器机制决定),只能生成动态库;
同时Python调用外部库只用动态库 (不支持直接链接静态库),核心工具:ctypes 标准库。
而且 Python是解释型,没有原生ABI。
1. Python 打包生成 动态库(两种方式)
使用 Cython(Python转C,编译成.so/.dll,最常用)
1)安装cython
bash
pip install cython
2)创建 py_lib.pyx
cython
cpdef int py_add(int a, int b):
return a + b
cpdef void py_hello(char* msg):
print("Python(Cython) lib:", msg)
3)创建编译脚本 setup.py
python
from setuptools import setup, Extension
from Cython.Build import cythonize
ext = Extension(
"pylib",
sources=["py_lib.pyx"],
)
setup(ext_modules=cythonize(ext))
4)编译生成动态库
bash
python setup.py build_ext --inplace
# 生成 pylib.cpython-xxx.so 动态库
2. Python 调用 外部 动态库
使用内置 ctypes,无需额外安装,示例调用之前的 libmylib.so
python
import ctypes
# 加载动态库
lib = ctypes.CDLL("./libmylib.so")
# 指定函数参数、返回值类型(必须,否则数值错乱)
lib.add.argtypes = (ctypes.c_int, ctypes.c_int)
lib.add.restype = ctypes.c_int
lib.hello.argtypes = (ctypes.c_char_p,) # char* 字符串
# 调用add
res = lib.add(100, 200)
print(f"Python调用C库 add(100,200) = {res}")
# 传入字节字符串(C字符串必须以\0结尾)
lib.hello(b"Hello Python ctypes\x00")
调用Rust编译的 librust_lib.so 示例
python
import ctypes
rust_lib = ctypes.CDLL("./librust_lib.so")
rust_lib.rust_add.restype = ctypes.c_int
print(rust_lib.rust_add(66, 34))
rust_lib.rust_hello(b"Call Rust from Python\x00")
调用Go编译的 libgolib.so 示例
python
import ctypes
go_lib = ctypes.CDLL("./libgolib.so")
print(go_lib.go_add(11, 22))
go_lib.go_hello(b"Call Go from Python\x00")
关键限制:Python无法直接使用静态库
Python运行时动态加载二进制,没有编译链接阶段,.a/.lib 静态库只能在编译程序时嵌入,Python做不到。
四、三大语言打包/调用库能力总汇总表
1. 能否生成静态库/动态库
| 语言 | 生成静态库(.a/.lib) | 生成动态库(.so/.dll/.dylib) |
|---|---|---|
| C/C++ | ✅ 原生支持 | ✅ 原生支持 |
| Rust | ✅ staticlib | ✅ cdylib(C兼容) |
| Go | ⚠️ c-archive,兼容性差,极少用 | ✅ c-shared 推荐 |
| Python | ❌ 无法生成 | ✅ 需Cython编译C扩展so |
2. 能否调用外部静态/动态库
| 语言 | 调用静态库 | 调用动态库 |
|---|---|---|
| C/C++ | ✅ gcc -static | ✅ -l链接动态库 |
| Rust | ✅ 编译时链接.a | ✅ 运行加载so |
| Go | ✅ cgo链接.a | ✅ cgo链接so |
| Python | ❌ 不支持 | ✅ ctypes 运行加载 |
五、避坑
-
所有跨语言库交互,只能用C基础类型
不能传Rust String、Go slice、Python对象、C++ std::string,只能用
int、char*、裸指针等C基础类型。 -
动态库分发坑
Linux运行程序找不到
.so报错:error while loading shared libraries解决:临时
export LD_LIBRARY_PATH=.;永久把库目录写入/etc/ld.so.conf更新缓存。 -
Windows特殊规则
Windows动态库
.dll需要配套.lib导入库;导出函数必须加__declspec(dllexport),否则外部无法调用。 -
为什么Rust必须
#[no_mangle]Rust编译器会自动修改函数名(名字混淆,支持泛型/重载),不加这个标记,外部C/Python找不到函数入口。
-
Go cgo性能损耗
Go和C库互相调用会切换运行时,高频计算场景优先纯Go实现,减少跨库调用。
-
Python ctypes内存风险
传给动态库的字符串必须是字节串,手动管理内存,跨库分配的内存不能交叉释放(C分配内存C释放,Rust分配Rust释放)。