news 2026/9/9 22:26:39

V 语言与 C 互操作实战:在 C 代码中调用 V 编译的共享库与源码函数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
V 语言与 C 互操作实战:在 C 代码中调用 V 编译的共享库与源码函数

V 语言与 C 互操作实战:在 C 代码中调用 V 编译的共享库与源码函数

【免费下载链接】vSimple, fast, safe, compiled language for developing maintainable software. Compiles itself in <1s with zero library dependencies. Supports automatic C => V translation. https://vlang.io项目地址: https://gitcode.com/GitHub_Trending/v/v

本指南围绕 call_v_from_c 示例展开,完整演示如何把用 V 语言编写的函数暴露给 C 程序调用。文章覆盖两种可行路径——把 V 代码编译成平台共享库(.so/.dylib/.dll)并链接,以及让 V 直接生成可参与 C 编译的.c源码——并提供 Linux、macOS、Windows 三平台逐条可复制的命令。读完你即可在自己的 C/C++ 项目中安全、规范地复用 V 实现的算法模块与字符串处理函数。

示例全景:一个 C 主程序调用两个 V 模块

示例目录 examples/call_v_from_c 的文件结构清晰地把“被调用的 V 库”与“发起调用的 C 程序”分开:

文件角色说明
v_test_print.vV 侧“库”导出一个 C 符号foo,内部把 C 字符串打印出来
v_test_math.vV 侧“库”导出squaresqrt_of_sum_of_squares两个数值函数
test_print.cC 主程序声明extern void foo(const char*)并调用两次
test_math.cC 主程序声明并调用两个数学函数,用printf输出结果

值得注意的是,两个 V 源文件首行都带有注释// vtest build: false,含义即“本文件不参与常规v test构建,需要特殊的编译选项”。这印证了它们必须以本文所述的库/源码方式手工编译,不能像普通main程序那样直接运行。

整个调用链条是:C 的main函数通过extern声明引用 C ABI 符号 → 链接器解析该符号 → 符号来自 V 编译器生成的共享库或 C 源码 → 运行时在 V 实现的函数内部完成真正的计算与输出。理论上这允许在 C 生态中长期稳定地复用 V 代码——V 编译产物就是标准 C ABI,不依赖任何 V 运行时解释器。

读懂示例代码:V 侧如何“导出”函数

@[export: 'name']指定 C 可见符号名

这是整条互操作链的关键。看 v_test_print.v:

module test_print @[export: 'foo'] fn show_foo(s &char) { println(unsafe { cstring_to_vstring(s) }) }

V 函数名是show_foo,但@[export: 'foo']把它生成的真实 C 符号名强制改写为foo——这正是 C 端extern void foo(const char* s);所引用的名字。若去掉该属性,编译器会按内部命名规则生成带模块限定的符号,C 端难以用简短的固定名字可靠声明。@[export]在 V 标准库中也被普遍用于把内置函数暴露成标准 C 库符号名,例如 libc_impl.v 中@[export: 'memcpy']@[export: 'malloc'],以及 builtin_backtraces_d_musl.c.v 中@[export: 'backtrace']——可见“让 V 函数以指定 C 名字输出”是编译器一等公民能力,而非示例专用技巧。

再看数值模块 v_test_math.v:

module test_math import math @[export: 'square'] fn calculate_square(i int) int { return i * i } @[export: 'sqrt_of_sum_of_squares'] fn calculate_sqrt_of_sum_of_squares(x f64, y f64) f64 { return math.sqrt(x * x + y * y) }

calculate_squaresquare导出,calculate_sqrt_of_sum_of_squares则以完整名字直接导出(导出名与 V 函数名可相同也可不同)。

字符串跨语言边界:&charcstring_to_vstring

show_foo的参数类型是&char,对应 C 侧const char*。V 本身是 GC 语言,其string由(指针, 长度)二元组构成,并不等于 C 的 NUL 结尾char*;因此在 FFI 边界上 V 用&char(即 Cchar*)接收 C 字符串,再在函数体内转成 V 字符串。转换依赖 vlib/builtin/string.v 中的cstring_to_vstring

@[unsafe] pub fn cstring_to_vstring(const_s &char) string { s := unsafe { tos2(byteptr(const_s)) } return s.clone() }

从源码(string.v 第 79-100 行)可以确认两个关键语义:

  1. 它通过s.clone()生成独立的新拷贝,不共享 C 侧内存,因此 C 端随后释放或改写原缓冲区是安全的;
  2. 该函数被标记为@[unsafe],且注释明确说明若指针为0会 panic,所以调用必须包在unsafe { }块里(示例代码正是unsafe { cstring_to_vstring(s) }的写法)。

与此相对的tos/tos2等函数则复用而非拷贝 C 内存,注释中特别警告要理解其与-autofree@[manualfree]的相互作用后才可使用(见 string.v 第 102-107 行)。对跨语言传入的字符串,优先用cstring_to_vstring做拷贝是默认安全选择。

C 端的extern声明与调用

test_print.c 极其精简:

extern void foo(const char* s); int main() { foo("hello"); foo("bye"); }

test_math.c 则演示了带返回值的数值调用与输出:

#include <stdio.h> extern int square(int i); extern double sqrt_of_sum_of_squares(double x, double y); int main() { int i = 10; printf("square(%d) = %d\n", i, square(i)); double x = 0.9; double y = 1.2; printf("sqrt_of_sum_of_squares(%f, %f) = %f\n", x, y, sqrt_of_sum_of_squares(x, y)); }

例中函数签名一一对应:Vfn calculate_square(i int) int↔ Cint square(int);Vfn calculate_sqrt_of_sum_of_squares(x f64, y f64) f64↔ Cdouble sqrt_of_sum_of_squares(double, double)。V 的int对应 Cintf64对应 Cdouble&char对应char*,这些在示例的 extern 声明中即为准确映射。成功运行时test_math应输出square(10) = 100sqrt_of_sum_of_squares(0.9, 1.2) = 1.5

方式一:把 V 代码编译成共享库并链接进 C 程序

共享库是最通用、最贴近生产部署的方式:V 先产出动态库产物,C 程序编译时链接它,运行期由系统动态加载器解析符号。README 为三大平台分别给出了逐条命令。

Linux

# 第 1 步:把 V 代码编译成共享库(默认输出 v_test_print.so / v_test_math.so) v -cc gcc -shared v_test_print.v v -cc gcc -shared v_test_math.v # 第 2 步:编译 C 文件并链接共享库,-Wl,-rpath=. 让运行时在当前目录查找 .so gcc test_print.c v_test_print.so -o test_print -Wl,-rpath=. gcc test_math.c v_test_math.so -o test_math -Wl,-rpath=. # 第 3 步:运行 ./test_print ./test_math

要点说明:

  • -cc gcc显式指定 V 调用的 C 后端编译器为 gcc,保证与后续手写 gcc 编译命令的工具链一致;
  • -shared让 V 产出共享库而非可执行文件;
  • -Wl,-rpath=..(当前目录)写入可执行文件的运行时库搜索路径。若省略,运行时常报error while loading shared libraries: v_test_print.so: cannot open shared object file

macOS(OSX)

# 0. 安装 Boehm GC(示例依赖 libgc) brew install libgc # 1. 编译 V 代码为共享库(macOS 产物后缀为 .dylib) v -cc gcc -shared v_test_print.v v -cc gcc -shared v_test_math.v # 2. 编译链接。按架构追加头文件/库路径: # x86_64: -I/usr/local/include -L/usr/local/lib # arm64: -I/opt/homebrew/include -L/opt/homebrew/lib gcc test_print.c v_test_print.dylib -o test_print -I/usr/local/include -L/usr/local/lib gcc test_math.c v_test_math.dylib -o test_math -I/usr/local/include -L/usr/local/lib # 3. 运行时通过 LD_LIBRARY_PATH=. 指定共享库所在目录 LD_LIBRARY_PATH=. ./test_print LD_LIBRARY_PATH=. ./test_math

macOS 上没有 Linux 的$ORIGIN-rpath 惯例时,README 给出的做法是在运行期用环境变量LD_LIBRARY_PATH=.告知加载器去当前目录找.dylib。头文件与库路径则取决于 Homebrew 的安装前缀——Intel 机型为/usr/local,Apple Silicon(arm64)为/opt/homebrew,两条 arch 匹配的-I/-L缺一不可。

Windows

# 1. 编译 V 代码为共享库(Windows 产物后缀为 .dll) v -cc gcc -shared v_test_print.v v -cc gcc -shared v_test_math.v # 2. 用 gcc(如 MinGW-w64)编译链接 gcc test_print.c v_test_print.dll -o test_print.exe gcc test_math.c v_test_math.dll -o test_math.exe # 3. 运行 test_print.exe test_math.exe

在 Windows 上产物后缀为.dll。由于 Windows 加载器默认包含可执行文件所在目录,链接后通常可直接运行,无需 rpath 或LD_LIBRARY_PATH。若使用 MSVC 工具链,需注意示例命令面向 gcc,并保证 V 编译时通过-cc gcc使用的是同一套 MinGW gcc。

方式二:让 V 生成可参与 C 编译的 C 源码

若不想引入动态链接、希望把 V 生成的 C 代码直接与手写 C 文件一起静态编入单一可执行文件,可采用 README 的第二种路径——利用 V 的 C 后端“输出源码”能力。

前置要求:必须安装 libgc。此路径下 gcc 手工编译需要显式链接 Boehm GC 运行时,README 特别标注了该依赖。macOS 可用brew install libgc;Linux 一般从发行版软件源安装 Boehm GC 开发包即可。

# 第 1 步:让 V 生成 C 源码文件。 # 当 -o 指定 .c 后缀时,V 生成对应的 C 源文件而非二进制: v -shared -cc gcc -o v_test_print.c v_test_print.v v -shared -cc gcc -o v_test_math.c v_test_math.v # 第 2 步:把 V 生成的 .c 与手写 C 主程序一起编译链接: # test_math 需要额外 -lm(数学库) gcc test_print.c v_test_print.c -o test_print -lgc gcc test_math.c v_test_math.c -o test_math -lgc -lm # 第 3 步:运行 ./test_print ./test_math

原 README 明确提示了该机制的要点:“Specifying the output with a.cextension will generate the corresponding C source file.” 也就是说,V 编译器本身就是 C 后端——把 V 翻译成等价的 C 再交给系统编译器。用-shared -o xxx.c的形式输出后,V 不再替你调用 C 编译器,而是把翻译结果完整交到你手里,因此链接阶段的一切依赖都要手工补齐:

  • -lgc:V 默认使用 Boehm GC 做内存管理,生成的 C 源码引用了 GC 符号;
  • -lmtest_math用到math.sqrt,需链接数学库(test_print纯字符串/IO 则不需要)。

对照之下,方式一(动态库)中这些依赖由 V 在生成.so/.dylib/.dll时自行处理完毕,C 侧只需链接库文件本身。

底层原理与源码佐证

-shared在编译器中的定义

在 vlib/v/help/build/build-c.txt 中可见 V 对-shared的官方说明:

-shared:Tell V to compile a shared object instead of an executable. The resulting file extension will be.dllon Windows and.soon Unix systems.

-shared把默认的“可执行文件”产物切换为“共享对象”,在 Windows 上产出.dll。README 中 macOS 实际使用.dylib后缀,属于 Darwin 平台的动态库惯例。由于 V 默认走 C 后端,这一标志最终作用于其调用的 gcc/clang 等底层 C 编译器。

@[export]是 V 语言层提供的符号控制原语

@[export: 'foo']这类属性在 V 语言中被解析为“导出属性”,其作用是让被修饰函数在生成代码中直接使用给定名字作为符号,从而形成稳定的 C ABI 契约。除本示例外,仓库大量内建实现也依赖同一机制对接 libc 符号(如 libc_impl.v 里的memcpymallocqsort),充分说明它正是 V 官方实现 C 符号互操作的统一手段。

字符串转换的内存安全层级

跨语言字符串的安全性问题在源码里体现得非常明确(string.v):

  • cstring_to_vstring(L79-L90):构造 V 字符串后立即clone(),与输入 C 内存解耦,推荐默认使用;
  • tos_clone(L92-L100):语义与cstring_to_vstring相同,区别仅在于入参类型为&u8
  • tos(L102-L107):仅包装不拷贝,直接复用外部内存块,且对0指针 panic,只有清楚内存生命周期归属时才应使用。

三者共同构成“拷贝安全 → 零拷贝高风险”的完整光谱,写互操作代码时应按需选择。

常见问题排查速查

现象原因与对策
链接时报undefined reference to 'foo'/ 运行时报符号找不到V 侧漏写@[export: 'foo'],或导出名与 C 的extern声明不一致。可用nm -D v_test_print.so \| grep foo(Linux/macOS)核对真实符号名
Linux 运行报cannot open shared object file缺少-Wl,-rpath=.;或改为LD_LIBRARY_PATH=. ./test_print
手工编译 V 生成的.c时报undefined reference to GC_*未加-lgc,先确认已安装 Boehm GC(macOS:brew install libgc
test_math链接报sqrt相关未定义数学函数需追加-lm
macOS 上找不到头文件/库架构路径写错:x86_64 用/usr/local,arm64 用/opt/homebrew
字符串内容异常或崩溃确认 V 侧参数为&char并用unsafe { cstring_to_vstring(s) }转换后再使用
默认输出产物名与预期不符共享库产物通常为v_test_print.so/.dylib/.dll,如目录中已有同名旧产物可先清理避免链接到过期文件

延伸阅读

  • C 与 V 类型互操作性详解:系统梳理 C/V 两侧标量、指针、结构体、回调等类型映射的官方文档;
  • call_c_from_v 示例:反向场景——在 V 中调用 C 代码的对照实现,可与本文组成完整的双向互操作闭环;
  • V 编译器 C 后端选项说明:-shared-o file.c-cflags等与本主题直接相关的编译标志权威说明;
  • 字符串与内存语义源码:cstring_to_vstringtostos_clone的实现细节与生命周期注释。

V 编译出的共享库与 C 源码都遵循标准 C ABI,这使得 V 可以平滑地作为 C/C++ 项目内部的“可维护性语言插件”存在:用 V 书写类型安全、可快速迭代的业务算法,再以@[export]暴露成稳定 C 接口,由既有 C 工程按本文两条路径之一完成集成。

【免费下载链接】vSimple, fast, safe, compiled language for developing maintainable software. Compiles itself in <1s with zero library dependencies. Supports automatic C => V translation. https://vlang.io项目地址: https://gitcode.com/GitHub_Trending/v/v

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 22:26:16

从零搭建LeNet风格CNN:PyTorch实现MNIST图像分类

前两篇我们把卷积的计算方式、卷积核的含义、特征图是怎么来的这些基础概念过了一遍&#xff0c;可能你已经有点感觉了&#xff1a;卷积本身不复杂&#xff0c;复杂的是怎么组织成一整套能解决实际问题的网络。这一篇就干一件事——把前面那些零散概念组装起来&#xff0c;搭出…

作者头像 李华
网站建设 2026/9/9 22:22:33

Seelen-UI:如何把 Windows 桌面改造成 5 分钟上手的高效率工具

Seelen-UI&#xff1a;如何把 Windows 桌面改造成 5 分钟上手的高效率工具 【免费下载链接】Seelen-UI The Fully Customizable Desktop Environment for Windows 10/11. 项目地址: https://gitcode.com/GitHub_Trending/se/Seelen-UI Seelen-UI 是一套跑在 Windows 10/…

作者头像 李华
网站建设 2026/9/9 22:21:29

DeepSeek接入QQ机器人:多段回复逻辑实现拟人化聊天体验

把 DeepSeek 接进 QQ 群&#xff0c;很多人的第一版做法都差不多&#xff1a;写个脚本&#xff0c;群里发一句话&#xff0c;脚本调一次 API&#xff0c;再把返回结果发回群里。跑通不难&#xff0c;但用两天你就会发现&#xff0c;这个机器人非常“AI”——回复动辄几百字&…

作者头像 李华
网站建设 2026/9/9 22:21:20

栈与队列深度解析:从底层实现到工程实战

1. 先说结论&#xff1a;为什么栈和队列永远值得再聊一遍栈和队列这两个词&#xff0c;刷过题的人闭着眼都能写出几个操作&#xff0c;背八股的人张口就是"后进先出、先进先出"。但真到项目落地的时候&#xff0c;能把它们用得漂亮的人其实没那么多。我看了一圈最近的…

作者头像 李华
网站建设 2026/9/9 22:20:53

SEO检测报告怎么读?墨衍 6 大维度小白解读

标签&#xff1a;SEO检测 SEO检测报告 墨衍 教程 跑完 SEO检测 却看不懂报告&#xff1f;本文按 墨衍 6 大维度&#xff0c;用 小白能懂 的话解读 SEO检测报告 每一栏该干啥。入口&#xff1a;https://mp.csdn.net/seo。 读 SEO检测报告 的顺序 综合建议&#xff08;先抓待办…

作者头像 李华