C链接库,联动 Rust、Golang、Python

基础概念铺垫

1. 链接库是什么?

写代码时很多通用功能(加密、网络、数学计算)不用每次重写,把一堆函数、变量、类打包成独立二进制文件 ,这个文件就是链接库

程序编译时分两步:

  1. 编译 :源代码 → 机器码 目标文件 .o / .obj
  2. 链接 :把 目标文件 + 依赖库 合并成最终可执行程序

举栗子:

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

链接器做的事:

  1. 读取 main.ocalc.o 里的业务机器码;
  2. 去系统C标准库 libc 补全 printfexit 这类系统函数地址;
  3. 把所有代码、数据、符号表整合,生成能直接运行的 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语言

  1. 早年 汇编时 代没有 概念
    C语言诞生, 后为了 代码复用模块化,设计了 编译 + 链接 模型,
    静态库 .a 直接沿用Unix系统设计;
  2. C++ 完全兼容C链接模型,扩展了类、重载,动态库引入符号导出机制;
  3. 后面所有 高级语言 (Rust/Go/Python/Java)的 库 交互,底层全部复用操作系统提供的C ABI二进制标准 ,也就是说:所有语言 跨语言 调用库,本质都是走 C兼容接口

ABI (Application Bin Interface)

关键知识点:ABI二进制应用接口

不同语言内存布局、函数调用规则不一样,但 C语言 ABI 是 全操作系统 统一标准。

所以 任何语言 想对外提供库、调用外部库,都必须封装一层 C兼容接口,不能直接用语言自身特色语法(Rust所有权、Go协程、Python对象都不能跨库传递)。

两个核心场景

每个语言分两块:

  1. 【场景A】本语言打包生成静态库/动态库(给其他语言调用)
  2. 【场景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)

  1. 先编译出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
  1. 新建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

    1. build.rs 自身源码被修改;
    2. build.rs 里通过 println!("cargo:rerun-if-changed=xxx") 声明的文件/文件夹发生改动; rerun-if-changed 是为了增量构建提速
    3. 构建环境发生变化:Rust版本、编译target、feature开关、传入的RUSTFLAGS变更;
    4. 上一次 build.rs 执行异常崩溃(panic、非0退出码),下次强制重跑;
    5. 执行 cargo clean 清空缓存后,第一次构建必然重跑。
  • 下面的 全部条件 同时满足 才会跳过 不执行 build.rs, Cargo 直接复用上次 build.rs 输出的编译参数:

    1. build.rs 代码无改动;
    2. 所有 rerun-if-changed 监控的文件/目录完全没变化;
    3. 编译环境(target、feature、编译参数、rust版本)和上次一致;
    4. 上次 build.rs 正常成功退出。
整体流程
  1. 你执行 cargo build / cargo run
  2. Cargo 自动检测项目根目录存在 build.rs
  3. Cargo 单独启动一个子进程,编译、运行这个 build.rs
  4. Cargo 全程实时监听这个子进程的标准输出 stdout (就是 println! 打印出来的文字)
  5. 只要输出行匹配固定前缀 cargo:,Cargo 就解析这行指令,转化成编译参数传给 rustc
  6. 普通无 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内置特殊指令:

  1. cargo:rustc-link-search=路径
    作用:传递 -L 参数给链接器,告诉链接器「去这个文件夹找 .so/.a 库文件」
  2. cargo:rustc-link-lib=库名
    作用:传递 -l 参数给链接器,链接 libxxx.so 只需要写 xxx,不用加 lib 和后缀
  3. 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 额外扩展能力(生产常用)

  1. 库放在子文件夹 ./libs
rust 复制代码
println!("cargo:rustc-link-search=./libs");
  1. 强制静态链接 libmylib.a(如果同时存在.a和.so
rust 复制代码
println!("cargo:rustc-link-lib=static=mylib");
  1. 打印调试日志,看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"]

配置逐行解释

  1. [links.mylib]
    mylib = 库名,对应 libmylib.so,和 -lmylib 完全对应
  2. 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
  1. 静态库会把Go整套运行机制塞进你的程序里

    Go的协程、垃圾回收、内存管理是一整套独立系统。静态链接时,这套东西会直接合并到Rust/C主程序。如果你链接2个Go静态库,程序里会出现两套独立的垃圾回收、内存管理器,互相打架,直接崩溃。

    动态库.so是独立文件,每个库单独一套运行环境,互不干扰。

  2. 静态Go库会抢主程序的系统控制权

    静态打包后,Go会霸占程序的内存分配、信号报错处理逻辑,和Rust/C本身的内存、报错机制冲突,经常出现内存错乱、程序卡死;

    动态库只是运行时临时加载,不会篡改主程序底层全局逻辑。

  3. 静态库对编译环境要求极度苛刻

    想用Go静态库,你的Rust/C编译工具、系统底层库、Go版本必须完全一致,换台机器、升级Go就大概率链接失败;

    动态库只需要运行时存在对应.so文件,编译阶段无强制绑定,随便分发。

  4. 官方定位: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 addhello 两个C函数的签名,否则Go不知道这两个函数入参、返回值类型,编译报错。

3. 关键限制:位置不能乱

  1. 必须紧贴 import "C",中间不能有空行、不能有Go代码隔开;
  2. 只能写在文件最顶部、package main 之后、import "C" 之前;
  3. 如果挪到别的地方,就变成普通注释,完全失效。
失效示例(中间空一行,指令作废)
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 运行加载

五、避坑

  1. 所有跨语言库交互,只能用C基础类型

    不能传Rust String、Go slice、Python对象、C++ std::string,只能用 intchar*、裸指针等C基础类型。

  2. 动态库分发坑

    Linux运行程序找不到 .so 报错:error while loading shared libraries

    解决:临时 export LD_LIBRARY_PATH=.;永久把库目录写入 /etc/ld.so.conf 更新缓存。

  3. Windows特殊规则

    Windows动态库 .dll 需要配套 .lib 导入库;导出函数必须加 __declspec(dllexport),否则外部无法调用。

  4. 为什么Rust必须 #[no_mangle]

    Rust编译器会自动修改函数名(名字混淆,支持泛型/重载),不加这个标记,外部C/Python找不到函数入口。

  5. Go cgo性能损耗

    Go和C库互相调用会切换运行时,高频计算场景优先纯Go实现,减少跨库调用。

  6. Python ctypes内存风险

    传给动态库的字符串必须是字节串,手动管理内存,跨库分配的内存不能交叉释放(C分配内存C释放,Rust分配Rust释放)。