如何将C++代码包装成C接口

为什么需要 C 接口包装

让 Go、Python、Rust 等语言调用 C++ 代码,直接链接通常行不通,因为 C++ 有三个 C 无法理解的机制:

  • 名称修饰(Name Mangling):支持重载的 C++ 编译器会对函数名做修饰,C 链接器找不到原始符号;
  • this 指针:成员函数隐含 this 参数,C 里没有;
  • 异常 / 构造析构:C 没有异常机制,也无法表达对象的生命周期。

解法:用 extern "C" 导出一层纯 C 接口,用不透明指针(void*)代表 C++ 对象,手动处理生命周期和异常。

案例 1:简单类包装

原始 C++ 类:

// counter.hpp
class Counter {
public:
    Counter(int init = 0) : count(init) {}
    void increment(int by = 1) { count += by; }
    int get() const { return count; }
private:
    int count;
};

C 接口头文件(extern "C" 防止名称修饰):

// counter_c.h
#ifdef __cplusplus
extern "C" {
#endif

typedef void* CounterPtr;            // 不透明指针

CounterPtr counter_create(int init);
void counter_increment(CounterPtr ptr, int by);
int counter_get(CounterPtr ptr);
void counter_destroy(CounterPtr ptr);

#ifdef __cplusplus
}
#endif

实现文件:

// counter_c.cpp
#include "counter.hpp"
#include "counter_c.h"

extern "C" {
    CounterPtr counter_create(int init) {
        try { return new Counter(init); }
        catch (...) { return nullptr; }       // 异常转错误
    }
    void counter_increment(CounterPtr ptr, int by) {
        auto c = static_cast<Counter*>(ptr);
        if (c) c->increment(by);
    }
    int counter_get(CounterPtr ptr) {
        auto c = static_cast<Counter*>(ptr);
        return c ? c->get() : -1;
    }
    void counter_destroy(CounterPtr ptr) {
        delete static_cast<Counter*>(ptr);
    }
}

案例 2:继承与多态

// shapes.hpp
class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};
class Circle : public Shape {
    double r;
public:
    Circle(double radius) : r(radius) {}
    double area() const override { return 3.14159 * r * r; }
};
// C 接口:创建时返回具体对象,用基类指针操作(多态照常生效)
ShapePtr circle_create(double radius) {
    try { return new Circle(radius); }
    catch (...) { return nullptr; }
}
double shape_area(ShapePtr shape) {
    auto s = static_cast<Shape*>(shape);
    return s ? s->area() : -1.0;
}
void shape_destroy(ShapePtr shape) {
    delete static_cast<Shape*>(shape);
}

异常处理与内存管理

// 异常绝不能穿过 C 边界(C 没有异常栈展开机制,会直接崩溃)
extern "C" int safe_op() {
    try { doCppWork(); return 0; }
    catch (const std::exception& e) { log(e.what()); return -1; }
    catch (...) { return -2; }
}

内存管理两种模型:

  • 显式 create/destroy:谁创建谁销毁,最简单;
  • 引用计数:接口提供 add_ref/release,适合共享对象。

规则必须写进头文件注释,明确所有权归属,否则跨语言的内存泄漏/重复释放防不胜防。

STL 容器跨边界

STL 类型不能直接跨 C 边界(内存布局不稳定),正确姿势是拷贝到 C 数组

int int_vector_copy_data(IntVectorPtr ptr, int* out, int max_size) {
    auto vec = static_cast<std::vector<int>*>(ptr);
    int n = std::min(max_size, (int)vec->size());
    std::copy(vec->begin(), vec->begin() + n, out);
    return n;
}

实战:Go 通过 CGO 调用

// 编译动态库
g++ -fPIC -shared counter.cpp counter_c.cpp -o libcounter.so
package main

/*
#cgo LDFLAGS: -L. -lcounter
#include "counter_c.h"
*/
import "C"

func main() {
    counter := C.counter_create(10)
    C.counter_increment(counter, 5)
    value := C.counter_get(counter)
    C.counter_destroy(counter)
}

常见问题

  • 内存泄漏:每个 create 必须有对应 destroy,所有权规则写进接口文档;
  • 线程安全:C++ 库非线程安全时在包装层加锁;
  • 类型安全:void* 无类型信息,可用结构体封装(如 struct Counter; typedef struct Counter* CounterPtr;)让编译器帮忙检查;
  • 性能:减少 C/C++ 边界跨越次数,批量传数据而非逐元素。

工具辅助

  • SWIG:从 .hpp 自动生成多语言包装(.i 文件配置);
  • CppSharp:专门生成 C# 绑定;
  • 手动包装:复杂项目更可控,本文即手动方案。

核心要点一句话:extern “C” 去掉修饰、void* 藏住对象、显式管理生命周期、异常转错误码——记住这四件事,C++ 就能被任何支持 C ABI 的语言调用。

滚动至顶部