Rewrite example 01 as standalone example

This example is meant to demonstrate how one can write a wayland client
without any libraries.
dev
LeRoyce Pearson 2023-12-18 12:01:28 -07:00
parent 13b0ea6a6f
commit 50a9774a07
2 changed files with 604 additions and 1 deletions

View File

@ -27,8 +27,16 @@ pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run library tests"); const test_step = b.step("test", "Run library tests");
test_step.dependOn(&run_main_tests.step); test_step.dependOn(&run_main_tests.step);
const client_connect_raw_exe = b.addExecutable(.{
.name = "00_client_connect",
.root_source_file = .{ .path = "examples/00_client_connect.zig" },
.target = target,
.optimize = optimize,
});
b.installArtifact(client_connect_raw_exe);
const client_connect_exe = b.addExecutable(.{ const client_connect_exe = b.addExecutable(.{
.name = "client_connect", .name = "01_client_connect",
.root_source_file = .{ .path = "examples/01_client_connect.zig" }, .root_source_file = .{ .path = "examples/01_client_connect.zig" },
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,

View File

@ -0,0 +1,595 @@
const std = @import("std");
/// The version of the wl_shm protocol we will be targeting.
const WL_SHM_VERSION = 1;
/// The version of the wl_compositor protocol we will be targeting.
const WL_COMPOSITOR_VERSION = 5;
/// The version of the xdg_wm_base protocol we will be targeting.
const XDG_WM_BASE_VERSION = 2;
/// https://wayland.app/protocols/xdg-shell#xdg_surface:request:ack_configure
const XDG_SURFACE_REQUEST_ACK_CONFIGURE = 4;
pub fn main() !void {
var general_allocator = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = general_allocator.deinit();
const gpa = general_allocator.allocator();
const display_path = try getDisplayPath(gpa);
defer gpa.free(display_path);
const socket = try std.net.connectUnixSocket(display_path);
defer socket.close();
var next_id: u32 = 2;
// reserve an object id for the registry
const registry_id = next_id;
next_id += 1;
try socket.writeAll(std.mem.sliceAsBytes(&[_]u32{
// ID of the object; in this case the default wl_display object at 1
1,
// The size (in bytes) of the message and the opcode, which is object specific.
// In this case we are using opcode 1, which corresponds to `wl_display::get_registry`.
//
// The size includes the size of the header.
(0x000C << 16) | (0x0001),
// Finally, we pass in the only argument that this opcode takes: an id for the `wl_registry`
// we are creating.
registry_id,
}));
// create a sync callback so we know when we are caught up with the server
const display_id = 1;
const registry_done_callback_id = next_id;
next_id += 1;
try socket.writeAll(std.mem.sliceAsBytes(&[_]u32{
display_id,
// The size (in bytes) of the message and the opcode.
// In this case we are using opcode 0, which corresponds to `wl_display::sync`.
//
// The size includes the size of the header.
(0x000C << 16) | (0x0000),
// Finally, we pass in the only argument that this opcode takes: an id for the `wl_registry`
// we are creating.
registry_done_callback_id,
}));
var shm_id_opt: ?u32 = null;
var compositor_id_opt: ?u32 = null;
var xdg_wm_base_id_opt: ?u32 = null;
// How do we know that the opcode for WL_REGISTRY_REQUEST is 0? Because it is the first `request` in the protocol for `wl_registry`.
const WL_REGISTRY_REQUEST_BIND = 0;
var message_bytes = std.ArrayList(u8).init(gpa);
defer message_bytes.deinit();
while (true) {
message_bytes.shrinkRetainingCapacity(0);
var header: Header = undefined;
const header_bytes_read = try socket.readAll(std.mem.asBytes(&header));
if (header_bytes_read < @sizeOf(Header)) {
break;
}
try message_bytes.resize(header.size - @sizeOf(Header));
const message_bytes_read = try socket.readAll(message_bytes.items);
if (message_bytes_read < message_bytes.items.len) {
return error.UnexpectedEOF;
}
// Parse event messages based on which object it is for
if (header.object_id == registry_done_callback_id) {
// No need to parse the message, there is only one event
break;
}
if (header.object_id == registry_id and header.opcode == 0) {
// Parse out the fields of the global event
const name: u32 = @bitCast(message_bytes.items[0..4].*);
const interface_str_len: u32 = @bitCast(message_bytes.items[4..8].*);
const interface_str: [:0]const u8 = message_bytes.items[8..][0 .. interface_str_len - 1 :0];
const interface_str_len_u32_align = std.mem.alignForward(u32, interface_str_len, @alignOf(u32));
const version: u32 = @bitCast(message_bytes.items[8 + interface_str_len_u32_align ..][0..4].*);
// Check to see if the interface is one of the globals we are looking for
if (std.mem.eql(u8, interface_str, "wl_shm")) {
if (version < WL_SHM_VERSION) {
std.log.err("compositor supports only {s} version {}, client expected version >= {}", .{ interface_str, version, WL_SHM_VERSION });
return error.WaylandInterfaceOutOfDate;
}
shm_id_opt = next_id;
next_id += 1;
const registry_bind_request_message_body = [_]u32{
// The numeric name of the global we want to bind.
name,
// `new_id` arguments have three parts when the sub-type is not specified by the protocol:
// 1. A string specifying the textual name of the interface
"wl_shm".len + 1, // length of "wl_shm" plus one for the required null byte
@bitCast(@as([4]u8, "wl_s".*)),
@bitCast(@as([4]u8, "hm\x00\x00".*)), // we have two 0x00 bytes to align the string with u32
// 2. The version you are using, affects which functions you can access
WL_SHM_VERSION,
// 3. And the `new_id` part, where we tell it which client id we are giving it
shm_id_opt.?,
};
const registry_bind_request_header = Header{
.object_id = registry_id,
.opcode = WL_REGISTRY_REQUEST_BIND,
.size = @sizeOf(Header) + registry_bind_request_message_body.len * @sizeOf(u32),
};
try socket.writeAll(std.mem.asBytes(&registry_bind_request_header));
try socket.writeAll(std.mem.sliceAsBytes(&registry_bind_request_message_body));
} else if (std.mem.eql(u8, interface_str, "wl_compositor")) {
if (version < WL_COMPOSITOR_VERSION) {
std.log.err("compositor supports only {s} version {}, client expected version >= {}", .{ interface_str, version, WL_COMPOSITOR_VERSION });
return error.WaylandInterfaceOutOfDate;
}
compositor_id_opt = next_id;
next_id += 1;
const registry_bind_request_message_body = [_]u32{
name,
"wl_compositor".len + 1, // add one for the required null byte
@bitCast(@as([4]u8, "wl_c".*)),
@bitCast(@as([4]u8, "ompo".*)),
@bitCast(@as([4]u8, "sito".*)),
@bitCast(@as([4]u8, "r\x00\x00\x00".*)),
WL_COMPOSITOR_VERSION,
compositor_id_opt.?,
};
const registry_bind_request_header = Header{
.object_id = registry_id,
.opcode = WL_REGISTRY_REQUEST_BIND,
.size = @sizeOf(Header) + registry_bind_request_message_body.len * @sizeOf(u32),
};
try socket.writeAll(std.mem.asBytes(&registry_bind_request_header));
try socket.writeAll(std.mem.sliceAsBytes(&registry_bind_request_message_body));
} else if (std.mem.eql(u8, interface_str, "xdg_wm_base")) {
if (version < XDG_WM_BASE_VERSION) {
std.log.err("compositor supports only {s} version {}, client expected version >= {}", .{ interface_str, version, XDG_WM_BASE_VERSION });
return error.WaylandInterfaceOutOfDate;
}
xdg_wm_base_id_opt = next_id;
next_id += 1;
const registry_bind_request_message_body = [_]u32{
name,
"xdg_wm_base".len + 1,
@bitCast(@as([4]u8, "xdg_".*)),
@bitCast(@as([4]u8, "wm_b".*)),
@bitCast(@as([4]u8, "ase\x00".*)),
XDG_WM_BASE_VERSION,
xdg_wm_base_id_opt.?,
};
const registry_bind_request_header = Header{
.object_id = registry_id,
.opcode = WL_REGISTRY_REQUEST_BIND,
.size = @sizeOf(Header) + registry_bind_request_message_body.len * @sizeOf(u32),
};
try socket.writeAll(std.mem.asBytes(&registry_bind_request_header));
try socket.writeAll(std.mem.sliceAsBytes(&registry_bind_request_message_body));
}
continue;
}
}
const shm_id = shm_id_opt orelse return error.NeccessaryWaylandExtensionMissing;
const compositor_id = compositor_id_opt orelse return error.NeccessaryWaylandExtensionMissing;
const xdg_wm_base_id = xdg_wm_base_id_opt orelse return error.NeccessaryWaylandExtensionMissing;
std.log.debug("wl_shm client id = {}; wl_compositor client id = {}; xdg_wm_base client id = {}", .{ shm_id, compositor_id, xdg_wm_base_id });
// Create a surface using wl_compositor::create_surface
const surface_id = next_id;
next_id += 1;
const WL_COMPOSITOR_REQUEST_CREATE_SURFACE = 0;
try writeRequest(socket, compositor_id, WL_COMPOSITOR_REQUEST_CREATE_SURFACE, &[_]u32{
// id: new_id<wl_surface>
surface_id,
});
// Create an xdg_surface
const xdg_surface_id = next_id;
next_id += 1;
const XDG_WM_BASE_REQUEST_GET_XDG_SURFACE = 2;
try writeRequest(socket, xdg_wm_base_id, XDG_WM_BASE_REQUEST_GET_XDG_SURFACE, &[_]u32{
// id: new_id<xdg_surface>
xdg_surface_id,
// surface: object<wl_surface>
surface_id,
});
// Get the xdg_surface as an xdg_toplevel object
const xdg_toplevel_id = next_id;
next_id += 1;
const XDG_SURFACE_REQUEST_GET_TOPLEVEL = 1;
try writeRequest(socket, xdg_surface_id, XDG_SURFACE_REQUEST_GET_TOPLEVEL, &[_]u32{
// id: new_id<xdg_surface>
xdg_toplevel_id,
});
// Commit the surface. This tells wayland that we are done making changes, and it can display all the changes that have been
// made so far.
const WL_SURFACE_REQUEST_COMMIT = 6;
try writeRequest(socket, surface_id, WL_SURFACE_REQUEST_COMMIT, &[_]u32{});
// create another wl_callback
const create_surface_done_id = next_id;
next_id += 1;
const WL_DISPLAY_REQUEST_DONE = 0;
try writeRequest(socket, display_id, WL_DISPLAY_REQUEST_DONE, &[_]u32{create_surface_done_id});
// Wait for the surface to be configured before moving on
var done = false;
var surface_configured = false;
while (!done or !surface_configured) {
message_bytes.shrinkRetainingCapacity(0);
var header: Header = undefined;
const header_bytes_read = try socket.readAll(std.mem.asBytes(&header));
if (header_bytes_read < @sizeOf(Header)) {
break;
}
try message_bytes.resize(header.size - @sizeOf(Header));
const message_bytes_read = try socket.readAll(message_bytes.items);
if (message_bytes_read < message_bytes.items.len) {
return error.UnexpectedEOF;
}
if (header.object_id == create_surface_done_id) {
done = true;
} else if (header.object_id == xdg_surface_id) {
switch (header.opcode) {
// https://wayland.app/protocols/xdg-shell#xdg_surface:event:configure
0 => {
// The configure event acts as a heartbeat. Every once in a while the compositor will send us
// a `configure` event, and if our application doesn't respond with an `ack_configure` response
// it will assume our program has died and destroy the window.
const serial: u32 = @bitCast(message_bytes.items[0..4].*);
try writeRequest(socket, xdg_surface_id, XDG_SURFACE_REQUEST_ACK_CONFIGURE, &[_]u32{
// We respond with the number it sent us, so it knows which configure we are responding to.
serial,
});
surface_configured = true;
},
else => return error.InvalidOpcode,
}
} else if (header.object_id == display_id) {
switch (header.opcode) {
// https://wayland.app/protocols/wayland#wl_display:event:error
0 => {
const object_id: u32 = @bitCast(message_bytes.items[0..4].*);
const error_code: u32 = @bitCast(message_bytes.items[4..8].*);
const error_message_len: u32 = @bitCast(message_bytes.items[8..12].*);
const error_message = message_bytes.items[12 .. error_message_len - 1 :0];
std.log.warn("wl_display:error({}, {}, \"{}\")", .{ object_id, error_code, std.zig.fmtEscapes(error_message) });
},
// https://wayland.app/protocols/wayland#wl_display:event:delete_id
1 => {
// wl_display:delete_id tells us that we can reuse an id. In this article we log it, but
// otherwise ignore it.
const name: u32 = @bitCast(message_bytes.items[0..4].*);
std.log.debug("wl_display:delete_id({})", .{name});
},
else => return error.InvalidOpcode,
}
} else {
std.log.warn("unknown event {{ .object_id = {}, .opcode = {x}, .message = \"{}\" }}", .{ header.object_id, header.opcode, std.zig.fmtEscapes(std.mem.sliceAsBytes(message_bytes.items)) });
}
}
// allocate a shared memory file, which we will use as a framebuffer to write pixels into
const Pixel = [4]u8;
const framebuffer_size = [2]usize{ 128, 128 };
const shared_memory_pool_len = framebuffer_size[0] * framebuffer_size[1] * @sizeOf(Pixel);
const shared_memory_pool_fd = try std.os.memfd_create("my-wayland-framebuffer", 0);
try std.os.ftruncate(shared_memory_pool_fd, shared_memory_pool_len);
const shared_memory_pool_bytes = try std.os.mmap(null, shared_memory_pool_len, std.os.PROT.READ | std.os.PROT.WRITE, std.os.MAP.SHARED, shared_memory_pool_fd, 0);
// Create a wl_shm_pool (wayland shared memory pool). This will be used to create framebuffers,
// though in this article we only plan on creating one.
const wl_shm_pool_id = try writeWlShmRequestCreatePool(
socket,
shm_id,
&next_id,
shared_memory_pool_fd,
@intCast(shared_memory_pool_bytes.len),
);
// Now we allocate a framebuffer from the shared memory pool
const wl_buffer_id = next_id;
next_id += 1;
const WL_SHM_POOL_REQUEST_CREATE_BUFFER = 0;
const WL_SHM_POOL_ENUM_FORMAT_ARGB8888 = 0;
try writeRequest(socket, wl_shm_pool_id, WL_SHM_POOL_REQUEST_CREATE_BUFFER, &[_]u32{
// id: new_id<wl_buffer>,
wl_buffer_id,
// Byte offset of the framebuffer in the pool. In this case we allocate it at the very start of the file.
0,
// Width of the framebuffer.
framebuffer_size[0],
// Height of the framebuffer.
framebuffer_size[1],
// Stride of the framebuffer, or rather, how many bytes are in a single row of pixels.
framebuffer_size[0] * @sizeOf(Pixel),
// The format of the framebuffer. In this case we choose argb8888.
WL_SHM_POOL_ENUM_FORMAT_ARGB8888,
});
// Now we turn the framebuffer we just allocated into a slice on our side for ease of use.
const framebuffer = @as([*]Pixel, @ptrCast(shared_memory_pool_bytes.ptr))[0 .. shared_memory_pool_bytes.len / @sizeOf(Pixel)];
// put some interesting colors into the framebuffer
for (0..framebuffer_size[1]) |y| {
const row = framebuffer[y * framebuffer_size[0] .. (y + 1) * framebuffer_size[0]];
for (row, 0..framebuffer_size[0]) |*pixel, x| {
pixel.* = .{
@truncate(x),
@truncate(y),
0x00,
0xFF,
};
}
}
// Now we attach the framebuffer to the surface at <0, 0>. The x and y MUST be <0, 0> since version 5 of WL_SURFACE,
// which we are using.
// https://wayland.app/protocols/wayland#wl_surface:request:attach
const WL_SURFACE_REQUEST_ATTACH = 1;
try writeRequest(socket, surface_id, WL_SURFACE_REQUEST_ATTACH, &[_]u32{
// buffer: object<wl_buffer>,
wl_buffer_id,
// The x offset of the buffer.
0,
// The y offset of the buffer.
0,
});
// We mark the surface as damaged, meaning that the compositor should update what is rendered on the window.
// You can specify specific damage regions; but in this case we just damage the entire surface.
// https://wayland.app/protocols/wayland#wl_surface:request:damage
const WL_SURFACE_REQUEST_DAMAGE = 2;
try writeRequest(socket, surface_id, WL_SURFACE_REQUEST_DAMAGE, &[_]u32{
// The x offset of the damage region.
0,
// The y offset of the damage region.
0,
// The width of the damage region.
@bitCast(@as(i32, std.math.maxInt(i32))),
// The height of the damage region.
@bitCast(@as(i32, std.math.maxInt(i32))),
});
// Commit the surface. This tells wayland that we are done making changes, and it can display all the changes that have been
// made so far.
// const WL_SURFACE_REQUEST_COMMIT = 6;
try writeRequest(socket, surface_id, WL_SURFACE_REQUEST_COMMIT, &[_]u32{});
// Now we finally, finally, get to the main loop of the program.
var running = true;
while (running) {
message_bytes.shrinkRetainingCapacity(0);
var header: Header = undefined;
const header_bytes_read = try socket.readAll(std.mem.asBytes(&header));
if (header_bytes_read < @sizeOf(Header)) {
break;
}
try message_bytes.resize(header.size - @sizeOf(Header));
const message_bytes_read = try socket.readAll(message_bytes.items);
if (message_bytes_read < message_bytes.items.len) {
return error.UnexpectedEOF;
}
if (header.object_id == xdg_surface_id) {
switch (header.opcode) {
// https://wayland.app/protocols/xdg-shell#xdg_surface:event:configure
0 => {
// The configure event acts as a heartbeat. Every once in a while the compositor will send us
// a `configure` event, and if our application doesn't respond with an `ack_configure` response
// it will assume our program has died and destroy the window.
const serial: u32 = @bitCast(message_bytes.items[0..4].*);
try writeRequest(socket, xdg_surface_id, XDG_SURFACE_REQUEST_ACK_CONFIGURE, &[_]u32{
// We respond with the number it sent us, so it knows which configure we are responding to.
serial,
});
},
else => return error.InvalidOpcode,
}
} else if (header.object_id == xdg_toplevel_id) {
switch (header.opcode) {
// https://wayland.app/protocols/xdg-shell#xdg_toplevel:event:configure
0 => {
// The xdg_toplevel:configure event asks us to resize the window. For now, we will ignore it expect to
// log it.
const width: u32 = @bitCast(message_bytes.items[0..4].*);
const height: u32 = @bitCast(message_bytes.items[4..8].*);
const states_len: u32 = @bitCast(message_bytes.items[8..12].*);
const states = @as([*]u32, @ptrCast(@alignCast(message_bytes.items[12..].ptr)))[0..states_len];
std.log.debug("xdg_toplevel:configure({}, {}, {any})", .{ width, height, states });
},
// https://wayland.app/protocols/xdg-shell#xdg_toplevel:event:close
1 => {
// The compositor asked us to close the window.
running = false;
std.log.debug("xdg_toplevel:close()", .{});
},
// https://wayland.app/protocols/xdg-shell#xdg_toplevel:event:configure_bounds
2 => std.log.debug("xdg_toplevel:configure_bounds()", .{}),
// https://wayland.app/protocols/xdg-shell#xdg_toplevel:event:wm_capabilities
3 => std.log.debug("xdg_toplevel:wm_capabilities()", .{}),
else => return error.InvalidOpcode,
}
} else if (header.object_id == wl_buffer_id) {
switch (header.opcode) {
// https://wayland.app/protocols/wayland#wl_buffer:event:release
0 => {
// The xdg_toplevel:release event let's us know that it is safe to reuse the buffer now.
std.log.debug("wl_buffer:release()", .{});
},
else => return error.InvalidOpcode,
}
} else if (header.object_id == display_id) {
switch (header.opcode) {
// https://wayland.app/protocols/wayland#wl_display:event:error
0 => {
const object_id: u32 = @bitCast(message_bytes.items[0..4].*);
const error_code: u32 = @bitCast(message_bytes.items[4..8].*);
const error_message_len: u32 = @bitCast(message_bytes.items[8..12].*);
const error_message = message_bytes.items[12 .. error_message_len - 1 :0];
std.log.warn("wl_display:error({}, {}, \"{}\")", .{ object_id, error_code, std.zig.fmtEscapes(error_message) });
},
// https://wayland.app/protocols/wayland#wl_display:event:delete_id
1 => {
// wl_display:delete_id tells us that we can reuse an id. In this article we log it, but
// otherwise ignore it.
const name: u32 = @bitCast(message_bytes.items[0..4].*);
std.log.debug("wl_display:delete_id({})", .{name});
},
else => return error.InvalidOpcode,
}
} else {
std.log.warn("unknown event {{ .object_id = {}, .opcode = {x}, .message = \"{}\" }}", .{ header.object_id, header.opcode, std.zig.fmtEscapes(std.mem.sliceAsBytes(message_bytes.items)) });
}
}
}
pub fn getDisplayPath(gpa: std.mem.Allocator) ![]u8 {
const xdg_runtime_dir_path = try std.process.getEnvVarOwned(gpa, "XDG_RUNTIME_DIR");
defer gpa.free(xdg_runtime_dir_path);
const display_name = try std.process.getEnvVarOwned(gpa, "WAYLAND_DISPLAY");
defer gpa.free(display_name);
return try std.fs.path.join(gpa, &.{ xdg_runtime_dir_path, display_name });
}
// Let's turn that manual work above into a struct to make things easier to understand.
const Header = extern struct {
object_id: u32 align(1),
opcode: u16 align(1),
size: u16 align(1),
};
pub fn writeRequest(socket: std.net.Stream, object_id: u32, opcode: u16, message: []const u32) !void {
const message_bytes = std.mem.sliceAsBytes(message);
const header = Header{
.object_id = object_id,
.opcode = opcode,
.size = @sizeOf(Header) + @as(u16, @intCast(message_bytes.len)),
};
try socket.writeAll(std.mem.asBytes(&header));
try socket.writeAll(message_bytes);
}
/// https://wayland.app/protocols/wayland#wl_shm:request:create_pool
const WL_SHM_REQUEST_CREATE_POOL = 0;
/// This request is more complicated that most other requests, because it has to send the file descriptor to the
/// compositor using a control message.
///
/// Returns the id of the newly create wl_shm_pool
pub fn writeWlShmRequestCreatePool(socket: std.net.Stream, wl_shm_id: u32, next_id: *u32, fd: std.os.fd_t, fd_len: i32) !u32 {
const wl_shm_pool_id = next_id.*;
const message = [_]u32{
// id: new_id<wl_shm_pool>
wl_shm_pool_id,
// size: int
@intCast(fd_len),
};
// If you're paying close attention, you'll notice that our message only has two parameters in it, despite the
// documentation calling for 3: wl_shm_pool_id, fd, and size. This is because `fd` is sent in the control message,
// and so not included in the regular message body.
// Send the file descriptor through a control message
const message_bytes = std.mem.sliceAsBytes(&message);
const header = Header{
.object_id = wl_shm_id,
.opcode = WL_SHM_REQUEST_CREATE_POOL,
.size = @sizeOf(Header) + @as(u16, @intCast(message_bytes.len)),
};
const header_bytes = std.mem.asBytes(&header);
// we'll be using `std.os.sendmsg` to send a control message, so we may as well use the vectorized
// IO to send the header and the message body while we're at it.
const msg_iov = [_]std.os.iovec_const{
.{
.iov_base = header_bytes.ptr,
.iov_len = header_bytes.len,
},
.{
.iov_base = message_bytes.ptr,
.iov_len = message_bytes.len,
},
};
// This is the control message! It is not a fixed size struct. Instead it varies depending on the message you want to send.
// C uses macros to define it, here we make a comptime function instead.
const control_message = cmsg(std.os.fd_t){
.level = std.os.SOL.SOCKET,
.type = 0x01, // value of SCM_RIGHTS
.data = fd,
};
const control_message_bytes = std.mem.asBytes(&control_message);
const socket_message = std.os.msghdr_const{
.name = null,
.namelen = 0,
.iov = &msg_iov,
.iovlen = msg_iov.len,
.control = control_message_bytes.ptr,
// This is the size of the control message in bytes
.controllen = control_message_bytes.len,
.flags = 0,
};
const bytes_sent = try std.os.sendmsg(socket.handle, &socket_message, 0);
if (bytes_sent < header_bytes.len + message_bytes.len) {
return error.ConnectionClosed;
}
next_id.* += 1;
return wl_shm_pool_id;
}
fn cmsg(comptime T: type) type {
const padding_size = (@sizeOf(T) + @sizeOf(c_long) - 1) & ~(@as(usize, @sizeOf(c_long)) - 1);
return extern struct {
len: c_ulong = @sizeOf(@This()) - padding_size,
level: c_int,
type: c_int,
data: T,
_padding: [padding_size]u8 align(1) = [_]u8{0} ** padding_size,
};
}