CppCon 2014 学习:How to call C libraries from C++

int listener = socket(AF_INET6, SOCK_STREAM, 0);
if (listener == -1) throw std::system_error(errno, std::system_category());
try {
    if (listen(listener, 1)) throw std::system_error(errno, std::system_category());
    sockaddr_in6 listeningAddress;
    socklen_t listeningAddressLength = sizeof(listeningAddress);
    if (getsockname(listener, reinterpret_cast<sockaddr*>(&listeningAddress),
                    &listeningAddressLength))
        throw std::system_error(errno, std::system_category());
    char listeningAddressString[INET6_ADDRSTRLEN];
    if (!inet_ntop(AF_INET6, &listeningAddress.sin6_addr, listeningAddressString,
                   sizeof(listeningAddressString)))
        throw std::system_error(errno, std::system_category());
    int listener = socket(AF_INET6, SOCK_STREAM, 0);
    if (listener == -1) throw std::system_error(errno, std::system_category());
    try {
        if (listen(listener, 1)) throw std::system_error(errno, std::system_category());
        sockaddr_in6 listeningAddress;
        socklen_t listeningAddressLength = sizeof(listeningAddress);
        if (getsockname(listener, reinterpret_cast<sockaddr*>(&listeningAddress),
                        &listeningAddressLength))
            throw std::system_error(errno, std::system_category());
        char listeningAddressString[INET6_ADDRSTRLEN];
        if (!inet_ntop(AF_INET6, &listeningAddress.sin6_addr, listeningAddressString,
                       sizeof(listeningAddressString)))
            throw std::system_error(errno, std::system_category());
    }
}

如何在 C++ 中调用 C 库函数

本例,展示了如何在 C++ 程序中使用 C 标准网络库(如 等)并用 C++ 异常进行错误处理。

代码用途概览:

该代码段的功能是:

  1. 创建一个 IPv6 socket;
  2. 启动监听;
  3. 获取监听 socket 的本地地址;
  4. 打印地址为字符串形式(如 ::1);
  5. 所有 C 库返回错误时,使用 C++ 的 std::system_error 抛出异常。

关键概念解释:

C 函数 作用
socket(AF_INET6, SOCK_STREAM, 0) 创建一个 IPv6 TCP socket
listen(listener, 1) 开始监听 socket
getsockname(...) 获取 socket 的本地绑定地址
inet_ntop(...) 把二进制 IP 地址转为可读字符串

错误处理方式:

传统 C 的做法是检查返回值并手动处理 errno
在 C++ 中,你可以通过:

if (some_c_function() == -1)
    throw std::system_error(errno, std::system_category());

这样你可以使用 try/catch 语法来管理错误。

类型转换说明:

reinterpret_cast<sockaddr*>(&listeningAddress)

这是因为 getsockname 的参数要求的是 sockaddr*,但我们使用的是 sockaddr_in6 类型,需要强制转换。

总结重点:

  • 可以直接在 C++ 中调用 C 标准库函数;
  • 要小心使用 reinterpret_cast 来匹配参数;
  • 推荐使用 C++ 的 来统一处理 errno 风格的错误;
  • 所有这些调用都建立在 C 头文件的基础上,但通过 C++ 语义让代码更现代、更安全。

为什么不要使用面向对象(OO)的框架?

作者的观点强调经济成本高,总结为以下几点:

面向对象框架的“高昂代价”:

成本 原因
设计成本高 OO 框架通常需要复杂的抽象层次,设计难度大。
文档成本高 框架必须详细说明类、继承关系、接口等,写清楚很花时间。
学习成本高 初学者或新团队成员难以理解和掌握框架结构。
迁移/集成成本高 把旧代码适配到新框架可能涉及重构大量逻辑。

结论:

作者建议避免强制使用 OO 框架,尤其是在性能、维护、迭代成本敏感的项目中。相反,推荐使用:

  • 更轻量级的编程模型(如函数式、组合优先);
  • 透明的数据结构;
  • 简单的模块化设计。

编写设计规则,并将其统一应用到整个库中

理解如下:

Match the original library

Type for type, function for function, except for the exceptions.

在重新封装或绑定一个已有库时:

  • “类型对类型”:新接口中的类型设计应直接对应原库中的每个类型(例如,如果原库有结构体 Foo,你也应该有一个封装后的 Foo 类型或类)。

  • “函数对函数”:每个原始函数应有一个等价的新函数,保持功能一致。

  • “除了异常(exceptions)”:唯一可以不直接映射的是异常机制,特别是跨语言或二进制边界时(因为异常处理机制可能不兼容)。在这种情况下,应使用错误码或返回结构代替异常。

为什么要这样做?

  • 保持语义一致性:用户可以轻松理解封装后的库,因为它与原始库行为一致。
  • 简化维护:更容易同步和调试。
  • 跨语言友好:异常处理改为错误码或回调,更适合 C 接口或 FFI。

示例:

如果原始 C++ 库有:

struct Widget {
    std::string name;
};
int do_work(Widget w);

封装后的 C API 应为:

typedef struct widget* widget_t;
int do_work(widget_t w, error_t* out_error);

注意:不要直接暴露 std::string,不要用 C++ 异常 —— 使用 error_t 等替代。

Design rules for names(命名设计规则)

在为库设计接口(特别是封装 C 接口或编写适配层)时,应遵循以下命名规则,以保证一致性、清晰性和安全性:

1. 选择简洁的命名空间(namespace)

  • 使用一个简短有力的命名空间名(例如 Po7),避免冲突,同时易记。

2. 沿用原库中的命名

  • 如果原始库中有某个函数或类型的名称,就在命名空间中复用它,以保持熟悉感。
  • 例外: 如果原库中的名字是宏(MACRO),用小写字母命名以避免命名冲突。

3. 命名语义惯例

  • unique_* → 表示拥有资源所有权的封装类型(如 unique_ptr),暗示自动释放。
  • *_cast → 表示是一个不安全或类型转换操作,提示使用时应谨慎(类似 C++ 中的 static_castreinterpret_cast 等)。

4. 遵循原库风格

  • 除了特殊前缀规则外,其他命名应尽量与原库风格保持一致(例如驼峰式、下划线等),减少使用者的学习成本。

示例:

假设你在封装一个叫 libfoo 的 C 库,有函数:

FOO_HANDLE foo_create();
void foo_destroy(FOO_HANDLE h);

你在 C++ 中可设计为:

namespace po7 {
    using handle = FOO_HANDLE;

    unique_handle unique_create();      // 拥有资源,自动释放
    void destroy(handle h);             // 显式释放资源
    int64_t unsafe_cast(handle h);      // 明确表示是转换操作
}

Design rules for types(类型设计规则)

1. 无单位数值使用现有类型

  • 对于没有单位的数字(例如计数、索引),直接用已有的基本数值类型(如 int, float 等),不必新建类型。

2. 数值类型表示特殊含义时,用封装类型

  • 如果库里使用某个数字类型代表特殊的含义(比如“时间戳”、“颜色值”等),应创建一个包装类型(wrapper),为它定义合理的操作符重载(加减乘除、比较等),以增强类型安全和代码可读性。

3. 尽量使用已有结构体类型

  • 对于库中已有的结构体,直接重用它们,不必重新定义。
  • 可以通过 using 声明把它们引入到你的命名空间,方便使用:
using library::ExistingStruct;

4. 必要时使用模板细化类型

  • 如果原库中某些结构可以被模板化以增强通用性或复用性,则进行模板封装。

5. 为资源创建所有权类型

  • 对于需要管理生命周期的资源(文件句柄、内存指针等),创建专门的所有权类型(如 unique_*,实现 RAII,防止资源泄漏。

例子:

  • 单纯的计数器直接用 int32_t
  • 时间戳用:
struct Timestamp {
    int64_t value;
    // 重载加减等操作符
};
  • 对已有结构体 struct Point { float x, y; };,直接
using library::Point;
  • 对文件资源用
class unique_file {
  FILE* fp;
  // 析构时自动 fclose
};

理解!设计规则关于类型转换的总结如下:

Design rules for conversions(转换设计规则)

1. 包装和拆包

  • 包装(Wrap)
    Wrap(unwrapped) 把一个裸类型包进封装类型。
    例如:Wrap(foo_raw) 返回 Foo 类型的对象。

  • 拆包(Unwrap)
    Unwrap(wrapped) 从封装类型中取出裸类型。
    例如:Unwrap(foo) 返回内部的裸指针或值。

2. 所有权管理(Seize & Release)

  • 夺取所有权(Seize)
    Seize(released) 从原始资源夺取所有权,封装成独占所有权类型。
    例如:Seize(FILE*)

  • 释放所有权(Release)
    Release(seized) 将封装的所有权类型释放,得到裸资源。
    例如:Release(unique_file) 返回 FILE*

3. 安全构造和显式转换(Make)

  • Make(parameters...) 进行安全的显式构造或转换。
  • 适合创建结构体或需要复杂参数转换的类型。

4. 不安全转换(*_cast 函数)

  • 对于不安全或者可能失败的转换,使用命名为 xxx_cast 的函数。
  • 明确告知调用者这里存在风险或转换不是类型安全的。

举例说明

// 假设 Foo 是包装类型,FooRaw 是裸类型

Foo foo = Wrap<Foo>(foo_raw);  // 包装
FooRaw raw = Unwrap(foo);      // 拆包

unique_file uf = Seize<unique_file>(fptr); // 夺取所有权
FILE* fptr2 = Release(uf);                 // 释放所有权

MyStruct s = Make<MyStruct>(param1, param2);  // 安全构造

// 不安全转换示例
Bar* bar = bar_cast(foo);  // 明确表示转换有风险

设计参数规则的重点是:

  • 参数保持与原库函数一致,方便匹配和调用。
  • 跳过输出参数(通常是通过指针传出的结果),避免复杂的指针操作。
  • 传递参数时用值传递或引用传递,不用裸指针,提高类型安全和易用性。
  • 对于封装的类型,传递封装类型,如果涉及所有权转移,则传递所有权类型(比如智能指针封装)。
  • 如果参数的类型会影响函数行为,考虑用模板重载函数,提高灵活性。
  • 为函数提供合理的默认参数和重载,简化调用。

设计返回值规则要点:

  • 返回值类型保持和库函数相同,保证接口一致性。
  • 错误处理改为抛异常,不用通过返回值或输出参数传递错误信息,这样更符合现代C++习惯。
  • 把库函数的输出参数改成函数的返回值,更直观简洁。
  • 对于类型安全已经覆盖的冗余输出,可以省略,避免重复。
  • 返回封装类型或所有权类型,保持封装一致性和安全性
  • 函数有多个输出时,用 std::tuple 返回,顺序为:先库函数原始返回值,然后是所有输出参数按原顺序排列,方便解构和调用。
auto listener = Po7::socket<Po7::af_inet6>(Po7::sock_stream, Po7::socket_protocol_t());
Po7::listen(*listener, 1);
std::cout << "Listening on “ 
          << Po7::Make<std::string>(Po7::getsockname(*listener)) << "\n";
auto acceptedTuple = Po7::accept(*listener);
auto& connectedSocket = std::get<0>(acceptedTuple);
auto& connectedAddress = std::get<1>(acceptedTuple);
std::cout << "Accepted a connection from “ 
          << Po7::Make<std::string>(connectedAddress) << "\n";
Po7::close(std::move(listener));
Po7::send(*connectedSocket, greeting);
char buffer[256];
while (std::size_t receivedLength = Po7::recv(*connectedSocket, buffer))
    std::cout.write(buffer, static_cast<std::streamsize>(receivedLength));
Po7::close(std::move(connectedSocket));
auto listener = Po7::socket<Po7::af_inet6>(Po7::sock_stream, Po7::socket_protocol_t());
Po7::listen(*listener, 1);
std::cout << "Listening on " << Po7::Make<std::string>(Po7::getsockname(*listener)) << "\n";
auto acceptedTuple = Po7::accept(*listener);
auto& connectedSocket = std::get<0>(acceptedTuple);
auto& connectedAddress = std::get<1>(acceptedTuple);
std::cout << "Accepted a connection from " << Po7::Make<std::string>(connectedAddress) << "\n";
Po7::close(std::move(listener));
Po7::send(*connectedSocket, greeting);
char buffer[256];
while (std::size_t receivedLength = Po7::recv(*connectedSocket, buffer))
    std::cout.write(buffer, static_cast<std::streamsize>(receivedLength));
Po7::close(std::move(connectedSocket));

当然,下面是你示例代码中用到的 Po7 命名空间对应的函数列表和简要说明:

// 创建套接字,使用协议族模板参数(如 af_inet6)
auto listener = Po7::socket<Po7::af_inet6>(Po7::sock_stream, Po7::socket_protocol_t());

// 监听套接字,传入引用,不用指针
Po7::listen(*listener, 1);

// 获取套接字绑定的地址信息,返回系统类型,使用 Make 转换成 std::string
auto address = Po7::getsockname(*listener);
auto addressStr = Po7::Make<std::string>(address);

// 接受一个连接,返回元组(套接字,地址)
auto acceptedTuple = Po7::accept(*listener);

// 关闭套接字,传递所有权(移动语义)
Po7::close(std::move(listener));

// 发送数据,传入引用套接字和数据
Po7::send(*connectedSocket, greeting);

// 接收数据,传入引用套接字和缓冲区,返回接收长度
auto receivedLength = Po7::recv(*connectedSocket, buffer);
  
// 关闭已连接套接字,传递所有权
Po7::close(std::move(connectedSocket));

总结:

函数名 功能描述 参数类型 返回类型
Po7::socket 创建套接字 模板参数协议族,协议类型 所有权类型(智能指针封装套接字)
Po7::listen 监听套接字 套接字引用,监听队列长度 void,异常抛出错误
Po7::getsockname 获取套接字地址 套接字引用 套接字地址类型
Po7::Make 类型转换,包装成标准类型 底层类型 T(如 std::string
Po7::accept 接受连接,返回新套接字和地址的元组 套接字引用 std::tuple<套接字类型, 地址类型>
Po7::close 关闭套接字,释放资源 套接字所有权类型(移动语义) void,异常抛出错误
Po7::send 发送数据 套接字引用,数据缓冲区 void,异常抛出错误
Po7::recv 接收数据 套接字引用,缓冲区指针 接收长度,零表示连接关闭

这些函数都遵守设计规则,比如:

  • 参数以引用或所有权类型传递,避免裸指针。
  • 异常代替错误码。
  • 多返回值用 std::tuple
  • 类型安全,显式转换。

这段内容讲的是如何用一套类型安全、封装良好的 C++ 机制,把传统的 C 库(如 socket API)自动化地封装起来。具体来说:

核心思想

  • 自动化封装: 用模板和类型映射,让电脑帮你机械式地完成函数包装工作,但前提是你得先提供足够多的元信息(比如类型映射、资源释放函数、参数区分等)。
  • 类型安全与资源管理: 通过自定义的包装类型 PlusPlus::Boxed 以及 std::unique_ptr(带自定义删除器)来管理底层资源(如套接字),防止资源泄漏。
  • 错误处理: 通过 ThrowErrorFromErrno() 把系统调用的错误码转换成异常,统一错误处理逻辑。
  • 模板分离领域: 使用枚举 socket_domain_t 定义协议族,配合模板 socket_in_domain,使得不同协议族的套接字类型区分明确。

代码结构解析

  • 枚举封装

    enum class socket_domain_t : int {};
    template <> struct Wrapper<socket_domain_t> : PlusPlus::EnumWrapper<socket_domain_t> {};
    const socket_domain_t af_inet = socket_domain_t(AF_INET);
    // 其他协议族
    

    用强类型枚举封装了 C 的协议族宏,避免魔法数字。

  • 套接字类型定义和模板封装

    struct SocketTag { ... };
    using socket_t = PlusPlus::Boxed<SocketTag>;
    
    template <socket_domain_t domain>
    struct SocketInDomainTag { ... };
    
    template <socket_domain_t domain>
    using socket_in_domain = PlusPlus::Boxed<SocketInDomainTag<domain>>;
    

    Boxed 类型包装底层整数(文件描述符),不同域用不同类型区分。

  • 资源释放机制

    struct SocketDeleter { 
        using pointer = PlusPlus::PointerToValue<socket_t>;
        void operator()(pointer s) const; // 实现关闭套接字
    };
    
    using unique_socket = std::unique_ptr<const socket_t, SocketDeleter>;
    
    template <socket_domain_t domain>
    struct SocketInDomainDeleter {
        using pointer = PlusPlus::PointerToValue<socket_in_domain<domain>>;
        void operator()(pointer s) const { SocketDeleter()(s); }
        operator SocketDeleter() const { return SocketDeleter(); }
    };
    
    template <socket_domain_t domain>
    using unique_socket_in_domain = std::unique_ptr<const socket_in_domain<domain>, SocketInDomainDeleter<domain>>;
    

    通过智能指针和自定义删除器自动关闭套接字,防止忘记关闭。

  • 函数封装示例

    unique_socket socket(socket_domain_t domain, socket_type_t type, socket_protocol_t protocol) {
        return Invoke(Result<unique_socket>() + FailsWhenFalse(), 
                      ::socket, 
                      In(domain, type, protocol), 
                      ThrowErrorFromErrno());
    }
    
    template <socket_domain_t domain>
    unique_socket_in_domain<domain> socket(socket_type_t type, socket_protocol_t protocol) {
        return domain_cast<domain>(socket(domain, type, protocol));
    }
    
    • Invoke 是自动调用底层 C 函数的模板,带有调用结果包装、错误检测和异常转换。
    • 重载模板方便根据协议族调用。

总结

这套设计:

  • 通过模板和包装类型,清晰地区分不同的协议族和资源类型。
  • 利用智能指针和删除器管理资源生命周期。
  • 统一错误处理,抛出异常代替错误码。
  • 用元信息辅助自动生成封装代码,极大减少手写工作,避免出错。

你可能感兴趣的:(CppCon,学习,c语言,c++)