C语言如何通过封装+extern C调用C++模板生成的具体函数?
用C语言去调用C模板这事儿乍一听颇为反常识然而在实际项目当中却着实常常会碰到比如说你正维护着一个年代久远的C项目此刻想要运用C标准库里面现成的排序算法又或者你手头现有的嵌入式SDK仅仅提供C接口可团队内部却打算采用更具现代感的C容器来编写业务逻辑。在这种情形下要是能够于C当中运用C模板所带来的类型安全以及代码复用特性那么就能够省去大量重复做无用功的精力。先分清模板和函数的本质区别有不少开发者觉得C模板是“高级类别技术”实际上并非如此。模板更近似于一种代码生成装置在你写出template T add(T a, T b)这种形式时编译器并不会马上生成机器代码。仅在你于代码之中写下add(1, 2)亦或add(1.5, 2.5)的时候编译器才会按照这般明确类型去“镌刻”出相应的函数。此一镌刻进程就被称作实例化。C语言编译器没法看懂模板语法也根本不知道该如何去实例化。然而C存在着一个天然的优势它所认的是编译之后的具体函数名。只要我们在C那一侧提前将模板实例化成实实在在的函数接着按照C的命名规则把它暴露出去C便能够像调用普通函数那样去调用它。这可是打通壁垒的关键一步。包装层是唯一可行的桥梁硬要直接让C代码去#include C模板头文件这是绝对不可能达成的因为一旦C编译器碰到尖括号以及template关键字它就会毫不留情地报错。而正确的行径应当是去编写一个中间层。这个中间层是用C来撰写的其职责在于对模板进行实例化操作并且通过externC去声明若干个包装函数。externC 的作用在于禁止 C 编译器针对函数名进行“名字修饰” 如此一来所生成的函数符号是 c_add_int 这种纯 C 风格并非 _Z3addIiET_S0_S0_ 这种 C 特有的混乱符号。比如你有一个 C 模板加法函数想给 C 用。在C包装文件当中首先要进行显式实例化具体为template int32_t add(int32_t, int32_t);之后还要写一个代码块此块前面有externC 内容是int32_t c_add_int(int32_t a, int32_t b) { return add(a, b); }。显式实例化确保链接器能够找寻到函数体externC 确保 C 可以正确寻找到函数名。完成这两步之后C 端仅仅需要声明 int32_t c_add_int(int32_t, int32_t); 便能够直接发起调用操作了。从代码到运行的全流程拆解// math_template.hpp #ifndef MATH_TEMPLATE_HPP #define MATH_TEMPLATE_HPP template T add(T a, T b) { return a b; } #endif后续撰写 wrapper.cpp将这个头文件予以包含接着对 template int add(int, int);进行显式实例化随后定义 externC 的 int c_add_int(int a, int b) { return add(a, b); }。另外去撰写一个 math_c_api.h这里面仅仅包含着 #ifdef __cplusplus 的那种兼容性宏以及 int c_add_int(int, int); 这样的声明。最终去编写 main.c 它要包含这个头文件还要直接调用 c_add_int(3, 4) 。// wrapper.cpp #include math_template.hpp #include // for int32_t // 显式实例化模板必须否则链接时找不到符号 template int32_t add(int32_t, int32_t); template double add(double, double); // C 风格包装函数 extern C { int32_t c_add_int(int32_t a, int32_t b) { return add(a, b); } double c_add_double(double a, double b) { return add(a, b); } }编译的时候首先编译wrapper.cpp进而生成目标文件接着编译main.c又生成另外一个目标文件最后进行链接形成可执行文件。运行之后会输出7。在整个过程当中C代码并不清楚模板的存在它仅仅瞧见一个普通的C函数然而底层却运用上了C模板的类型安全以及代码复用。为什么显式实例化这一步不能省// math_c_api.h #ifndef MATH_C_API_H #define MATH_C_API_H #ifdef __cplusplus extern C { #endif int32_t c_add_int(int32_t a, int32_t b); double c_add_double(double a, double b); #ifdef __cplusplus } #endif #endif有不少刚开始学习的人在这儿遭遇挫折。他们觉得只要于包装函数当中去调用add(a, b)那么编译器便会自行生成实例。然而C的编译单元是彼此独立的要是wrapper.cpp里未曾出现过add的任何实例化代码那么编译器在编译此文件之际就不会生成add的机器码。待到进行链接之际链接器察觉到在 c_add_int 当中存有对 add 的调用情况可遍寻所有目标文件均寻觅不到其定义所在如此一来便会呈报出“undefined reference”这般的错误。// main.c #include #include #include math_c_api.h int main() { int32_t i c_add_int(10, 20); double d c_add_double(3.14, 2.86); printf(Int result: %d\n, i); // 输出 30 printf(Double result: %.2f\n, d); // 输出 6.00 return 0; }显式实例化template int add(int, int);这意味着向编译器传达要于这个编译单元当中依照模板去生成一份属于int版本的add函数。如此一来机器码便会切实地被生成出来到了链接的时候才能够找得到。C 和 C 混合项目的编译要点# 编译 C 部分生成目标文件 g -c wrapper.cpp -o wrapper.o # 编译 C 部分并链接 gcc main.c wrapper.o -o demo # 运行 ./demo此等混合编译的项目一般没办法借由一条命令予以搞定。最为稳妥的方式乃是分开进行编译运用 g 去编译所有的 C 文件涵盖包装文件借助 gcc 编译所有的 C 文件最终使用 g 或者 gcc 链接都行得通但却建议采用 g缘由在于它能够自动链接 C 标准库。若是在链接这个行为当中运用了gcc那就得手动去添加 -lstdc不然的话就会出现那种显示找不到C标准库符号的错误情况。Int result: 30 Double result: 6.00关于文件组织将 C 端能够看到的 API 声明单独放置于一个纯粹的 C 头文件之中此头文件不涵盖任何 C特有的语法包装层的实现文件 wrapper.cpp 能够包含繁杂的 C头文件以及模板逻辑如此一来C 端代码全然无需知晓 C的存在维护起来也显得清爽。不是所有模板都值得这样封装这种采用“封装 显式实例化”的方式存在其适用的边界范围它最为适宜用于封装那些类型已然确定、数量并非众多的模板实例举例来说倘若你的业务仅仅需要处理int、float、double这三种类型的模板函数那么针对每种类型编写一个包装函数所耗费的成本是很低的然而要是模板参数存在几十种组合情况又或者涉及到复杂的模板特化以及偏特化那么手动进行封装的工作量将会呈现出爆炸式增长。此时此刻倒不如思索考量改变并使用C风格的void*加上回调函数又或者再度全面评估是不是真的有必要让C去调用这一套模板。同样需要留意要是模板内部运用了 C 所特有的异常或者 RTTI那么在包装层当中最好对异常进行捕获并且将其转换为 C 能够理解的错误码不然的话异常穿越 externC 边界抛到 C 端将会致使程序崩溃。当你来到此处时你会发觉C语言去调用C模板并非是什么神秘莫测的黑魔法其核心要点仅有两步先是在C一侧进行显式实例化接着运用externC去做一层纯粹函数的包装。这样的一种做法既将模板的类型安全以及代码复用给保留了下来又能够使得古老的C代码平稳地接入现代C库。要是你手头此刻正在维护一个C与C相混合的老旧项目那么不妨尝试一下这个思路。有哪些尴尬场景是你碰到过的是那种“C 想用 C 特性”的情况最终解决它所采用的办法是什么快点在评论区把你的实际操作经验分享出来。