C++ 引用能否用于 extern "C",如何提供 C 兼容头文件?
结论
extern "C" 函数可以使用 C++ 引用参数,这在 C++ 中是合法的。但这种声明需要由 C++ 编译器处理,不能直接放进供 C 编译的头文件。
如果调用方需要生成 C 代码,建议提供一个使用指针参数的 C 接口,由 C++ 实现将指针转换成原函数需要的引用。原来的 foo(quz::Bar &s) 可以保留。
为什么这段代码能编译?
原来的声明是:
struct Bar; // opaque type
extern "C" {
void foo(Bar &s);
}
这段声明由 C++ 编译器按 C++ 语法解析。extern "C" 指定语言链接,涉及函数符号和调用约定等实现规则。它不会将块内代码切换成 C 语法,也不要求参数只能使用 C 类型,所以这里的引用参数合法。相关规则见 C++ 标准草案的语言链接章节。
声明一个接收引用或指针的函数时,无需知道所指类型的完整布局。struct Bar; 这样的前置声明已经足够,因此仅声明接口时不必引入类型定义所在的头文件。
不过,声明通过编译只能说明编译器接受了它,不能证明它与目标函数的定义、链接符号和跨语言调用约定一致。
此前 FFI 调用成功,是否说明引用可以当作指针使用?
这取决于目标平台的 ABI 和 FFI 实现。
例如,Itanium C++ ABI 的引用参数规则规定,引用参数通过传递其绑定对象的指针来实现。如果原来的 FFI 恰好以匹配的方式传入有效对象的地址,调用就可能正常工作。
但题目没有给出编译器、平台、函数定义和 FFI 绑定代码,无法确认此前成功的具体原因,也不能将这种行为推广到所有平台。
不要只在 C 头文件中把参数写成 Bar *,却让同一个导出函数的 C++ 定义继续使用 Bar &,并默认两者具有可移植的兼容性。即使某个 ABI 下两者的二进制传参方式相同,兼容性仍依赖该实现的互操作规则。增加一个明确的指针参数包装函数,更容易维护和验证。
如何提供 C 兼容头文件?
可以使用不透明句柄:C 调用方只持有指针,具体对象仍由 C++ 一侧管理。
下面用 BarHandle 作为独立的包装类型。错误码是示例约定,可以按项目现有接口调整。
1. 公共头文件
#ifndef FOO_C_H
#define FOO_C_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct BarHandle BarHandle;
/* 0: success; -1: null argument; -2: C++ exception */
int foo_c(BarHandle *s);
#ifdef __cplusplus
}
#endif
#endif
C 编译器看不到 extern "C",也不会遇到引用、命名空间或 C++ 类定义。C++ 编译器则会为 foo_c 使用 C 语言链接。
2. C++ 包装实现
#include "foo_c.h"
namespace quz {
struct Bar;
}
// Original C++ function.
void foo(quz::Bar &s);
// Private definition, invisible to C callers.
struct BarHandle {
quz::Bar *object;
};
extern "C" int foo_c(BarHandle *s)
{
if (s == nullptr || s->object == nullptr) {
return -1;
}
try {
foo(*s->object);
return 0;
} catch (...) {
return -2;
}
}
实际项目中,包装实现应包含原函数的真实声明,避免手写声明与库接口不一致。这里用前置声明展示接口结构;传递 quz::Bar 的引用本身不需要该类型的完整定义。
包装函数接收指针参数,先检查空指针,再通过 *s->object 调用原来的引用接口。C++ 包装层将异常转换成错误码,避免让普通 C 调用方处理 C++ 异常。
3. 明确句柄从哪里来
这个示例只展示调用适配。库还需要提供获取和释放 BarHandle 的方式,例如通过已有对象获取句柄,或提供创建、销毁函数。
题目没有给出 quz::Bar 的构造方式和所有权规则,因此不能假设它支持默认构造,也不能随意补上 new quz::Bar()。
C 调用方应只传回库提供的有效句柄。还需约定底层对象的生命周期、句柄由谁释放,以及包装层是否拥有底层对象。空指针检查无法识别悬空指针或伪造的句柄。
原代码还有一个类型区别
最初的函数使用:
void foo(quz::Bar &s);
后面的声明却使用:
struct Bar;
extern "C" void foo(Bar &s);
这里新声明的全局 ::Bar 与 quz::Bar 是不同的类型,即使名称相似,也不会自动成为同一种类型。第二个声明也不会自动生成调用第一个函数的包装代码。
如果它们确实是题目所示的两个不同类型,这两个声明就不是同一个函数的等价声明。若实际代码使用的是同一种类型,也应确保所有声明和定义的语言链接一致,不能把已经声明为 C++ 链接的同一个函数直接改成 C 链接。
需要从 C 调用时,应为包装函数选择明确的导出名称,例如 foo_c,使其声明和定义都使用指针参数,再由它调用原有 C++ 函数。
备注:内容仅供参考。
