{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreiceyc2vqgpuqttyy526gr3aovlwbbmxkvvj7u26qp6qjwz6nrgopq",
"uri": "at://did:plc:wziiohjsf6mmaqbibugrffla/app.bsky.feed.post/3limvdpe3xwx2"
},
"path": "/Comparing-Strings-as-Integers-with-bitCast/",
"publishedAt": "2026-03-08T09:10:32.935Z",
"site": "https://www.openmymind.net",
"tags": [
"different ways to compare strings in Zig",
"introduced Zig's `@bitCast`",
"4]u8{3, 0, 0, 0}` and not `[4]u8{0, 0, 0, 3}`, I talked about binary encoding in my [Learning TCP",
"@bitCast",
"@import",
"@as",
"@Type",
"@typeInfo"
],
"textContent": "In the last blog posts, we looked at different ways to compare strings in Zig. A few posts back, we introduced Zig's `@bitCast`. As a quick recap, `@bitCast` lets us force a specific type onto a value. For example, the following prints 1067282596:\n\n\n const std = @import(\"std\"); pub fn main() !void { const f: f32 = 1.23; const n: u32 = @bitCast(f); std.debug.print(\"{d}\\n\", .{n}); }\n\nWhat's happening here is that Zig represents the 32-bit float value of `1.23` as: `[4]u8{164, 112, 157, 63}`. This is also how Zig represents the 32-bit unsigned integer value of `1067282596`. Data is just bytes; it's the type system - the compiler's knowledge of what data is what type - that controls what and how that data is manipulated.\n\nIt might seem like there's something special about bitcasting from a float to an integer; they're both numbers after all. But you can `@bitCast` from any two equivalently sized types. Can you guess what this prints?:\n\n\n const std = @import(\"std\"); pub fn main() !void { const data = [_]u8{3, 0, 0, 0}; const x: i32 = @bitCast(data); std.debug.print(\"{d}\\n\", .{x}); }\n\nThe answer is `3`. Think about the above snippet a bit more. We're taking an array of bytes and telling the compiler to treat it like an integer. If we made `data` equal to `[_]u8{'b', 'l', 'u', 'e'}`, it would still work (and print `1702194274`). We're slowly heading towards being able to compare strings as-if they were integers.\n\nIf you're wondering why 3 is encoded as `4]u8{3, 0, 0, 0}` and not `[4]u8{0, 0, 0, 3}`, I talked about binary encoding in my [Learning TCP series.\n\nFrom the last post, we could use multiple `std.mem.eql` or, more simply, `std.meta.stringToEnum` to complete the following method:\n\n\n fn parseMethod(value: []const u8) ?Method { // ... } const Method = enum { get, put, post, head, };\n\nWe can also use `@bitCast`. Let's take it step-by-step.\n\nThe first thing we'll need to do is switch on `value.len`. This is necessary because the three-byte \"GET\" will need to be `@bitCast` to a `u24`, whereas the four-byte \"POST\" needs to be `@bitCast` to a `u32`:\n\n\n fn parseMethod(value: []const u8) ?Method { switch (value.len) { 3 => switch (@as(u24, @bitCast(value[0..3]))) { // TODO else => {}, }, 4 => switch (@as(u32, @bitCast(value[0..4]))) { // TODO else => {}, }, else => {}, } return null; }\n\nIf you try to run this code, you'll get a compilation error: _cannot @bitCast from '*const [3]u8'_. `@bitCast` works on actual bits, but when we slice our `[]const u8` with a compile-time known range (`[0..3]`), we get a pointer to an array. We can't `@bitCast` a pointer, we can only `@bitCast` actual bits of data. For this to work, we need to derefence the pointer, i.e. use: `value[0..3].*`. This will turn our `*const [3]u8` into a `const [3]u8`.\n\n\n fn parseMethod(value: []const u8) ?Method { switch (value.len) { // changed: we now derefernce the value (.*) 3 => switch (@as(u24, @bitCast(value[0..3].*))) { // TODO else => {}, }, // changed: we now dereference the value (.*) 4 => switch (@as(u32, @bitCast(value[0..4].*))) { // TODO else => {}, }, else => {}, } return null; }\n\nAlso, you might have noticed the `@as(u24, ...)` and `@as(u32, ...)`. `@bitCast`, like most of Zig's builtin functions, infers its return type. When we're assiging the result of a `@bitCast` to a variable of a known type, i.e: `const x: i32 = @bitCast(data);`, the return type of `i32` is inferred. In the above `switch`, we aren't assigning the result to a varible. We have to use `@as(u24, ...)` in order for `@bitCast` to kknow what it should be casting to (i.e. what its return type should be).\n\nThe last thing we need to do is fill our switch blocks. Hopefully it's obvious that we can't just do:\n\n\n 3 => switch (@as(u24, @bitCast(value[0..3].*))) { \"GET\" => return .get, \"PUT\" => return .put, else => {}, }, ...\n\nBut you might be thinking that, while ugly, something like this might work:\n\n\n 3 => switch (@as(u24, @bitCast(value[0..3].*))) { @as(u24, @bitCast(\"GET\".*)) => return .get, @as(u24, @bitCast(\"PUT\".*)) => return .put, else => {}, }, ...\n\nBecause `\"GET\"` and `\"PUT\"` are string literals, they're null terminated and of type `*const [3:0]u8`. When we dereference them, we get a `const [3:0]u8`. It's close, but it means that the value is 4 bytes (`[4]u8{'G', 'E', 'T', 0}`) and thus cannot be `@bitCast` into a `u24`. This is ugly, but it works:\n\n\n fn parseMethod(value: []const u8) ?Method { switch (value.len) { 3 => switch (@as(u24, @bitCast(value[0..3].*))) { @as(u24, @bitCast(@as([]const u8, \"GET\")[0..3].*)) => return .get, @as(u24, @bitCast(@as([]const u8, \"PUT\")[0..3].*)) => return .put, else => {}, }, 4 => switch (@as(u32, @bitCast(value[0..4].*))) { @as(u32, @bitCast(@as([]const u8, \"HEAD\")[0..4].*)) => return .head, @as(u32, @bitCast(@as([]const u8, \"POST\")[0..4].*)) => return .post, else => {}, }, else => {}, } return null; }\n\nThat's a mouthful, so we can add small function to help:\n\n\n fn parseMethod(value: []const u8) ?Method { switch (value.len) { 3 => switch (@as(u24, @bitCast(value[0..3].*))) { asUint(u24, \"GET\") => return .get, asUint(u24, \"PUT\") => return .put, else => {}, }, 4 => switch (@as(u32, @bitCast(value[0..4].*))) { asUint(u32, \"HEAD\") => return .head, asUint(u32, \"POST\") => return .post, else => {}, }, else => {}, } return null; } pub fn asUint(comptime T: type, comptime string: []const u8) T { return @bitCast(string[0..string.len].*); }\n\nLike the verbose version, the trick is to cast our null-terminated string literal into a string slice, `[]const u8`. By passing it through the `asUint` function, we get this without needing to add the explicit `@as([]const u8)`.\n\nThere is a more advanced version of `asUint` which doesn't take the uint type parameter (`T`). If you think about it, the uint type can be inferred from the string's length:\n\n\n pub fn asUint(comptime string: []const u8) @Type(.{ .int = .{ // bits, not bytes, hence * 8 .bits = string.len * 8, .signedness = .unsigned, }, }) { return @bitCast(string[0..string.len].*); }\n\nWhich allows us to call it with a single parameter: `asUint(\"GET\")`. This might be your first time seeing such a return type. The `@Type` builtin is the opposite of `@typeInfo`. The latter takes a type and returns information on it in the shape of a `std.builtin.Type` union. Whereas `@Type` takes the `std.builtin.Type` and returns an actual usable type. One of these days I'll find the courage to blog about `std.builtin.Type`!\n\nAs a final note, some people dislike the look of this sort of return type and rather encapsulate the logic in its own function. This is the same:\n\n\n pub fn asUint(comptime string: []const u8) AsUintReturn(string) { return @bitCast(string[0..string.len].*); } // Remember that, in Zig, by convention, a function should be // PascalCase if it returns a type (because types are PascalCase). fn AsUintReturn(comptime string: []const u8) type { return @Type(.{ .int = .{ // bits, not bytes, hence * 8 .bits = string.len * 8, .signedness = .unsigned, }, }); }\n\n### Conclusion\n\nOf the three approaches, this is the least readable and less approachable. Is it worth it? It depends on your input and the values you're comparing against. In my benchmarks, using `@bitCast` performs roughly the same as `std.meta.stringToEnum`. But there are some cases where `@bitCast` can outperform `std.meta.stringToEnum` by as much as 50%. Perhaps that's the real value of this approach: the performance is less dependent on the input or the values being matched against.\n\nLeave a comment",
"title": "Comparing Strings as Integers with @bitCast",
"updatedAt": "2025-02-20T00:00:00.000Z"
}