CppCon 2014 学习:Hourglass Interfaces for C++ APIs

article/2025/7/30 13:40:09

共享库(Shared Libraries) 的基本结构和机制。


什么是 Shared Library?

共享库是在多个程序之间共享的一组可执行代码和数据,可以在运行时动态加载。

  • 在 Windows 中通常是 .dll
  • 在 Linux 中是 .so(Shared Object)
  • 在 macOS 中是 .dylib

构成组件解释:

Library(共享库本身)

  • 包含:

    • Code(函数实现、算法等)
    • Data(全局变量、配置等)
  • 提供一个 Interface:让外部程序(客户端)调用


Client Program(使用共享库的程序)

  • 编译时只需要知道库的 接口(例如头文件),而不需要库的完整实现
  • 运行时通过 动态链接 加载库

ABI(Application Binary Interface,应用二进制接口)

  • 是客户端和库之间的**“契约”**

  • 包括:

    • 函数调用规则(参数如何传递)
    • 内存对齐
    • 名字修饰(Name mangling)
  • 不同编译器或平台 ABI 可能不兼容


Common Libraries(常见共享库)

比如:

类别示例
C/C++ 标准库libc.so, msvcrt.dll
图形处理OpenGL, DirectX, Vulkan
数学库BLAS, cuBLAS, MKL
并行库OpenMP, TBB, AAL

优点:

  • 节省内存:多个程序共享同一个库代码
  • 便于更新:库更新无需重新编译所有程序
  • 可扩展性强:可以动态替换/加载模块(插件机制)

在这里插入图片描述

  • Client Program(客户端程序):包含代码(Code)和数据(Data),用橙色表示。
  • Library(库):同样包含代码(Code)和数据(Data),用蓝色表示。

两者通过一个Interface(接口)连接,接口定义了客户端程序与库之间的交互规则。右侧紫色框标注为ABI(Application Binary Interface,应用程序二进制接口),表示接口的标准化规范,确保客户端程序和库之间的兼容性。箭头指向“Common libraries(通用库)”,说明多个客户端程序可以通过相同的ABI共享通用库,实现代码和数据的重用与高效管理。

不同 ABI(应用二进制接口)之间的兼容性问题


不同 ABI 引发的细微问题(Subtle Problems)

示例:Class A vs Class B

假设你有两个类:

class A {
public:int x;virtual void foo();
};class B {
public:int x;virtual void foo();
};

从源代码看,A 和 B 看起来一样。
但编译器可能会因编译选项不同(如 debug/release)而给出不同的布局:

编译设置影响 ABI 的因素
Debug vs Release插入调试符号、填充额外字段、启用断言等
编译器版本虚函数表(vtable)布局、名字修饰规则可能不同
平台差异字节对齐方式、大小端序、参数传递规则

为什么不同 ABI 会导致问题?

  1. 类布局不一致
    比如调试版本插入了额外的成员,或 release 启用了内联优化,导致对象结构不同。

  2. 名字修饰不一致(Name Mangling)
    编译器会对函数名进行“修饰”,不同设置下可能生成不同符号名。

  3. vtable(虚函数表)结构变化
    虚函数顺序或表结构不一致,调用错地址。


典型 Bug 现象

  • 程序崩溃(Segmentation fault / Access Violation)
  • 虚函数调用错误对象
  • 内存泄漏或 double delete
  • 程序行为诡异而难以调试

如何避免 ABI 不兼容?

方法说明
使用稳定 ABI 的库接口如纯 C 接口 (extern "C"),避免类传递
编译共享库与客户端使用相同设置相同编译器 + 编译选项(debug/release)
使用 POD 类型(Plain Old Data)避免虚函数、继承、STL 类型跨 ABI
明确版本和 ABI 兼容策略比如 C++20 [[abi_tag]]

建议:

  • 共享库之间尽量通过纯 C 接口(或者简单 POD 结构)通信
  • 对外暴露 API 时避免使用 std::string, std::vector 之类 STL 类型
  • 编译时统一 flags 和工具链,尽量使用 release 或 debug 全套一致版本

构建一个对客户端友好的库(Library),减少使用成本、维护负担和技术门槛。你列出的四点体现了构建高质量、可维护库的核心理念:


你的目标逐条解析:

1. Don’t make my clients build my code if they don’t want to

提供预编译库(如 .dll, .so, .lib
提供头文件(头文件+二进制 = Header-only or Binary distribution)
这样,客户端可以直接使用你的库,无需重新编译源码,大大降低集成成本。


2. Don’t make them change how they build their code

避免要求特定编译器、编译选项、宏定义、特殊 flags
保持 ABI 稳定,支持多平台/多构建系统(如 CMake)

关键在于兼容性与灵活性,你的库应该无缝嵌入用户现有的构建流程。


3. Don’t make them rebuild their code because I shipped a fix in mine

保持 ABI 向后兼容(Binary Compatibility)
避免在头文件中频繁更改公开接口
可选:使用 Pimpl(Pointer to Implementation)或纯虚接口来稳定 ABI

这样用户在你升级库时,只需替换 .dll / .so,无需重编他们自己的代码。


4. Don’t make them learn a new language if they don’t want to

用他们已经熟悉的语言(如 C++)对外提供接口
或者提供 C API 或语言绑定(如 Python/C++, Java/C++, etc.)

这条的重点是不强加技术栈或学习成本。如果你的库是用 C++ 实现的,对外暴露 C 接口可以让其他语言绑定也变得容易。


总结:高质量库的设计哲学

原则做法
易用性头文件 + 编译好库,示例、文档清晰
兼容性稳定 ABI,避免头文件变化,使用 C 接口
灵活性不强制依赖特定编译器、构建系统
解耦性用户更新库无需重编所有代码

使用 C 接口(或类似 PIMPL 模式)来封装 C++ 库的好处,是开发长期维护性强、跨语言兼容性好的库的关键策略。我们逐条来看:


为什么这有帮助?

1. 避免 ABI(应用二进制接口)相关的兼容性问题

  • 不同编译器、编译选项、甚至 C++ 标准库实现,都会导致 ABI 不兼容。
  • C 接口是稳定的(例如:extern "C" 没有 name mangling),更易维护。

用户不需要因为你升级了编译器或用不同 STL 实现就重编他们的代码。


2. 不允许在二进制接口中使用复杂的 C++ 类型(如 std::string

  • std::string, std::vector, std::map 等在不同环境中 ABI 不同。
  • const char* 或自定义 POD 类型(Plain Old Data)暴露接口更加安全。

这避免了隐藏的内存管理风险和类型不匹配崩溃。


3. 保持数据结构的内部布局私有

  • 通过封装(例如使用不透明指针 struct Impl;),你可以在不改变头文件的情况下修改内部实现。
  • 用户只看到一个指针,不知道也不依赖你的内部细节。

大大增强了封装性和后期维护能力。


4. 更容易绑定到其他语言

  • C 接口可被几乎所有语言绑定(Python, Java, Rust, Go…)
  • C++ 类型和特性(虚函数、多态、模板)不容易跨语言调用。

C 接口 + 明确内存模型 = 更好地支持语言绑定和 FFI(Foreign Function Interface)


5. 两边仍然可以使用 C++ 的所有便利

  • 你内部用现代 C++(如 STL、RAII、智能指针、lambda)提高开发效率。
  • 客户端同样可以用 C++,只是通过一个稳定、简单的 C 接口访问。

对内现代化、对外稳定性,两全其美。


总结一句话:

用 C 接口隐藏 C++ 实现细节,可以提升库的兼容性、稳定性、跨语言能力,同时保留内部的开发效率。


如果你要构建这种库结构,我可以帮你写一个 最小可用的 C++ 动态库接口设计模板,包括:

  • 头文件(干净的 C API)
  • 实现文件(用 C++ 实现)
  • 编译为 .dll / .so
  • 示例客户端代码

hairpoll.h:

typedef struct hairpoll* hairpoll_t; 
hairpoll_t hairpoll_construct(const char* person); 
void hairpoll_destruct(hairpoll_t poll); 
int32_t hairpoll_add_option(hairpoll_t hairpoll, const char* name, const char* image_url); 
void hairpoll_vote(hairpoll_t hairpoll, int32_t option); 
typedef void (*hairpoll_result_handler_t)(void* client_data, const char* name, int32_t votes, const char* html); 
void hairpoll_tally(const hairpoll_t hairpoll, hairpoll_result_handler_t handler, void* client_data);

你描述的是一个典型的 C 风格 ABI 封装 + C++ 面向对象封装 的库设计方案。这个设计将内部逻辑与外部接口很好地解耦,同时提供了用户友好的 C++ 封装。


核心概念解析

1. C 接口 (ABI-Stable)hairpoll.h

typedef struct hairpoll* hairpoll_t;
  • 这是一个 opaque pointer(不透明指针)。
  • 用户无法知道 struct hairpoll 的内部结构,起到了 封装实现细节 的作用。
hairpoll_t hairpoll_construct(const char* person);
void hairpoll_destruct(hairpoll_t poll);
  • 构造和析构:分配资源,初始化结构体。
int32_t hairpoll_add_option(hairpoll_t, const char*, const char*);
  • 添加候选项(名字 + 图片 URL),返回一个 option ID(整数索引)。
void hairpoll_vote(hairpoll_t, int32_t option);
  • 给对应的选项投票。
typedef void (*hairpoll_result_handler_t)(void*, const char*, int32_t, const char*);
void hairpoll_tally(const hairpoll_t, hairpoll_result_handler_t handler, void* client_data);
  • 通过回调函数报告投票结果(候选人名、票数、HTML格式渲染内容)。
  • client_data 允许客户端传入上下文。

2. C++ 封装类:HairPoll

这个类是为 C++ 用户准备的,封装了 C 接口中的 hairpoll_t

class HairPoll {
public:HairPoll(std::string person);~HairPoll();int addOption(std::string name, std::string imageUrl);void vote(int option);struct Result {std::string name;int votes;std::string html;};std::vector<Result> results() const;private:hairpoll_t _opaque;
};
特点:
  • HairPoll 的实例拥有一个私有 _opaque,内部调用底层 C 函数。
  • 所有成员函数都用 hairpoll_... 函数实现底层功能。
  • results() 封装了 hairpoll_tally 的回调逻辑,填充 std::vector<Result>,对 C++ 用户友好。

优点总结

技术点优势
hairpoll_t 不透明指针隐藏实现细节,保持 ABI 稳定
C 接口可与其他语言绑定,兼容性强
C++ 封装面向对象、易于使用
回调处理结果不依赖特定 UI,灵活扩展
使用 HTML 输出可以支持网页端、图形界面等多种展示方式

你列出的内容是设计 “thin C APIs”(轻量级 C 接口) 时的最佳实践,用来 最大化跨语言兼容性、避免 ABI 问题,同时保持接口简单、稳定、易绑定(例如与 Python、Rust、C#、JavaScript 等语言)。


解释各个点的意义

C89 + const 和 // 注释
  • 使用最老的 ANSI C 标准 (C89) 保证 最大兼容性
  • 允许 const// 注释(这些是后加的扩展,大多数编译器都支持)。
  • 不用 C99inline, bool, 复合字面量等特性,保持最低标准。
函数,但不使用变参函数
  • 使用固定参数列表的函数,避免使用 printf(const char* fmt, ...) 这种变参函数(...)。
  • 因为变参函数不易绑定,跨语言不友好,甚至跨平台 ABI 不稳定。
只使用 C 原始类型int, char, void
  • 不使用 std::string, bool, size_t(除非自己 typedef 一下),保持类型简单明确。
  • 所有参数和返回值类型必须能被其他语言理解。
使用指针:char*, void*, struct*
  • 是 C 的核心手段,用于传递数据、构造对象、不透明结构等。
结构体只前向声明,不暴露内部结构
typedef struct mylib_handle* mylib_t;
  • 保持接口 封装性和 ABI 稳定性,用户不能访问或依赖 struct 的内部布局。
枚举(enum
  • 允许返回或传入状态码、配置项等,且在不同语言中容易绑定。
typedef enum {MYLIB_OK = 0,MYLIB_ERROR = 1,
} mylib_status_t;
函数指针(Callbacks)
  • 回调机制允许“事件驱动”或“数据处理”接口,适合多种用途。
typedef void (*my_callback_t)(void* context, int result);

总结:为什么这么做?

原因解释
ABI 稳定性保证结构体布局、函数调用在不同编译器/平台上行为一致
可移植性强易于绑定到其他语言或通过 FFI(如 Rust 的 extern "C")使用
简洁减少模板、继承、异常等 C++ 特有机制带来的复杂性
可维护性改动内部实现不会破坏已发布的接口

如果你想构建一个稳定的公共 C 接口库(如 SDK、插件框架、跨平台模块),这套风格就是行业标准。例如:

  • SQLite C API
  • libpng, zlib
  • Vulkan SDK
  • FFmpeg 的 libavcodec 系列

这里讲的是 Opaque Types(不透明类型) 在 C 接口中的应用方式 —— 它是构建跨语言/跨 ABI 安全接口的重要技巧之一。


什么是 Opaque Type?

Opaque Type” 就是不公开其内部结构的类型,客户端只能通过指针使用它,而不能访问内部字段。


完全理解!你给出的例子很好地展示了如何用 不透明类型(Opaque Types)C++ 封装实现 来设计一个安全且跨语言友好的库接口。


这段代码整体架构总结:

  1. 外部接口:
typedef struct hairpoll* hairpoll_t;hairpoll_t hairpoll_construct(const char* person);
void hairpoll_destruct(hairpoll_t poll);
void hairpoll_vote(hairpoll_t hairpoll, int32_t option);
  • 对用户隐藏了 hairpoll 的内部结构,只暴露了一个指针类型 hairpoll_t
  • 用户只能通过这些函数操作 hairpoll,无法直接访问其内部数据。

  1. 内部实现:
extern "C" struct hairpoll {template<typename... Args>hairpoll(Args&&... args) : actual(std::forward<Args>(args)...) {}Poll actual;
};
  • 这里 hairpoll 实际上封装了一个真正的 Poll 类实例。
  • 利用模板构造函数实现参数完美转发,初始化内部 Poll

  1. Poll 类定义:
class Poll {
public:Poll(std::string person);struct option {option(std::string name, std::string url);std::string name;std::string url;int votes = 0;};std::string person;std::vector<option> options;
};
  • Poll 是功能具体的 C++ 类,管理投票信息。
  • hairpoll 只是一个封装层,保持 API 简单且 ABI 兼容。

  1. 构造和析构接口:
hairpoll_t hairpoll_construct(const char* person) {return new hairpoll(person);
}void hairpoll_destruct(hairpoll_t poll) {delete poll;
}
  • 通过 newdelete 管理内存,避免用户直接操作内部对象。
  • 保持跨语言边界的安全。

设计亮点

  • 内存安全:客户端不需要知道内存管理细节。
  • ABI 兼容:隐藏复杂 C++ 类型,避免 ABI 崩溃。
  • 跨语言易用:只暴露 C 风格接口,其他语言更容易绑定。
  • 灵活性:内部用现代 C++,API 层保持简单。

**整型类型(Integral types)**的建议很重要,尤其是在跨平台和跨语言的API设计中:


为什么推荐使用 <stdint.h> 中的标准整型?

  • 明确大小int32_t, int64_t 等明确指定位宽,不受平台差异影响。
  • 跨平台兼容性强:保证数据大小一致,防止不同编译器或平台上因 intlong 大小不同导致的bug。
  • ABI稳定性:保持接口二进制兼容,方便不同编译单元、不同语言交互。

示例:

void hairpoll_vote(hairpoll_t hairpoll, int32_t option);
int32_t hairpoll_add_option(hairpoll_t hairpoll, const char* name, const char* image_url);
  • int32_t 来标明 option 的类型,清晰又安全。
  • const char* 用于字符串,标准 C 风格,语言绑定方便。

关于 char 类型:

  • char 作为字符类型是安全且常用的做法,因为字符串就是以 char* 形式传递的。
  • 只要字符串的编码和约定清晰(例如 UTF-8),它们就可以跨语言使用。

总结

  • 接口层尽量避免使用平台相关的类型,统一用 <stdint.h> 定义的类型。
  • 字符串用 const char* 传递。
  • 避免用 short, long 等可能大小不一致的类型。

枚举类型(Enumerations)在接口设计中的使用要点:

typedef enum motu_alignment { heroic_warrior = 0, evil_warrior = 1, 
} motu_alignment_t;int32_t motu_count(int32_t alignment);

说明:

  • 定义枚举类型时,最好给枚举类型取一个 _t 后缀的名字,比如 motu_alignment_t,方便类型识别和维护。
  • 函数接口中一般用标准整型(如 int32_t)来接收枚举值,而不是枚举类型本身。原因是 C 枚举大小在标准中并不严格规定,不同编译器实现可能不一样,使用固定大小的整型可以保证二进制兼容性。
  • 传递的值必须是定义好的枚举值之一,或者是其整型对应值。

推荐改写:

typedef enum motu_alignment { heroic_warrior = 0, evil_warrior = 1, 
} motu_alignment_t;int32_t motu_count(int32_t alignment);

调用时:

int32_t count = motu_count(heroic_warrior);

这样写既保证了类型安全,也保证了 ABI 兼容。


总结一下你展示的错误处理设计思路:


1. C 风格错误处理接口

typedef struct error* error_t;void hairpoll_vote(hairpoll_t hairpoll, int32_t option, error_t* out_error);
const char* error_message(error_t error);
void error_destruct(error_t error);
  • 通过 error_t* out_error 参数返回错误信息。
  • 如果函数执行成功,out_error 置空。
  • 错误用字符串表示,动态分配内存,调用者负责释放。

2. C++ 异常与 C 错误接口桥接

struct Error {Error() : opaque(nullptr) {}~Error() { if (opaque) error_destruct(opaque); }error_t opaque;
};class ThrowOnError {
public:~ThrowOnError() noexcept(false) {if (_error.opaque) {throw std::runtime_error(error_message(_error.opaque));}}operator error_t*() { return &_error.opaque; }
private:Error _error;
};
  • ThrowOnError 是一个辅助类,用于在析构时自动将 C 错误转换成 C++ 异常。
  • ThrowOnError 作为参数传入 C API 函数,自动管理错误生命周期。

3. C++ 调用 C API

void vote(int option) {return hairpoll_vote(_opaque, option, ThrowOnError{});
}
  • 方便 C++ 端直接调用,自动抛异常,无需手动检查错误。

4. 异常转换成错误(Exceptions → Errors)

template<typename Fn>
bool translateExceptions(error_t* out_error, Fn&& fn) {try {fn();} catch (const std::exception& e) {*out_error = new error{e.what()};return false;} catch (...) {*out_error = new error{"Unknown internal error"};return false;}return true;
}
  • 在 C API 的实现端,捕获 C++ 异常并转换成 error_t
  • 保证 C API 不抛异常,保持二进制兼容。

5. 具体示例

void hairpoll_vote(const hairpoll_t poll, int32_t option, error_t* out_error) {translateExceptions(out_error, [&]{if (option < 0 || option >= poll->actual.options.size()) {throw std::runtime_error("Bad option index");}poll->actual.options[option].votes++;});
}
  • 使用 translateExceptions 来保证接口安全,统一错误处理。

总结

  • C API 用 error_t* 返回错误信息,不抛异常。
  • C++ 层用辅助类实现错误与异常的互转,使用方便。
  • 错误消息以字符串形式传递,确保跨语言、跨ABI安全。

**回调(Callbacks)**及其用法:


1. C 风格的回调定义

typedef void (*hairpoll_result_handler_t)(void* client_data,const char* name,int32_t votes,const char* html
);void hairpoll_tally(const hairpoll_t hairpoll,hairpoll_result_handler_t handler,void* client_data,error_t* out_error
);
  • 回调函数接收一个 void* client_data,这是用户传递的上下文指针,方便在回调中访问外部状态。
  • C 接口中传入回调和上下文。

2. 用 C++ lambda 包装回调

std::vector<Result> results() const {std::vector<Result> ret;// 用 lambda 封装将数据添加到 retauto addResult = [&ret](const char* name, int32_t votes, const char* html) {ret.push_back(Result{name, votes, html});};// 适配器 lambda 转换 C 风格回调接口,传入 client_data 作为 addResult 的地址auto callback = [](void* client_data, const char* name, int32_t votes, const char* html) {// 将 void* 还原成 addResult 的类型指针,调用它auto fn = static_cast<decltype(&addResult)>(client_data);(*fn)(name, votes, html);};// 调用 C API,传入 callback 和上下文 addResult 的地址hairpoll_tally(_opaque, callback, &addResult, ThrowOnError{});return ret;
}
  • 这里的核心技巧是:

    • 用 C++ lambda 把结果收集到 ret
    • 因为 C API 要 void* client_data,所以传入 addResult 的地址。
    • 回调中通过强制类型转换拿回 lambda,然后执行。

3. 为什么这样写?

  • 类型安全:lambda 是 C++ 的闭包,可以捕获外部变量,方便状态管理。
  • 兼容 C API:C 函数只能接受 void*,通过传递 lambda 指针做桥接。
  • 易用:C++ 层调用者写起来简单,内部自动做转接。

**符号可见性(Symbol Visibility)**的跨平台控制技巧,作用是:


目的

  • 控制哪些符号(函数、变量)对动态库外部可见,避免符号污染,减少链接时间,保护内部实现细节。

Windows vs Unix/Linux/Mac 区别

  • Windows (MSVC/MinGW)
    使用 __declspec(dllexport) 导出符号,__declspec(dllimport) 导入符号。

    • 编译库时定义 hairpoll_EXPORTS,导出符号。
    • 使用库时则导入符号。
  • Unix/Linux (GCC/Clang)
    使用 __attribute__((visibility("default"))) 标记导出符号。

    • 其它符号默认隐藏(需通过 -fvisibility=hidden 让所有符号默认隐藏)。

代码示例宏定义

#if defined(_WIN32) || defined(__CYGWIN__)#ifdef hairpoll_EXPORTS#ifdef __GNUC__#define HAIRPOLL_EXPORT __attribute__ ((dllexport))#else#define HAIRPOLL_EXPORT __declspec(dllexport)#endif#else#ifdef __GNUC__#define HAIRPOLL_EXPORT __attribute__ ((dllimport))#else#define HAIRPOLL_EXPORT __declspec(dllimport)#endif#endif
#else#if __GNUC__ >= 4#define HAIRPOLL_EXPORT __attribute__ ((visibility ("default")))#else#define HAIRPOLL_EXPORT#endif
#endif

用法示例

#include "visibility.h"HAIRPOLL_EXPORT hairpoll_t hairpoll_construct(const char* person);
HAIRPOLL_EXPORT void hairpoll_destruct(hairpoll_t poll);
  • 这样写保证库导出函数对外可见,避免其他内部符号暴露。

编译提示

  • 在使用 GCC/Clang 编译库时加上编译选项:
-fvisibility=hidden

这样默认隐藏符号,只有带 HAIRPOLL_EXPORT 的函数才导出。


这段代码用来确保 C++ 编译器按照 C 的方式来编译接口,避免名字改编(name mangling),保证 C++ 编写的库能被 C 或其他语言正确调用。


关键点:

#ifdef __cplusplus
extern "C" {
#endif// C API 代码放这里#ifdef __cplusplus
} // extern "C"
#endif
  • extern "C" 告诉编译器用 C 的函数命名规则(不改编符号名)
  • #ifdef __cplusplus 是为了兼容 C 和 C++,只有用 C++ 编译时才加 extern "C"

作用总结

  • 防止 C++ 编译器自动给函数名加前后缀(name mangling)
  • 使库接口对 C 和其他语言都保持兼容
  • 是写跨语言库 API 的必备技巧

跨语言或库设计中管理 对象生命周期(Lifetime)和 所有权(Ownership)的最佳实践,以下是要点解析:


核心原则解释:

1. Keep it simple: construct/destruct
  • 提供明确的构造和析构函数,比如:

    hairpoll_t hairpoll_construct(const char* person);
    void hairpoll_destruct(hairpoll_t poll);
    
  • 客户端调用构造函数创建资源,调用析构函数释放资源,清晰明了。

2. Always use RAII on the client side
  • 在 C++ 客户端中封装裸指针为类成员,利用构造函数/析构函数自动管理资源释放:

    class HairPoll {
    public:~HairPoll() { hairpoll_destruct(_opaque); }
    private:hairpoll_t _opaque;
    };
    
3. Can use refcounting internally or externally
  • 在库内部或客户端中都可以使用 std::shared_ptr 管理共享资源,避免手动计数或内存泄漏:

    std::shared_ptr<Poll> shared_poll;
    
  • 但注意不要将 shared_ptr 直接暴露在 C 接口中!

4. For callbacks, you can use the stack
  • 在回调场景中,通常用栈变量即可(如 lambda 捕获、函数指针传参),避免动态分配:

    auto callback = [](void* data, ...) {auto fn = static_cast<CallbackFn*>(data);(*fn)(...);
    };
    hairpoll_tally(..., callback, &callbackFn);
    

你应该掌握的设计思维:

  • 对外暴露 最小的生命周期控制接口(Create/Destroy)
  • 对内使用 C++ 强大的资源管理(RAII, 智能指针)
  • 保持 ABI 安全(不暴露 STL、虚函数)
  • 将生命周期管理责任交给最终拥有资源的一方(通常是调用者)

如何用 C 接口与其他语言(如 Python)进行交互,也就是 FFI(Foreign Function Interface) 的核心概念和实践方式。以下是逐点解释:


核心概念:Foreign Function Interface (FFI)

允许一种语言中的代码调用另一种语言编写的函数。

  • 在实际工程中,C 语言 ABI 是跨语言互操作的事实标准,因为它:

    • 简单(不含虚函数、模板等复杂特性)
    • 稳定(ABI 不容易变)
    • 几乎所有语言都能调用 C 函数

C ABI 的跨语言支持示例(你提到的):

  • Python
  • Java
  • JavaScript (via WebAssembly or Native Modules)
  • .NET (C#, F#, VB.NET)
  • Matlab
  • Common Lisp
  • Ruby
  • OCaml
  • 甚至很多嵌入式 DSL…

实例讲解:Hairpoll 库在 Python 中的使用

步骤 1:用 ctypes 加载动态链接库
from ctypes import *
lib = CDLL("libhairpoll.dylib")  # 加载动态库(Windows: .dll, Linux: .so)
步骤 2:指定每个函数的参数类型和返回值
lib.hairpoll_construct.restype = c_void_p
lib.hairpoll_construct.argtypes = [c_char_p, c_void_p]  # 参数是 const char*, void*lib.hairpoll_add_option.restype = c_int
lib.hairpoll_add_option.argtypes = [c_void_p, c_char_p, c_char_p, c_void_p]
步骤 3:调用库函数
hairpoll = lib.hairpoll_construct(b"Stefanus Du Toit", None)
skeletor = lib.hairpoll_add_option(hairpoll, b"Skeletor", b"<url>", None)
lib.hairpoll_vote(hairpoll, skeletor, None)
步骤 4:注册回调函数(handler)
def print_result(client_data, name, votes, html):print(name, votes)# 创建函数指针类型
hairpoll_result_handler = CFUNCTYPE(None, c_void_p, c_char_p, c_int, c_char_p)# 注册并调用
lib.hairpoll_tally(hairpoll, hairpoll_result_handler(print_result), None, None)
步骤 5:清理资源
lib.hairpoll_destruct(hairpoll)

为什么这样设计是最佳实践?

  • 使用 C 接口 + typedef struct* opaque 类型 → 隐藏实现细节,稳定 ABI。
  • 所有函数使用 C 原始类型作为参数 → 容易用 ctypesJNIP/Invoke 等绑定。
  • 使用 回调函数指针 → 支持事件式交互,如 tally 后返回结果。
  • 可以从 C++ 中导出复杂逻辑,而客户端只需要链接 .dll/.so 并调用函数。

结论:你应该掌握的要点

要点理解
用 C API 做接口保证跨语言兼容
函数参数类型要简单只用 int, char*, void*, 不用 std::string
回调用函数指针CFUNCTYPE 等机制包装
ctypes 调用 DLLPython 最常见做法
不暴露类/模板防止 ABI 崩溃

总结如下:


Hourglass 模式总结(C++ ↔ C89 ↔ C++)

架构图(“Hourglass”沙漏模型):

C++ 高层封装(客户端使用现代 C++)
🔸 C89 兼容的 C 接口(thin API)
内部用现代 C++ 实现(支持类、RAII、STL 等)

为什么要这样做?

目的原因
避免 ABI 问题C++ ABI 不稳定,各编译器/版本不兼容(如 std::string、虚函数)
隐藏实现细节Opaque 指针 (typedef struct*) 不暴露对象结构
让库易于绑定到其他语言C ABI 是所有语言 FFI 的通用桥梁
防止 sneaky dependencies避免 STL 或第三方库通过头文件“泄露”到客户端
仍然能在内部用现代 C++ 开发不限制开发效率与功能,仅暴露的是“稳定壳”

特点归纳:

  • 外部只暴露 C89 接口(primitive 类型、指针、函数)
  • 所有内部对象通过 opaque 指针管理生命周期
  • 错误处理用 error_t + C 风格返回值,C++ 端则用异常封装
  • 回调通过 function pointers + void* client_data 实现
  • 可跨语言绑定:如 Python、C#、Rust、Java、Matlab 等
  • C++ 封装类对 C API 提供面向对象封装,保持易用性

结论:

你设计的是一个 兼顾跨平台稳定性、C++ 强大功能和多语言互操作性 的库结构模式,适用于构建面向外部的、高质量的可复用组件库(如 SDK、插件系统、API 库等)。


http://www.hkcw.cn/article/NCOAtZjVia.shtml

相关文章

<3>, 常用控件

目录 一、控件概述 二、QWidget 核心属性 1&#xff0c; 核心属性列表 2&#xff0c;enabled 3&#xff0c;geometry 4&#xff0c;windowTitle 5&#xff0c;windowIcon 6&#xff0c;windowOpacity 7&#xff0c;font 8&#xff0c;toolTip 9&#xff0c;focusPol…

基于微服务架构的社交学习平台WEB系统的设计与实现

设计&#xff08;论文&#xff09;题目 基于微服务架构的社交学习平台WEB系统的设计与实现 摘 要 社交学习平台 web 系统要为学习者打造一个开放、互动且社交性强的在线教育环境&#xff0c;打算采用微服务架构来设计并实现一个社交学习平台 web 系统&#xff0c;以此适应学…

uboot启动流程分析之uboot启动阶段

uboot启动可分为汇编语言执行和C语言执行两个阶段&#xff0c;两个阶段以_main函数为分界。 uboot第一阶段由_start (arch/arm/lib/vectors.S)进入&#xff0c;然后跳转到reset(arch/arm/cpu/armv7/start.S)函数, reset函数进行设置CPU运行模式&#xff0c;关闭中断等一系列CP…

QT学习教程(十一)

​​​​​​实现文件菜单&#xff08;Implementing the File Menu&#xff09; 我们实现与文件菜单有关的槽函数和相关的私有函数&#xff0c;以使文件菜单可以工作&#xff0c;同时管理最近打开文件列表。 void MainWindow::newFile(){if (okToContinue()) { spreadsheet-…

【MATLAB代码】制导方法——平行接近法引导,二维环境,动态目标|附代码的下载链接

平行接近法是一种导引方法&#xff0c;其目标是保持目标瞄准线在空间中的平行移动。 本文所述的代码实现了二维平行接近法导引的动态仿真&#xff0c;模拟导弹追踪移动目标的过程。通过实时调整导弹速度方向&#xff0c;确保其逐渐逼近目标&#xff0c;最终在设定距离内完成拦截…

解决win自动重启(自用,留链接)

2025-05-30修改&#xff0c;如果再出现重启回来修改。没动静就是没事了 1、依据系统事件查看器确认错误代码 事件查看器步骤 &#xff08;上图没啥用&#xff09; 下图错误代码&#xff0c;如果原因一致 2、禁用“用户体验改善计划”点击此处步骤

AI入门示例

市面上有很多AI大模型&#xff0c;这里以 智谱的大模型 为示例 1.先要注册智谱AI开放平台 2.注册成功后&#xff0c;会赠送3个月的免费额度&#xff0c;如下 3.然后去控制台&#xff0c;创建一个API KEY 4.接着就可以开始写代码了 提前导入包&#xff1a; openai 示例1&…

仿真每日一练 | 静力学分析与动力学分析的区别

很多有限元初学者都在纠结一个问题&#xff0c;就是静力学分析和动力学分析有什么区别&#xff0c;今天以一个时变载荷的例子&#xff0c;带大家领悟其中奥妙。 首先来了解一下二者的物理方程&#xff1a; 静力学所解决的问题&#xff1a;KxF 动力学所解决的问题&#xff1…

Python数据处理中的查找和排序使用详解

概要 在编程中,查找和排序是两个常见且重要的操作,几乎所有数据处理任务都会涉及到这两个内容。Python 作为一门功能强大的编程语言,为我们提供了多种查找和排序的方式,不仅包括内置函数,还支持通过算法来自定义操作。本文将详细介绍 Python 中常用的查找与排序方法,包括…

面试-【搜索引擎】

elasticsearch分布式架构原理 index -> type -> mapping -> document -> field elasticsearch 7.x取消了type mapping类似于schema信息 document代表一行数据 field代表一个字段值 elasticsearch读写流程底层剖析 (1)es写数据过程 1)客户端选择一个 node 发送…

引领机器人交互未来!MANUS数据手套解锁精准手部追踪

MANUS数据手套为机器人技术带来高精度手部追踪&#xff0c;助力实现人与机器的自然交互&#xff01;近年&#xff0c;越来越多客户希望利用这项技术精准操控机械臂、灵巧手和人形机器人&#xff0c;不断提升设备的智能化水平和交互体验。 MANUS数据手套是高精度人机交互设备&am…

国标GB28181设备管理软件EasyGBS实现生产全流程可视化监控与精细化管理

一、引言​ 在全球经济一体化与工业化浪潮的推动下&#xff0c;市场竞争愈发激烈&#xff0c;企业想要在行业中占据优势&#xff0c;实现生产流程的高效管理与精准把控成为关键。生产流程不仅是产品质量的生命线&#xff0c;更是提升生产效率、保障经济效益的核心环节。国标GB…

数据治理系统是什么?数据治理工具有什么用?

目录 一、数据治理系统是什么&#xff1f; 二、数据治理系统的重要性 1. 保障数据质量 2. 确保数据安全 3. 促进数据共享与协作 三、常见的数据治理工具及其特点 1. 数据质量管理工具 2. 数据集成工具 3. 元数据管理工具 四、数据治理工具有哪些作用&#xff1f; 1.…

AD9361 的工作原理

AD9361 由Analog Devices 公司设计并产出的。该器件集成了相当多的滤波器&#xff0c;频率合成器&#xff0c;数字处理模块以及接收端自动增益控制模块等。拥有上千个寄存器可供配置&#xff0c;通过对寄存器存入数值的更改可对该器件进行工作控制&#xff0c;正是因为此芯片配…

AI Agent开发入门笔记(1)

目录 1️⃣ 选择框架2️⃣开发操作导入Python库创建功能函数装载环境变量创建Agent运行Agent 学习参考资料&#xff1a; 微软 AI Agents for Beginners 代码仓库 1️⃣ 选择框架 semantic-kernel开发框架 导入库创建功能函数&#xff08;Agent 要完成什么功能&#xff09;…

实验设计与分析(第6版,Montgomery)第5章析因设计引导5.7节思考题5.11 R语言解题

本文是实验设计与分析&#xff08;第6版&#xff0c;Montgomery著&#xff0c;傅珏生译) 第5章析因设计引导5.7节思考题5.11 R语言解题。主要涉及方差分析&#xff0c;正态假设检验&#xff0c;残差分析&#xff0c;交互作用图。 dataframe<-data.frame( densityc(570,565,…

Cadence学习笔记终章

目录 01 | 引 言 02 | 文章汇总 03 | 结 语 01 | 引 言 在历时一个半月后&#xff0c;终于更新完Cadence的原理图与PCB的设计流程&#xff1b; 本篇文章主要是将全部的Cadence学习文章汇总起来&#xff0c;以供朋友们以及自己日后进行翻阅。 02 | 文章汇总 文章从上至下的…

第100+41步 ChatGPT学习:R语言实现误判病例分析

本期是《第33步 机器学习分类实战&#xff1a;误判病例分析》的R版本。 尝试使用Deepseek-R1来试试写代码&#xff0c;效果还不错。 下面上R语言代码&#xff0c;以Xgboost为例&#xff1a; # 加载必要的库 library(caret) library(pROC) library(ggplot2) library(xgboost)…

实验设计与分析(第6版,Montgomery)第5章析因设计引导5.7节思考题5.8 R语言解题

本文是实验设计与分析&#xff08;第6版&#xff0c;Montgomery著&#xff0c;傅珏生译) 第5章析因设计引导5.7节思考题5.8 R语言解题。主要涉及方差分析&#xff0c;正态假设检验&#xff0c;残差分析&#xff0c;交互作用图。 (a) dataframe<-data.frame( Lightc(580,568…

OptiStruct实例:消声器前盖ERP分析(2)RADSND基础理论

13.2 Radiated Sound Output Analysis( RADSND ) RADSND 方法通过瑞利积分来求解结构对外的辐射噪声。其基本思路是分为两个阶段&#xff0c;如图 13-12 所示。 图13-12 结构辐射噪声计算示意图 第一阶段采用有限元方法&#xff0c;通过频响分析(模态叠加法、直接法)工况计算结…