Zig 中的 C 宏反射
C Macro Reflection in Zig

原始链接: https://jstrieb.github.io/posts/c-reflection-zig/

Zig 是一种快速发展的编程语言,专注于低级和系统编程,旨在取代 C。虽然仍处于积极开发阶段,但 Zig 提供了卓越的功能,并被 Bun 和 TigerBeetle 等多个著名项目所采用。 其突出特点是与 C 的出色兼容性(“互操作”),使得利用外部库(包括直接调用 C 函数)变得简单。 通过使用 Zig 的内置函数,用户可以轻松导入 C 头文件并将其内容合并到 Zig 程序中,而无需手动干预。 可以跨不同系统顺利实现各种平台的编译。 一个示例演示了通过 Zig 使用 Windows API 的简便性: ```生成文件 const win32 = @cImport({@cInclude("windows.h"), @cInclude("winuser.h");}); pub fn main() !void { _ = win32.MessageBoxA(null, "世界!", "你好", 0); } ```` 此示例展示了与传统方法相比,在 Zig 中使用 Windows API 的便利性,从而提供更好的生产力和可读性。 此外,将 C 宏从导入的标头映射到易于识别的变量的能力增强了可维护性,从而更容易理解复杂的代码结构。 总体而言,Zig 在与 C 库集成时提供了更高的效率和易用性,进一步巩固了其作为 C 语言有希望的替代品的地位。

摘要:本文讨论了 Zig 编程语言的 C 导入功能的更改。 最初,Zig 提供了神奇的语法(例如“@cImport”),可以轻松地将 C 代码集成到 Zig 项目中,而无需修改 C 源代码。 但是,这正在更改为常规导入(“@import”)。 此更改简化了构建过程并消除了对 Clang 和 Libclang 等外部工具的依赖。 此外,还将对导入的函数和类型进行更多控制,从而减少潜在的冲突。 尽管这一变化最初可能会给现有用户带来一些不便,但长期好处包括减少 Zig 和 C 生态系统之间的耦合并提高模块化程度。
相关文章

原文
C Macro Reflection in Zig – Zig Has Better C Interop Than C Itself

By Jacob Strieb.

Published on July 30, 2024.


Zig is a nascent programming language with an emphasis on low-level and systems programming that is positioned to be a C replacement. Despite being under active development (and having some rough edges as a result), Zig is extremely powerful, and is already used by a few substantial projects such as Bun and TigerBeetle.

Zig has many interesting features, but its outstanding interoperability (“interop”) with C is especially impressive. It is easy to call an external library, as in this example from the Zig website:

Calling external functions from C libraries is convenient, but lots of languages can do that. What is more impressive is that, in Zig, it is trivial to import C header files and use them as if they were regular Zig imports. We can rewrite the above to use the Windows header files, instead of manually forward-declaring extern functions:

The following command will compile both of the code examples above for Windows from any host operating system:

I continue to be astounded and delighted that that this code can both be written and cross-compiled so easily on any system.

I have done my fair share of C programming, but until recently, I had never written a Win32 application, nor had I ever written a program in Zig.

A typical Windows application has a main (or wWinMain) function and a “window procedure” (WindowProc) function. The main function initializes the application, and runs the loop in which messages are dispatched to the window procedure. The window procedure receives and handles the messages, typically taking a different action for each message type. To quote the Microsoft website:

Windows uses a message-passing model. The operating system communicates with your application window by passing messages to it. A message is simply a numeric code that designates a particular event. For example, if the user presses the left mouse button, the window receives a message that has the following message code.

Some messages have data associated with them. For example, the WM_LBUTTONDOWN message includes the x-coordinate and y-coordinate of the mouse cursor.

In practice, the window procedure becomes an enormous switch statement that matches the message code (uMsg in the example below) against macros defined in winuser.h. A minimal Zig example of a Win32 application with the standard structure (abridged from the Microsoft Win32 tutorial sequence) is as follows:

const std = @import("std");
const windows = std.os.windows;
const win32 = @cImport({
    @cInclude("windows.h");
    @cInclude("winuser.h");
});

var stdout: std.fs.File.Writer = undefined;

pub export fn WindowProc(hwnd: win32.HWND, uMsg: c_uint, wParam: win32.WPARAM, lParam: win32.LPARAM) callconv(windows.WINAPI) win32.LRESULT {
    // Handle each type of window message we care about
    _ = switch (uMsg) {
        win32.WM_CLOSE => win32.DestroyWindow(hwnd),
        win32.WM_DESTROY => win32.PostQuitMessage(0),
        else => {
            stdout.print("Unknown window message: 0x{x:0>4}\n", .{uMsg}) catch undefined;
        },
    };
    return win32.DefWindowProcA(hwnd, uMsg, wParam, lParam);
}

pub export fn main(hInstance: win32.HINSTANCE) c_int {
    stdout = std.io.getStdOut().writer();

    // Windows boilerplate to set up and draw a window
    var class = std.mem.zeroes(win32.WNDCLASSEXA);
    class.cbSize = @sizeOf(win32.WNDCLASSEXA);
    class.style = win32.CS_VREDRAW | win32.CS_HREDRAW;
    class.hInstance = hInstance;
    class.lpszClassName = "Class";
    class.lpfnWndProc = WindowProc; // Handle messages with this function
    _ = win32.RegisterClassExA(&class);

    const hwnd = win32.CreateWindowExA(win32.WS_EX_CLIENTEDGE, "Class", "Window", win32.WS_OVERLAPPEDWINDOW, win32.CW_USEDEFAULT, win32.CW_USEDEFAULT, win32.CW_USEDEFAULT, win32.CW_USEDEFAULT, null, null, hInstance, null);
    _ = win32.ShowWindow(hwnd, win32.SW_NORMAL);
    _ = win32.UpdateWindow(hwnd);

    // Dispatch messages to WindowProc
    var message: win32.MSG = std.mem.zeroes(win32.MSG);
    while (win32.GetMessageA(&message, null, 0, 0) > 0) {
        _ = win32.TranslateMessage(&message);
        _ = win32.DispatchMessageA(&message);
    }

    return 0;
}

The output of the code above looks like the following when it is run:

Unknown window message: 0x0024
Unknown window message: 0x0081
Unknown window message: 0x0083
Unknown window message: 0x0001
...
Unknown window message: 0x0008
Unknown window message: 0x0281
Unknown window message: 0x0282
Unknown window message: 0x0082

When extending the Windows code above to handle new message types, it is troublesome to determine which C macro corresponds to each message the window procedure receives. The numeric value of each message code is printed to the standard output, but mapping the numeric values back to C macro names involves either searching through documentation, or manually walking the header #include tree to find the right macro declaration.

The underlying cause of difficulty in mapping macro values back to macro names is that C does not have reflection for preprocessor macros – there is no way to get a list of all defined macros, let alone all macros with a specific value, from within C code. The preprocessor runs before the code is actually compiled, so the compiler itself is unaware of macros. The separation between the preprocessor and the compiler enables the user to make advanced changes to the code at compile time, but in practice, that separation means compiled code cannot introspect macros.

Though it may not be obvious from the code above, in Zig, references to macro and non-macro declarations from imported C header files are made in the same way. For example, win32.TranslateMessage is a function declared in the header file, and win32.WM_CLOSE is a macro declared using #define. Both are used in Zig by doing imported_name.declared_value. The Zig @import function returns a struct, so regular declarations and macros, alike, are represented as fields in the struct generated from importing the C header files.

It is significant that declarations are represented in imports as struct fields because, unlike C, Zig does have reflection. In particular, the @typeInfo function lists the fields and declarations of structs passed to it. This means that, though we cannot introspect C macros within C, we can introspect C macros within Zig. Consequently, we can create a mapping of macro values to macro names:

Using the global constant window_messages, we can change our WindowProc function to print more helpful information about the messages it is receiving:

Now, the output of the program looks much nicer when run:

...
WM_NCHITTEST: 0x0084
WM_SETCURSOR: 0x0020
WM_MOUSEMOVE: 0x0200
WM_SYSKEYDOWN: 0x0104
WM_CHAR: 0x0102
WM_KEYUP: 0x0101
WM_SYSKEYUP: 0x0105
WM_WINDOWPOSCHANGING: 0x0046
WM_WINDOWPOSCHANGED: 0x0047
WM_NCACTIVATE: 0x0086
WM_ACTIVATE: 0x0006
WM_ACTIVATEAPP: 0x001c
WM_KILLFOCUS: 0x0008
WM_IME_SETCONTEXT: 0x0281
WM_NCDESTROY: 0x0082

Though this example is small, it illustrates that Zig can do what C does, but can do so more ergonomically by employing modern programming language constructs. One of Zig’s unique superpowers is that it bundles a C compiler toolchain – that is what enables it to transcend C FFI and seamlessly include declarations from C header files, among other capabilities.

Incorporating C interoperability so deeply into the language highlights Zig’s prudent acknowledgment that C has been around for a long time, and is here to stay for a while longer. Integrating with C in this way means that Zig developers have had access to thousands of existing, battle-tested software libraries since the language’s first release. It also gives developers responsible for existing C or C++ codebases a path to transition them to Zig. Availability of high-quality libraries and transition paths for existing code are both critical obstacles to language adoption that Zig has cleverly bypassed by electing to subsume C in the course of replacing it.

Zig’s philosophy of pragmatism is apparent as soon as you begin learning the language. Within a few hours of getting started, I was able to come up with this C macro reflection trick, and also able to be generally productive. That is, to me, clear evidence of Zig’s intuitive, consistent design.

Zig’s straightforward cross-compilation and C integration are what drew me to the language, but its philosophy and design are what will keep me here to stay.

Thanks to Logan Snow and Amy Liu for reviewing a draft of this post.

Shout out to Andrew Kelley and the other Zig contributors.

联系我们 contact @ memedata.com