```Zig / C++ 互操作```
Zig / C++ Interop

原始链接: https://tuple.app/blog/zig-cpp-interop

## Zig 和 C++ 之间的互操作性 本文详细介绍了一种在 Zig 和 C++ 代码库之间实现无缝数据交换的策略,允许将一种语言的类型嵌入到另一种语言的结构体/类中。核心挑战是避免跨语言的完整类型定义,而是专注于大小和对齐方式。 该解决方案利用一个宏 `SIZED_OPAQUE`,它创建一个具有指定大小和对齐方式的不透明类型。Zig 和 C++ 都可以然后在编译时验证这些值,从而确保内存布局兼容性。这允许将 `std::shared_ptr` 之类的类型嵌入到 Zig 结构体中,并将 Zig 类型嵌入到 C++ 类中,而无需暴露内部细节。 数据传输依赖于指针和自定义函数来管理所有权(例如移动 `shared_ptr` 实例)。为了简化 C++ 代码处理不透明类型,引入了一个 `DEFINE_OPAQUE_CONCRETE` 宏。该宏生成在 Zig 使用的不透明指针与其具体的 C++ 对应项之间的转换函数,从而减少了冗长且容易出错的类型转换。这种方法提高了与共享指针和其他复杂 C++ 类型交互时的代码可读性和可维护性。 作者通过示例演示了这一点,包括嵌入 Zig 的 `std.http.Client` 和管理 `shared_ptr` 实例,强调了根据 Zig 优化级别调整不同类型大小的灵活性。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Zig / C++ 互操作 (tuple.app) 9 分,由 simonklee 发表于 2 小时前 | 隐藏 | 过去 | 收藏 | 讨论 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

Note: this blog post is cross-posted from my personal blog

I’ve been writing Zig and C++ that have to talk to each other. I want both languages to be able to store data types from the other in their own structs/classes.

const c = @cImport({ @cInclude("cppstuff.h"); });
const MyZigType = struct {
    foo: c.SharedPtrFoo,
};
#include <zigstuff.h>
class MyCppType {
    ZigFoo foo;
};

Keep in mind, I don’t want to just define all my Zig types as extern types. I want to use existing types from the standard library and be able to embed those inside my C++ types. I don’t want my choice of programming language to limit where I can put things in memory.

When you want to embed a type, you need its definition, but you don’t actually need the full definition. You just need the size/alignment. That tells the compiler where to offset it into the container type and how much space to reserve for it. In addition, both Zig and C++ can verify type sizes and alignment at compile time. So, we can replace needing the full type definition with a simple macro that provides an opaque type with the same size/alignment, and have the “home language” for that type verify it’s correct at compile time. Here’s such a macro:

#define SIZED_OPAQUE(name, size, align)                  \
    typedef struct {                                     \
        _Alignas(align) unsigned char _[size];           \
    } __attribute__((aligned(align))) name;              \
    enum { name##Size = size, name##Align = align }

// Allows Zig to include fields that store a shared_ptr<Foo>
SIZED_OPAQUE(SharedPtrFoo, 8, 8)

// Allows C++ to include fields that store a zig-native Foo struct
SIZED_OPAQUE(ZigFoo, 120, 4)

And both sides can verify the sizes at compile-time like this:

const c = @cImport({@cInclude("thestuff.h")});
comptime {
    if (@sizeOf(ZigFoo) != c.ZigFooSize) {
        @compileError(std.fmt.comptimePrint("define ZigFoo size as: {}", .{@sizeOf(ZigFoo)}));
    }
    if (@alignOf(ZigFoo) != c.ZigFooAlign) {
        @compileError(std.fmt.comptimePrint("define ZigFoo align as: {}", .{@alignOf(ZigFoo)}));
    }
}
static_assert(sizeof(SharedPtrFoo) == sizeof(std::shared_ptr<Foo>));
static_assert(alignof(SharedPtrFoo) == alignof(std::shared_ptr<Foo>));

One case where I’m using this is to store an instance of Zig’s std.http.Client in a C++ class. The size of that changes depending on the optimization mode, which looks like this:

#if defined(ZIG_OPTIMIZE_DEBUG)
    SIZED_OPAQUE(HttpZig, 81152, 8);
#elif defined(ZIG_OPTIMIZE_SMALL)
    SIZED_OPAQUE(HttpZig, 81136, 8);
#elif defined(ZIG_OPTIMIZE_SAFE)
    SIZED_OPAQUE(HttpZig, 81144, 8);
#else
    #error ZIG_OPTIMIZE_ not defined
#endif

Maybe at some point I’ll make a build step that generates this info, but this is a simple starting point. Plus, it’s nice to see the actual sizes and get notified when they change.

Once you have the types, how do you use them? The short answer is pointers. For example, if you want to pass a shared_ptr to Zig, you need to pass a pointer to the shared pointer, like this:

export fn takeMyString(from_cpp: *c.SharedPtrStdString) void {
    // DON'T do this: you can't just copy a shared ptr without asking C++ for permission first
    // const bad_boi: c.SharedPtrStdString = from_cpp.*;

    // DO this: define methods to play with your C++ types
    var my_string: c.SharedPtrStdString = undefined;
    c.shared_ptr_std_string_move(&my_string, from_cpp); // Use C++ function to move

    const data: [*]const u8 = c.shared_ptr_std_string_data(&my_string);
    const size: usize = c.shared_ptr_std_string_size(&my_string);
    std.log.info("the string is '{s}'", .{data[0..size]});

    giveBackToCpp(&my_string);
}
extern fn giveBackToCpp(s: *const c.SharedPtrStdString) void;

Notice that we’ve defined any function we need from C++ to move types around or access data. On the C++ side you’ll need a bunch of casting. However, I recently found a new pattern I quite like. In C++, if a type is defined by C++ then it’s “concrete,” otherwise it’s “opaque.” If you’re passing pointers to shared pointers and casting between them, you’ll find yourself with sore eyes from squinting too much, i.e.:

void screen_video_sink_remove(const SharedPtrScreenVideoSink* sink, const SharedPtrTnPeer* peer)
{
    const std::shared_ptr<ScreenVideoSink>& sink_cpp = *((std::shared_ptr<ScreenVideoSink>*)sink);
    const std::shared_ptr<tn::Peer>& peer_cpp = *(std::shared_ptr<tn::Peer>*)peer;
    peer_cpp->RemoveVideoSink(sink_cpp, tn::Peer::VideoSource::Screen);
}

The new pattern I’ve taken to is a macro that takes an opaque/concrete type pair and gives you the functions needed to convert them, i.e.

    // defines conversion functions between the "opaque types" used by Zig
    // and the concrete types in C++
    #define DEFINE_OPAQUE_CONCRETE(Opaque, Concrete) \
        Opaque* opaque(Concrete* c) { return reinterpret_cast<Opaque*>(c); } \
        const Opaque* opaque(const Concrete* c) { return reinterpret_cast<const Opaque*>(c); } \
        Concrete& concrete(Opaque* o) { return *reinterpret_cast<Concrete*>(o); } \
        const Concrete& concrete(const Opaque* o) { return *reinterpret_cast<const Concrete*>(o); }

    DEFINE_OPAQUE_CONCRETE(StdString, std::string)
    DEFINE_OPAQUE_CONCRETE(SharedPtrTnCall, std::shared_ptr<tn::Call>)
    DEFINE_OPAQUE_CONCRETE(SharedPtrTnPeer, std::shared_ptr<tn::Peer>)
    DEFINE_OPAQUE_CONCRETE(SharedPtrTnCallNotification, std::shared_ptr<tn::CallNotification>)
    DEFINE_OPAQUE_CONCRETE(ClientAuthSessionOpaque, tn::RestAPI::ClientAuthSession)
    DEFINE_OPAQUE_CONCRETE(TnCurrentUser, tn::RestAPI::CurrentUser)
    DEFINE_OPAQUE_CONCRETE(WebrtcVideoFrameBuffer, webrtc::VideoFrameBuffer)
    DEFINE_OPAQUE_CONCRETE(SharedPtrScreenVideoSink, std::shared_ptr<ScreenVideoSink>)

And our code above now becomes:

void screen_video_sink_remove(const SharedPtrScreenVideoSink* sink, const SharedPtrTnPeer* peer)
{
    concrete(peer)->RemoveVideoSink(concrete(sink), tn::Peer::VideoSource::Screen);
}

This is much improved as we’ve now pre-ordained the right conversions up-front and no longer have to re-audit the casts for every single conversion.

联系我们 contact @ memedata.com