https://www.lagerdata.com/articles/an-intro-to-zigs-integer-casting-for-c-programmers [604675a137] OverviewPricingDocumentationField Guide Sign InGet Started Sign InGet Started An intro to Zig's integer casting for C programmers Written by Evan Haas April 15, 2021 Section Zig Zagging Chapter An intro to Zig's integer casting for C programmers AKA Zig casts, and when to use them Today we'll just talk about integer casting, but Zig also provides explicit casting support for floats, bools, enums, pointers, and more. For the full list of explicit casts, see https://ziglang.org/ documentation/master/#Explicit-Casts There are two major problems with integer casting in C - you don't always know when it happens, and you don't always know what it does. A bit of a bold claim, but what I mean here is that a purely textual analysis of a C program (e.g, via {% c-line %}grep{% c-line-end %} or {% c-line %}ctrl+f{% c-line-end %}) cannot find all of the integer casts (due to implicit conversions) and of the ones it does find, there is no textual way to distinguish truncation, reinterpretation, or extension. For an example of the former problem, try compiling and running this program: {% c-block language="c" %} #include int main (void) { if (sizeof(int) < -1) abort(); return 0; } {% c-block-end %} You won't even get a warning unless {% c-line %}-Wextra{% c-line-end %} or {% c-line %}-Weverything{% c-line-end %} is turned on. What's happening is that {% c-line %}-1{% c-line-end %} is implicitly converted to unsigned (since the result of {% c-line %}sizeof{% c-line-end %} is unsigned) and results in a very large number. There are many other ways implicit conversions can cause trouble. Zig solves these problems by 1) eliminating implicit conversions unless they are guaranteed to be safe (for example, assigning a {% c-line %}u8{% c-line-end %} value to a {% c-line %}u16{% c-line-end %} variable cannot fail or lose data) and 2) giving each casting operator its own built-in function, making it easy to audit their use with simple text-based tools. So, let's review those built-ins and when to use them @as() {% c-line %}@as(){% c-line-end %}is only allowed when the casting operation is unambiguous and safe, meaning no information will be lost and the target type is guaranteed to be able to hold the source type. When to use: casting a compile-time integer for use in type inference: {% c-block language="zig" %} var x = 5; // not allowed - should x be signed? unsigned? what size? var x = @as(u8, 5); // type inference allows the compiler to determine that x is type u8 {% c-block-end %} When to use: casting an unsigned int to a larger signed int: {% c-block language="zig" %} var x : u8 = 5; var y = @as(i32, x); {% c-block-end %} When to use: casting an int to a larger-size int of the same sign {% c-block language="zig" %} var x : u8 = 5; var y = @as(u32, x); {% c-block-end %} @truncate() {% c-line %}@truncate(){% c-line-end %} is used to explicitly cast to a smaller-size integer with the same signedness, by removing the most-significant bits. It's the equivalent of an explicit C cast from a larger to a smaller integer of the same sign. {% c-block language="zig" %} var x = @as(u16, 513); // x in binary: 0000001000000001 var y = @truncate(u8, x); // y in binary: 00000001 {% c-block-end %} Warning: You can call {% c-line %}@truncate(){% c-line-end %} on signed integers, but you need to make sure that's really what you want to do - since {% c-line %}@truncate{% c-line-end %} always removes the most significant bits, the resulting value may or may not be negative, regardless of whether the original value was negative. @bitCast() {% c-line %}@bitCast{% c-line-end %} is used to to cast between types that are the same size, preserving the bitpattern. In other words the data in memory does not change, but its interpretation may. In the context of integer casting, this is relevant when casting between signed and unsigned types, if the source value is outside the bounds of the target type. For example, given the {% c-line %}u8{% c-line-end %} value {% c-line %}180{% c-line-end %}, the bitpattern is {% c-line %}10110100{% c-line-end %}. If we use {% c-line %} @bitCast{% c-line-end %} to convert this to an {% c-line %}i8{% c-line-end %} (assuming 2s complement notation), the value becomes {% c-line %}-76{% c-line-end %}. However if the original value were e.g. {% c-line %}100{% c-line-end %}, it would still be {% c-line %} 100{% c-line-end %} after {% c-line %}@bitCast{% c-line-end %} since {% c-line %}100{% c-line-end %} has the same bitwise representation for {% c-line %}i8{% c-line-end %} and {% c-line %}u8{% c-line-end %}. {% c-block language="zig" %} var x = @as(u8, 180); // x in binary: 10110100 (value is 180) var y = @bitCast(i8, x); // y in binary: 10110100 (value is -76) {% c-block-end %} @intCast() Finally, we have {% c-line %}@intCast{% c-line-end %}. In some ways {% c-line %}@intCast{% c-line-end %} is the dual of {% c-line %} @bitCast{% c-line-end %} - it casts between integer types at runtime, preserving the value but not necessarily the bitpattern. For example, {% c-line %}@intCast{% c-line-end %} could be used to convert an {% c-line %}i32{% c-line-end %} value of {% c-line %}3{% c-line-end %} to a {% c-line %}u8{% c-line-end %}. Since it's not always possible to do this (for example - a {% c-line %}u16{% c-line-end %} value of {% c-line %}0xFFFF{% c-line-end %} cannot be stored in a {% c-line %}u8{% c-line-end%}) {% c-line %}@intCast{% c-line-end %} can invoke safety-checked undefined behavior. In other words, if runtime safety is active (either due to the build mode or {% c-line %}@setRuntimeSafety(true){% c-line-end %}), then calling {% c-line %} @intCast{% c-line-end %} with invalid parameters will panic the program rather than leaving an unexpected value in the destination. If runtime safety is turned off, you get undefined behavior. {% c-block language="zig" %} // zig build-exe test.zig && ./test pub fn main() void { var x = @as(i16, 180); var y = @intCast(u8, x); // this is fine var z = @intCast(i8, y); // this will crash } {% c-block-end %} {% c-block language="console" %} ~ zig run test.zig thread 14280091 panic: integer cast truncated bits /Users/ehaas/test.zig:4:13: 0x1018bf5bc in main (test) var z = @intCast(i8, y); ^ /Users/ehaas/source/zig/build/lib/zig/std/start.zig:410:22: 0x1018c177c in std.start.callMain (test) root.main(); ^ /Users/ehaas/source/zig/build/lib/zig/std/start.zig:362:12: 0x1018bf767 in std.start.callMainWithArgs (test) return @call(.{ .modifier = .always_inline }, callMain, .{}); ^ /Users/ehaas/source/zig/build/lib/zig/std/start.zig:332:12: 0x1018bf6a5 in std.start.main (test) return @call(.{ .modifier = .always_inline }, callMainWithArgs, .{ @intCast(usize, c_argc), c_argv, envp }); ^ ???:?:?: 0x7fff2032e620 in ??? (???) ???:?:?: 0x0 in ??? (???) [1] 79460 abort zig run test.zig {% c-block-end %} Thus, when calling {% c-line %}@intCast{% c-line-end %} you should always check that the target type can hold the source value, unless you're certain the value will fit. What if you're not sure if the result will fit, and you don't want to check each time you add or multiply? In that case you'll want to see our next article, Preventing integer overflow in Zig Want to stay up to date on the future of firmware? Join our mailing list. [ ][Submit] Thank you! You've been added to the newsletter. Oops! Something went wrong while submitting the form. Next Chapter The hazards of C [606bd78de0] Section Chapter Published Zig Zagging The problem with pointers May 11, 2021 Incrementally porting an embedded C application to Zig, part 2 May 11, 2021 Incrementally porting an embedded C application to Zig May 11, 2021 Testing memory allocation failures with Zig April 26, 2021 Preventing integer overflow in Zig April 26, 2021 An intro to Zig's integer casting for C programmers April 29, 2021 The hazards of C April 26, 2021 Company Overview Product PricingDocumentation Support LegalPrivacy (c) 2021 Lager. All rights reserved Lager logo