diff --git a/content/posts/2024/08/zig/16Threads16,384Input-Bytes.plot.png b/content/posts/2024/08/zig/16Threads16,384Input-Bytes.plot.png new file mode 100644 index 0000000..d7c9ee3 Binary files /dev/null and b/content/posts/2024/08/zig/16Threads16,384Input-Bytes.plot.png differ diff --git a/content/posts/2024/08/zig/16Threads512Input-Bytes.plot.png b/content/posts/2024/08/zig/16Threads512Input-Bytes.plot.png new file mode 100644 index 0000000..68b6a69 Binary files /dev/null and b/content/posts/2024/08/zig/16Threads512Input-Bytes.plot.png differ diff --git a/content/posts/2024/08/zig/1Thread16,384Input-Bytes.plot.png b/content/posts/2024/08/zig/1Thread16,384Input-Bytes.plot.png new file mode 100644 index 0000000..3514f71 Binary files /dev/null and b/content/posts/2024/08/zig/1Thread16,384Input-Bytes.plot.png differ diff --git a/content/posts/2024/08/zig/1Thread512Input-Bytes.plot.png b/content/posts/2024/08/zig/1Thread512Input-Bytes.plot.png new file mode 100644 index 0000000..fc841fb Binary files /dev/null and b/content/posts/2024/08/zig/1Thread512Input-Bytes.plot.png differ diff --git a/content/posts/2024/08/zig/app-capy.png b/content/posts/2024/08/zig/app-capy.png new file mode 100644 index 0000000..5e93be8 Binary files /dev/null and b/content/posts/2024/08/zig/app-capy.png differ diff --git a/content/posts/2024/08/zig/app-wasm.png b/content/posts/2024/08/zig/app-wasm.png new file mode 100644 index 0000000..425d2d6 Binary files /dev/null and b/content/posts/2024/08/zig/app-wasm.png differ diff --git a/content/posts/2024/08/zig/binary-sizes.log2.plot.png b/content/posts/2024/08/zig/binary-sizes.log2.plot.png new file mode 100644 index 0000000..09ddaaf Binary files /dev/null and b/content/posts/2024/08/zig/binary-sizes.log2.plot.png differ diff --git a/content/posts/2024/08/zig/binary-sizes.plot.png b/content/posts/2024/08/zig/binary-sizes.plot.png new file mode 100644 index 0000000..694f259 Binary files /dev/null and b/content/posts/2024/08/zig/binary-sizes.plot.png differ diff --git a/content/posts/2024/08/zig/code/Md5.zig b/content/posts/2024/08/zig/code/Md5.zig new file mode 100644 index 0000000..380b85f --- /dev/null +++ b/content/posts/2024/08/zig/code/Md5.zig @@ -0,0 +1,233 @@ +/// Md5 Implementation following the Pseudocode on https://en.wikipedia.org/wiki/MD5 +const std = @import("std"); +const FixedBufferAllocator = @import("std").heap.FixedBufferAllocator; +const time = @import("std").time; + +const init_a: u32 = 0x67452301; +const init_b: u32 = 0xefcdab89; +const init_c: u32 = 0x98badcfe; +const init_d: u32 = 0x10325476; + +const s_table: [64]u5 = init: { + const shift_table = .{ + [_]u5{ 7, 12, 17, 22 }, // + [_]u5{ 5, 9, 14, 20 }, // + [_]u5{ 4, 11, 16, 23 }, // + [_]u5{ 6, 10, 15, 21 }, // + }; + + var result: [64]u5 = undefined; + for (0..4) |table_index| { + for (table_index * 16..(table_index + 1) * 16) |i| { + result[i] = shift_table[table_index][i % 4]; + } + } + break :init result; +}; + +const k_table = init: { + var result: [64]u32 = undefined; + for (&result, 0..) |*p, i| { + p.* = @as(u32, @floor((1 << 32) * @abs(@sin(@as(f64, i) + 1)))); + } + break :init result; +}; + +/// Computes the md5 hash using optimizations for short (length 0-13) inputs. +pub fn computeMd5_shortU32(message: []const u32, hash: *[4]u32) !void { + if (message.len > 13) return error.MessageTooLong; + + var block: [16]u32 = undefined; + @memcpy(block[0..message.len], message); + block[message.len] = 0x80; + for (message.len + 1..16) |i| { + block[i] = 0; + } + block[14] = @truncate(message.len * 32); + + var a = init_a; + var b = init_b; + var c = init_c; + var d = init_d; + + inline for (0..4) |round| { + inline for (round * 16..(round + 1) * 16) |i| { + const g = comptime switch (round) { + 0 => i, + 1 => ((5 * i) + 1) & 0x0f, + 2 => ((3 * i) + 5) & 0x0f, + 3 => (7 * i) & 0x0f, + else => unreachable, + }; + const f = switch (round) { + inline 0 => ((b & c) | (~b & d)), + inline 1 => ((d & b) | (~d & c)), + inline 2 => (b ^ c ^ d), + inline 3 => (c ^ (b | ~d)), + else => unreachable, + } +% block[g] +% a +% k_table[i]; + + a = d; + d = c; + c = b; + const shift: u5 = s_table[i]; + b = b +% ((f << shift) | (f >> (~shift +% 1))); + } + } + + hash[0] = init_a +% a; + hash[1] = init_b +% b; + hash[2] = init_c +% c; + hash[3] = init_d +% d; +} + +export fn c_computeMd5(message: [*c]const u8, len: usize, hash: *[4]u32) void { + computeMd5(message[0..len], hash); +} + +pub fn computeMd5(message: []const u8, hash: *[4]u32) void { + const num_extra: u8 = @truncate(message.len % 64); + const num_msg_blocks = message.len / 64; + var pad_len: usize = undefined; + if (num_extra >= 56) { + pad_len = 32; + } else { + pad_len = 16; + } + var pad_block = [_]u32{0} ** 32; + for (0..num_extra) |i| { + pad_block[i / 4] |= (@as(u32, message[message.len - num_extra + i]) << (@as(u5, @truncate(i % 4)) * 8)); + } + pad_block[num_extra / 4] |= @as(u32, 0x80) << (@as(u5, @truncate(num_extra % 4)) * 8); + + // Die letzten 8 bytes mit der urspruenglichen Laenge der Nachricht befuellen + const original_length_bits: u64 = message.len * 8; + pad_block[pad_len - 2] = @truncate(original_length_bits & 0xffffffff); + pad_block[pad_len - 1] = @truncate((original_length_bits >> 32) & 0xffffffff); + + hash[0] = init_a; + hash[1] = init_b; + hash[2] = init_c; + hash[3] = init_d; + + for (0..num_msg_blocks) |block_index| { + const buffer: [*]const u32 = @ptrCast(@alignCast(message[(block_index * 64)..((block_index + 1) * 64)])); + var a = hash[0]; + var b = hash[1]; + var c = hash[2]; + var d = hash[3]; + + inline for (0..4) |round| { + inline for (round * 16..(round + 1) * 16) |i| { + const g = comptime switch (round) { + 0 => undefined, + 1 => ((5 * i) + 1) & 0x0f, + 2 => ((3 * i) + 5) & 0x0f, + 3 => (7 * i) & 0x0f, + else => unreachable, + }; + const f = switch (round) { + inline 0 => ((b & c) | (~b & d)) +% buffer[i], + inline 1 => ((d & b) | (~d & c)) +% buffer[g], + inline 2 => (b ^ c ^ d) +% buffer[g], + inline 3 => (c ^ (b | ~d)) +% buffer[g], + else => unreachable, + } +% a +% k_table[i]; + + a = d; + d = c; + c = b; + const shift: u5 = s_table[i]; + b = b +% ((f << shift) | (f >> (~shift +% 1))); + } + } + + hash[0] +%= a; + hash[1] +%= b; + hash[2] +%= c; + hash[3] +%= d; + } + + const num_padding_blocks = pad_len / 16; + for (0..num_padding_blocks) |pad_index| { + // Letze Runde auf dem Padding drehen + const pad_block_i = 16 * pad_index; + var a = hash[0]; + var b = hash[1]; + var c = hash[2]; + var d = hash[3]; + + inline for (0..4) |round| { + inline for (round * 16..(round + 1) * 16) |i| { + const g = comptime switch (round) { + 0 => undefined, + 1 => ((5 * i) + 1) & 0x0f, + 2 => ((3 * i) + 5) & 0x0f, + 3 => (7 * i) & 0x0f, + else => unreachable, + }; + const f = switch (round) { + inline 0 => ((b & c) | (~b & d)) +% pad_block[i + pad_block_i], + inline 1 => ((d & b) | (~d & c)) +% pad_block[g + pad_block_i], + inline 2 => (b ^ c ^ d) +% pad_block[g + pad_block_i], + inline 3 => (c ^ (b | ~d)) +% pad_block[g + pad_block_i], + else => unreachable, + } +% a +% k_table[i]; + + a = d; + d = c; + c = b; + const shift: u5 = s_table[i]; + b = b +% ((f << shift) | (f >> (~shift +% 1))); + } + } + + hash[0] +%= a; + hash[1] +%= b; + hash[2] +%= c; + hash[3] +%= d; + } +} + +pub fn toHexString(hash: *[4]u32, result: *[32]u8) !void { + _ = try std.fmt.bufPrint( + result, + "{x:0>8}{x:0>8}{x:0>8}{x:0>8}", + .{ @byteSwap(hash[0]), @byteSwap(hash[1]), @byteSwap(hash[2]), @byteSwap(hash[3]) }, + ); +} + +test "message of 1016 bytes : matches precomputed hash" { + const message = [_]u8{ 0xff, 0xdd, 0x2b, 0x9b, 0x7f, 0x45, 0xde, 0x3a, 0xac, 0x32, 0xf1, 0x92, 0xb8, 0x0b, 0xb2, 0xc7, 0x7e, 0xca, 0x53, 0x05, 0x65, 0x01, 0x1b, 0x5f, 0x93, 0x34, 0xcb, 0x6e, 0x1f, 0xb1, 0xde, 0xdd, 0xb1, 0xf4, 0x77, 0x93, 0x46, 0xd6, 0xea, 0x65, 0x8f, 0x0a, 0x15, 0xde, 0x9b, 0x76, 0x1a, 0xef, 0xd7, 0x2a, 0x1d, 0xc9, 0x59, 0x77, 0xe9, 0x61, 0xae, 0x40, 0xf5, 0x06, 0xe7, 0x90, 0x2b, 0xf7, 0xc9, 0xdf, 0xf6, 0xed, 0x97, 0x5c, 0x37, 0xb8, 0x58, 0xe2, 0x99, 0xd3, 0x19, 0x6a, 0x14, 0xef, 0x4c, 0x53, 0xfd, 0xe6, 0x7e, 0x66, 0x94, 0x7c, 0xc4, 0x4c, 0x62, 0x09, 0x06, 0x75, 0xe6, 0xd7, 0xa6, 0x6b, 0x4d, 0xd4, 0x12, 0x7a, 0x9a, 0xb3, 0xad, 0x34, 0xaf, 0x6f, 0xe5, 0x52, 0x64, 0x80, 0x73, 0x14, 0x34, 0xd0, 0x24, 0x59, 0x37, 0x25, 0x44, 0xd3, 0xc0, 0x3c, 0x32, 0x2f, 0xee, 0x84, 0xb8, 0x36, 0x90, 0xaf, 0xd6, 0x3a, 0xb5, 0x55, 0x68, 0x6e, 0x16, 0xe9, 0xd4, 0x75, 0x7d, 0xcb, 0x38, 0x9e, 0xd3, 0x12, 0x5d, 0xe8, 0x95, 0xd2, 0x13, 0xb0, 0x41, 0x41, 0x81, 0x47, 0x23, 0x32, 0x06, 0x60, 0x9c, 0x52, 0x08, 0x62, 0x7c, 0xaa, 0x17, 0x81, 0x53, 0xf6, 0x61, 0x50, 0xcf, 0x8f, 0x5b, 0x96, 0x65, 0x90, 0x82, 0x50, 0x9d, 0x50, 0x0b, 0x5b, 0x2f, 0xdd, 0xf1, 0xe4, 0x5a, 0xab, 0xa9, 0x85, 0x67, 0x3c, 0x53, 0xc6, 0xee, 0x84, 0x7f, 0x45, 0x6d, 0x90, 0x2a, 0x89, 0x2e, 0xb6, 0xf0, 0x7e, 0xac, 0xd2, 0xac, 0x15, 0xd4, 0x16, 0x76, 0xf2, 0x41, 0xbe, 0xe1, 0x45, 0x96, 0xd4, 0x4f, 0x13, 0xe1, 0x1b, 0x63, 0xbe, 0x8a, 0x70, 0x15, 0xae, 0x67, 0x47, 0x70, 0x70, 0x3b, 0xa0, 0xdd, 0x6c, 0xbb, 0x67, 0x3b, 0xb8, 0x69, 0x0a, 0x97, 0x15, 0x7a, 0x9c, 0x96, 0xc8, 0x80, 0x8f, 0xb9, 0xe0, 0x3e, 0x98, 0x69, 0x9d, 0x54, 0xee, 0x83, 0xc9, 0xe1, 0xd7, 0x9b, 0x10, 0xe8, 0x20, 0x5f, 0x0e, 0xde, 0x5b, 0x53, 0x79, 0xd8, 0xd9, 0x21, 0x8f, 0x8c, 0xba, 0xb7, 0x83, 0x76, 0x52, 0x3d, 0x20, 0xcf, 0x21, 0x9a, 0x3f, 0x80, 0xd8, 0x29, 0x95, 0x81, 0x74, 0xb4, 0xce, 0x7f, 0xac, 0x85, 0x01, 0x0b, 0x7d, 0x5a, 0x2f, 0x4b, 0x2c, 0xbe, 0xcf, 0x28, 0x77, 0xd2, 0x6e, 0x95, 0x43, 0x44, 0x9d, 0xa9, 0x38, 0xb0, 0xa6, 0x0f, 0x58, 0x9a, 0x9f, 0x17, 0x21, 0xea, 0x9a, 0xd2, 0xf8, 0xb3, 0x7a, 0xda, 0x10, 0x7c, 0x2a, 0x39, 0x33, 0x89, 0x38, 0xa4, 0x01, 0xd5, 0x1a, 0x9b, 0xb8, 0xcd, 0x4a, 0xda, 0x27, 0x67, 0xa6, 0xf8, 0x2f, 0x1b, 0x7b, 0xa4, 0x90, 0x0e, 0xb1, 0x65, 0xf8, 0x21, 0x3b, 0x3b, 0x0b, 0x27, 0x29, 0xea, 0xc8, 0x94, 0x35, 0xd4, 0x4b, 0x95, 0xfa, 0x90, 0x92, 0xe9, 0x21, 0xdf, 0xac, 0x01, 0xa1, 0xa3, 0x41, 0x27, 0x00, 0xaa, 0x32, 0x16, 0xe9, 0xcb, 0xd5, 0x5a, 0xd7, 0x5f, 0xa1, 0x8f, 0x5c, 0xd4, 0x2a, 0x63, 0x28, 0x21, 0x02, 0x4a, 0x3b, 0x89, 0x0a, 0x3d, 0x50, 0x93, 0x64, 0xc3, 0x53, 0xd5, 0x16, 0xed, 0x5f, 0x72, 0x7f, 0x9d, 0x97, 0x87, 0x56, 0xbe, 0x4d, 0x9e, 0x49, 0x2b, 0x7d, 0x06, 0x8b, 0x6f, 0x22, 0x1c, 0xaa, 0x11, 0x2d, 0xee, 0x2a, 0x83, 0xb6, 0xe1, 0x1f, 0x76, 0xb3, 0x55, 0xed, 0xd0, 0x06, 0x37, 0x1e, 0x61, 0xfe, 0x17, 0xd7, 0x00, 0xed, 0x3b, 0xd6, 0x36, 0xef, 0x0f, 0x28, 0xa5, 0x6f, 0x21, 0x07, 0x69, 0x3b, 0x26, 0x5f, 0x09, 0xb7, 0xd8, 0x73, 0xe7, 0xd8, 0xe5, 0x38, 0xe6, 0x4e, 0x51, 0x33, 0xc4, 0x4c, 0x14, 0xe4, 0xa8, 0x8a, 0x5f, 0x5e, 0x2f, 0xf7, 0x18, 0x57, 0xe4, 0xf0, 0xd1, 0x8b, 0xb7, 0x75, 0x8e, 0x39, 0x30, 0x54, 0xf9, 0xf1, 0x67, 0xfd, 0x56, 0xd0, 0x7a, 0x02, 0xdf, 0xff, 0x4d, 0x30, 0x76, 0x4e, 0x91, 0x09, 0xa4, 0x15, 0x59, 0xfe, 0x1c, 0xf0, 0x83, 0x46, 0x6d, 0xee, 0xc2, 0xa7, 0x0d, 0xce, 0xee, 0x9e, 0xdb, 0x76, 0x4b, 0x87, 0xd4, 0x02, 0x8b, 0xae, 0x84, 0xff, 0x24, 0xf5, 0x75, 0x7b, 0x6c, 0x30, 0x05, 0x08, 0x3c, 0xe7, 0x78, 0xd8, 0xc8, 0xca, 0x56, 0xed, 0x37, 0x02, 0xc2, 0xc5, 0x4d, 0x75, 0x82, 0xe6, 0x6d, 0x26, 0xb3, 0xa2, 0x4a, 0xc5, 0x37, 0xdc, 0x99, 0x36, 0x9a, 0x9a, 0x3c, 0xd0, 0x20, 0xed, 0x22, 0x72, 0x3f, 0xfc, 0x71, 0x5c, 0x4f, 0xf0, 0x26, 0x53, 0x8b, 0x9e, 0xe9, 0x90, 0xf8, 0x7e, 0xa9, 0x00, 0x57, 0xab, 0xa0, 0x90, 0x3a, 0xe3, 0x9d, 0x1f, 0xdd, 0x5e, 0xd8, 0x3a, 0x06, 0xeb, 0x9b, 0xb1, 0x05, 0xe3, 0x2f, 0xf9, 0x2d, 0x7a, 0x83, 0xce, 0x73, 0x5f, 0x76, 0xe5, 0x10, 0xa3, 0x61, 0xee, 0x02, 0x88, 0xa6, 0xf4, 0xb3, 0x21, 0x47, 0x3b, 0x09, 0xd2, 0x6d, 0x09, 0x0e, 0x2e, 0xf9, 0x8e, 0x19, 0x31, 0x27, 0x28, 0x21, 0x4e, 0xa6, 0x66, 0x39, 0x69, 0x38, 0x4f, 0x64, 0xa0, 0xfa, 0xc6, 0xa2, 0x29, 0x2a, 0x02, 0x8d, 0xf2, 0x50, 0xd5, 0xaf, 0xdb, 0xf9, 0x32, 0x74, 0xb0, 0xd5, 0x5e, 0x54, 0xd2, 0x70, 0x34, 0xc3, 0xc2, 0xcb, 0xb4, 0x79, 0xf0, 0x8a, 0x91, 0xe6, 0x7c, 0x5d, 0xbd, 0xe2, 0x09, 0x31, 0x59, 0x7e, 0x92, 0x97, 0xea, 0x5a, 0x1f, 0xd1, 0x72, 0xae, 0x69, 0x88, 0x85, 0xfb, 0x3f, 0x97, 0x37, 0x8b, 0xfc, 0x93, 0x71, 0x42, 0x1d, 0x58, 0xa5, 0xac, 0x49, 0x76, 0xcc, 0x25, 0xf2, 0x6f, 0x5b, 0x77, 0x42, 0x9f, 0xa9, 0xdc, 0xe7, 0x1c, 0xff, 0xb8, 0x62, 0xb3, 0x2b, 0x61, 0x4f, 0x1d, 0xf4, 0x64, 0xb0, 0x81, 0x7d, 0x08, 0xc2, 0xa4, 0x57, 0xbe, 0xd1, 0xb8, 0x33, 0x2d, 0xdd, 0xd1, 0x2e, 0xc9, 0x9b, 0x0c, 0x83, 0xde, 0xef, 0x55, 0x8f, 0xd3, 0xd9, 0xb7, 0x02, 0x37, 0x3b, 0xf7, 0x7f, 0x79, 0x9e, 0x7e, 0x79, 0x47, 0x1a, 0x04, 0x38, 0x0d, 0xbd, 0x98, 0x03, 0x83, 0x92, 0xf3, 0x8f, 0xbf, 0xfc, 0x2a, 0xca, 0x11, 0xfa, 0xcd, 0xb2, 0x68, 0xb5, 0x3e, 0x19, 0xc5, 0x25, 0x0e, 0xbf, 0x72, 0x2c, 0x56, 0xef, 0x4b, 0x07, 0x22, 0xd7, 0xea, 0x68, 0x0e, 0xba, 0xf9, 0xaa, 0xab, 0x74, 0x91, 0x5f, 0x66, 0xe5, 0xa3, 0x55, 0x26, 0x77, 0x19, 0x03, 0xb0, 0xbf, 0x76, 0x26, 0xbd, 0xad, 0xc3, 0x65, 0x39, 0x0a, 0xea, 0xcf, 0x3b, 0x55, 0x98, 0xc8, 0x17, 0xef, 0xd2, 0x85, 0x7c, 0x7e, 0xd3, 0x80, 0xd2, 0x0e, 0xd8, 0x9d, 0xd9, 0x5e, 0x27, 0x25, 0xbf, 0x7c, 0x6f, 0x11, 0x56, 0x7b, 0xbf, 0x2f, 0x3b, 0xdf, 0x22, 0x7e, 0xa1, 0x40, 0xc9, 0x99, 0x35, 0x11, 0x5f, 0x56, 0x23, 0x3d, 0xd6, 0xf5, 0xa5, 0xa4, 0xe8, 0x3d, 0xb1, 0xa7, 0xb6, 0x9d, 0xdc, 0x11, 0x1a, 0x8c, 0x9d, 0x74, 0x89, 0xae, 0xd4, 0x45, 0xfa, 0x93, 0x39, 0xc4, 0x33, 0x1a, 0xed, 0x31, 0x28, 0xda, 0x0f, 0x41, 0x9a, 0xb6, 0xb9, 0x09, 0x92, 0x84, 0x4e, 0xe1, 0xa2, 0x1c, 0xbf, 0x8f, 0xe3, 0xad, 0x4b, 0xbe, 0x27, 0xaf, 0x60, 0x40, 0xff, 0xf6, 0xfe, 0x25, 0x88, 0x34, 0x48, 0xe2, 0xf9, 0x77, 0xe0, 0xfd, 0x43, 0xaa, 0x0c, 0x16, 0xe9, 0x5c, 0xa3, 0xd3, 0xdf, 0x91, 0xe3, 0x7a, 0x94, 0x60, 0x8c, 0xdf, 0x42, 0x6a, 0x9f, 0xf4, 0x48, 0xa0, 0x7c, 0x95, 0x69, 0xcc, 0x53, 0x1f, 0x58, 0x80, 0xef, 0x7c, 0x39, 0xf8, 0x3f, 0x17, 0x5f, 0x14, 0x79, 0xb5, 0xa0, 0x8b, 0x07, 0x00, 0xfa, 0xef }; + var hash = [_]u32{0} ** 4; + computeMd5(&message, &hash); + const expected_hash = [4]u32{ 3824439167, 125395705, 3767690293, 3983405441 }; + try std.testing.expectEqual(expected_hash, hash); +} + +test "message of 1016 bytes : matches builtin md5" { + const message = [_]u8{ 0xff, 0xdd, 0x2b, 0x9b, 0x7f, 0x45, 0xde, 0x3a, 0xac, 0x32, 0xf1, 0x92, 0xb8, 0x0b, 0xb2, 0xc7, 0x7e, 0xca, 0x53, 0x05, 0x65, 0x01, 0x1b, 0x5f, 0x93, 0x34, 0xcb, 0x6e, 0x1f, 0xb1, 0xde, 0xdd, 0xb1, 0xf4, 0x77, 0x93, 0x46, 0xd6, 0xea, 0x65, 0x8f, 0x0a, 0x15, 0xde, 0x9b, 0x76, 0x1a, 0xef, 0xd7, 0x2a, 0x1d, 0xc9, 0x59, 0x77, 0xe9, 0x61, 0xae, 0x40, 0xf5, 0x06, 0xe7, 0x90, 0x2b, 0xf7, 0xc9, 0xdf, 0xf6, 0xed, 0x97, 0x5c, 0x37, 0xb8, 0x58, 0xe2, 0x99, 0xd3, 0x19, 0x6a, 0x14, 0xef, 0x4c, 0x53, 0xfd, 0xe6, 0x7e, 0x66, 0x94, 0x7c, 0xc4, 0x4c, 0x62, 0x09, 0x06, 0x75, 0xe6, 0xd7, 0xa6, 0x6b, 0x4d, 0xd4, 0x12, 0x7a, 0x9a, 0xb3, 0xad, 0x34, 0xaf, 0x6f, 0xe5, 0x52, 0x64, 0x80, 0x73, 0x14, 0x34, 0xd0, 0x24, 0x59, 0x37, 0x25, 0x44, 0xd3, 0xc0, 0x3c, 0x32, 0x2f, 0xee, 0x84, 0xb8, 0x36, 0x90, 0xaf, 0xd6, 0x3a, 0xb5, 0x55, 0x68, 0x6e, 0x16, 0xe9, 0xd4, 0x75, 0x7d, 0xcb, 0x38, 0x9e, 0xd3, 0x12, 0x5d, 0xe8, 0x95, 0xd2, 0x13, 0xb0, 0x41, 0x41, 0x81, 0x47, 0x23, 0x32, 0x06, 0x60, 0x9c, 0x52, 0x08, 0x62, 0x7c, 0xaa, 0x17, 0x81, 0x53, 0xf6, 0x61, 0x50, 0xcf, 0x8f, 0x5b, 0x96, 0x65, 0x90, 0x82, 0x50, 0x9d, 0x50, 0x0b, 0x5b, 0x2f, 0xdd, 0xf1, 0xe4, 0x5a, 0xab, 0xa9, 0x85, 0x67, 0x3c, 0x53, 0xc6, 0xee, 0x84, 0x7f, 0x45, 0x6d, 0x90, 0x2a, 0x89, 0x2e, 0xb6, 0xf0, 0x7e, 0xac, 0xd2, 0xac, 0x15, 0xd4, 0x16, 0x76, 0xf2, 0x41, 0xbe, 0xe1, 0x45, 0x96, 0xd4, 0x4f, 0x13, 0xe1, 0x1b, 0x63, 0xbe, 0x8a, 0x70, 0x15, 0xae, 0x67, 0x47, 0x70, 0x70, 0x3b, 0xa0, 0xdd, 0x6c, 0xbb, 0x67, 0x3b, 0xb8, 0x69, 0x0a, 0x97, 0x15, 0x7a, 0x9c, 0x96, 0xc8, 0x80, 0x8f, 0xb9, 0xe0, 0x3e, 0x98, 0x69, 0x9d, 0x54, 0xee, 0x83, 0xc9, 0xe1, 0xd7, 0x9b, 0x10, 0xe8, 0x20, 0x5f, 0x0e, 0xde, 0x5b, 0x53, 0x79, 0xd8, 0xd9, 0x21, 0x8f, 0x8c, 0xba, 0xb7, 0x83, 0x76, 0x52, 0x3d, 0x20, 0xcf, 0x21, 0x9a, 0x3f, 0x80, 0xd8, 0x29, 0x95, 0x81, 0x74, 0xb4, 0xce, 0x7f, 0xac, 0x85, 0x01, 0x0b, 0x7d, 0x5a, 0x2f, 0x4b, 0x2c, 0xbe, 0xcf, 0x28, 0x77, 0xd2, 0x6e, 0x95, 0x43, 0x44, 0x9d, 0xa9, 0x38, 0xb0, 0xa6, 0x0f, 0x58, 0x9a, 0x9f, 0x17, 0x21, 0xea, 0x9a, 0xd2, 0xf8, 0xb3, 0x7a, 0xda, 0x10, 0x7c, 0x2a, 0x39, 0x33, 0x89, 0x38, 0xa4, 0x01, 0xd5, 0x1a, 0x9b, 0xb8, 0xcd, 0x4a, 0xda, 0x27, 0x67, 0xa6, 0xf8, 0x2f, 0x1b, 0x7b, 0xa4, 0x90, 0x0e, 0xb1, 0x65, 0xf8, 0x21, 0x3b, 0x3b, 0x0b, 0x27, 0x29, 0xea, 0xc8, 0x94, 0x35, 0xd4, 0x4b, 0x95, 0xfa, 0x90, 0x92, 0xe9, 0x21, 0xdf, 0xac, 0x01, 0xa1, 0xa3, 0x41, 0x27, 0x00, 0xaa, 0x32, 0x16, 0xe9, 0xcb, 0xd5, 0x5a, 0xd7, 0x5f, 0xa1, 0x8f, 0x5c, 0xd4, 0x2a, 0x63, 0x28, 0x21, 0x02, 0x4a, 0x3b, 0x89, 0x0a, 0x3d, 0x50, 0x93, 0x64, 0xc3, 0x53, 0xd5, 0x16, 0xed, 0x5f, 0x72, 0x7f, 0x9d, 0x97, 0x87, 0x56, 0xbe, 0x4d, 0x9e, 0x49, 0x2b, 0x7d, 0x06, 0x8b, 0x6f, 0x22, 0x1c, 0xaa, 0x11, 0x2d, 0xee, 0x2a, 0x83, 0xb6, 0xe1, 0x1f, 0x76, 0xb3, 0x55, 0xed, 0xd0, 0x06, 0x37, 0x1e, 0x61, 0xfe, 0x17, 0xd7, 0x00, 0xed, 0x3b, 0xd6, 0x36, 0xef, 0x0f, 0x28, 0xa5, 0x6f, 0x21, 0x07, 0x69, 0x3b, 0x26, 0x5f, 0x09, 0xb7, 0xd8, 0x73, 0xe7, 0xd8, 0xe5, 0x38, 0xe6, 0x4e, 0x51, 0x33, 0xc4, 0x4c, 0x14, 0xe4, 0xa8, 0x8a, 0x5f, 0x5e, 0x2f, 0xf7, 0x18, 0x57, 0xe4, 0xf0, 0xd1, 0x8b, 0xb7, 0x75, 0x8e, 0x39, 0x30, 0x54, 0xf9, 0xf1, 0x67, 0xfd, 0x56, 0xd0, 0x7a, 0x02, 0xdf, 0xff, 0x4d, 0x30, 0x76, 0x4e, 0x91, 0x09, 0xa4, 0x15, 0x59, 0xfe, 0x1c, 0xf0, 0x83, 0x46, 0x6d, 0xee, 0xc2, 0xa7, 0x0d, 0xce, 0xee, 0x9e, 0xdb, 0x76, 0x4b, 0x87, 0xd4, 0x02, 0x8b, 0xae, 0x84, 0xff, 0x24, 0xf5, 0x75, 0x7b, 0x6c, 0x30, 0x05, 0x08, 0x3c, 0xe7, 0x78, 0xd8, 0xc8, 0xca, 0x56, 0xed, 0x37, 0x02, 0xc2, 0xc5, 0x4d, 0x75, 0x82, 0xe6, 0x6d, 0x26, 0xb3, 0xa2, 0x4a, 0xc5, 0x37, 0xdc, 0x99, 0x36, 0x9a, 0x9a, 0x3c, 0xd0, 0x20, 0xed, 0x22, 0x72, 0x3f, 0xfc, 0x71, 0x5c, 0x4f, 0xf0, 0x26, 0x53, 0x8b, 0x9e, 0xe9, 0x90, 0xf8, 0x7e, 0xa9, 0x00, 0x57, 0xab, 0xa0, 0x90, 0x3a, 0xe3, 0x9d, 0x1f, 0xdd, 0x5e, 0xd8, 0x3a, 0x06, 0xeb, 0x9b, 0xb1, 0x05, 0xe3, 0x2f, 0xf9, 0x2d, 0x7a, 0x83, 0xce, 0x73, 0x5f, 0x76, 0xe5, 0x10, 0xa3, 0x61, 0xee, 0x02, 0x88, 0xa6, 0xf4, 0xb3, 0x21, 0x47, 0x3b, 0x09, 0xd2, 0x6d, 0x09, 0x0e, 0x2e, 0xf9, 0x8e, 0x19, 0x31, 0x27, 0x28, 0x21, 0x4e, 0xa6, 0x66, 0x39, 0x69, 0x38, 0x4f, 0x64, 0xa0, 0xfa, 0xc6, 0xa2, 0x29, 0x2a, 0x02, 0x8d, 0xf2, 0x50, 0xd5, 0xaf, 0xdb, 0xf9, 0x32, 0x74, 0xb0, 0xd5, 0x5e, 0x54, 0xd2, 0x70, 0x34, 0xc3, 0xc2, 0xcb, 0xb4, 0x79, 0xf0, 0x8a, 0x91, 0xe6, 0x7c, 0x5d, 0xbd, 0xe2, 0x09, 0x31, 0x59, 0x7e, 0x92, 0x97, 0xea, 0x5a, 0x1f, 0xd1, 0x72, 0xae, 0x69, 0x88, 0x85, 0xfb, 0x3f, 0x97, 0x37, 0x8b, 0xfc, 0x93, 0x71, 0x42, 0x1d, 0x58, 0xa5, 0xac, 0x49, 0x76, 0xcc, 0x25, 0xf2, 0x6f, 0x5b, 0x77, 0x42, 0x9f, 0xa9, 0xdc, 0xe7, 0x1c, 0xff, 0xb8, 0x62, 0xb3, 0x2b, 0x61, 0x4f, 0x1d, 0xf4, 0x64, 0xb0, 0x81, 0x7d, 0x08, 0xc2, 0xa4, 0x57, 0xbe, 0xd1, 0xb8, 0x33, 0x2d, 0xdd, 0xd1, 0x2e, 0xc9, 0x9b, 0x0c, 0x83, 0xde, 0xef, 0x55, 0x8f, 0xd3, 0xd9, 0xb7, 0x02, 0x37, 0x3b, 0xf7, 0x7f, 0x79, 0x9e, 0x7e, 0x79, 0x47, 0x1a, 0x04, 0x38, 0x0d, 0xbd, 0x98, 0x03, 0x83, 0x92, 0xf3, 0x8f, 0xbf, 0xfc, 0x2a, 0xca, 0x11, 0xfa, 0xcd, 0xb2, 0x68, 0xb5, 0x3e, 0x19, 0xc5, 0x25, 0x0e, 0xbf, 0x72, 0x2c, 0x56, 0xef, 0x4b, 0x07, 0x22, 0xd7, 0xea, 0x68, 0x0e, 0xba, 0xf9, 0xaa, 0xab, 0x74, 0x91, 0x5f, 0x66, 0xe5, 0xa3, 0x55, 0x26, 0x77, 0x19, 0x03, 0xb0, 0xbf, 0x76, 0x26, 0xbd, 0xad, 0xc3, 0x65, 0x39, 0x0a, 0xea, 0xcf, 0x3b, 0x55, 0x98, 0xc8, 0x17, 0xef, 0xd2, 0x85, 0x7c, 0x7e, 0xd3, 0x80, 0xd2, 0x0e, 0xd8, 0x9d, 0xd9, 0x5e, 0x27, 0x25, 0xbf, 0x7c, 0x6f, 0x11, 0x56, 0x7b, 0xbf, 0x2f, 0x3b, 0xdf, 0x22, 0x7e, 0xa1, 0x40, 0xc9, 0x99, 0x35, 0x11, 0x5f, 0x56, 0x23, 0x3d, 0xd6, 0xf5, 0xa5, 0xa4, 0xe8, 0x3d, 0xb1, 0xa7, 0xb6, 0x9d, 0xdc, 0x11, 0x1a, 0x8c, 0x9d, 0x74, 0x89, 0xae, 0xd4, 0x45, 0xfa, 0x93, 0x39, 0xc4, 0x33, 0x1a, 0xed, 0x31, 0x28, 0xda, 0x0f, 0x41, 0x9a, 0xb6, 0xb9, 0x09, 0x92, 0x84, 0x4e, 0xe1, 0xa2, 0x1c, 0xbf, 0x8f, 0xe3, 0xad, 0x4b, 0xbe, 0x27, 0xaf, 0x60, 0x40, 0xff, 0xf6, 0xfe, 0x25, 0x88, 0x34, 0x48, 0xe2, 0xf9, 0x77, 0xe0, 0xfd, 0x43, 0xaa, 0x0c, 0x16, 0xe9, 0x5c, 0xa3, 0xd3, 0xdf, 0x91, 0xe3, 0x7a, 0x94, 0x60, 0x8c, 0xdf, 0x42, 0x6a, 0x9f, 0xf4, 0x48, 0xa0, 0x7c, 0x95, 0x69, 0xcc, 0x53, 0x1f, 0x58, 0x80, 0xef, 0x7c, 0x39, 0xf8, 0x3f, 0x17, 0x5f, 0x14, 0x79, 0xb5, 0xa0, 0x8b, 0x07, 0x00, 0xfa, 0xef }; + + var hash = [_]u32{0} ** 4; + computeMd5(&message, &hash); + + var builtin_hash = [_]u8{0} ** 16; + std.crypto.hash.Md5.hash(&message, &builtin_hash, .{}); + const builtin_u32: [4]u32 = @as([*]const u32, @ptrCast(@alignCast(&builtin_hash)))[0..4].*; + + try std.testing.expectEqual(builtin_u32, hash); +} + +test "short message : matches builtin md5" { + const message = [_]u8{ 0xca, 0xfe, 0xba, 0xbe }; + const message_u32: []const u32 = @as([*]const u32, @ptrCast(@alignCast(&message)))[0..1]; + + var hash = [_]u32{0} ** 4; + try computeMd5_shortU32(message_u32, &hash); + + var builtin_hash = [_]u8{0} ** 16; + std.crypto.hash.Md5.hash(&message, &builtin_hash, .{}); + const builtin_u32: [4]u32 = @as([*]const u32, @ptrCast(@alignCast(&builtin_hash)))[0..4].*; + + try std.testing.expectEqual(builtin_u32, hash); +} diff --git a/content/posts/2024/08/zig/code/build.zig b/content/posts/2024/08/zig/code/build.zig new file mode 100644 index 0000000..854f5cc --- /dev/null +++ b/content/posts/2024/08/zig/code/build.zig @@ -0,0 +1,176 @@ +const std = @import("std"); +const Build = std.Build; +const ResolvedTarget = std.Build.ResolvedTarget; +const OptimizeMode = std.builtin.OptimizeMode; + +const BuildConfig = struct { + md5_module: *std.Build.Module, + target: ResolvedTarget, + optimize: OptimizeMode, + doRun: bool, + }; +var buildConfig: BuildConfig = undefined; + +fn add_build_lib(b: *Build) !void { + const make_lib = b.option(bool, "make-lib", "Compile a static library that computes md5 hashes.") orelse false; + if (!make_lib) { + return; + } + + const lib = b.addStaticLibrary(.{ + .name = "Md5", + .root_source_file = b.path("src/Md5.zig"), + .target = buildConfig.target, + .optimize = buildConfig.optimize, + }); + const install_step = b.addInstallArtifact(lib, .{}); + b.getInstallStep().dependOn(&install_step.step); +} + +fn add_build_cli(b: *Build) !void { + const make_cli = b.option(bool, "make-cli", "Compile a Command-Line Application that prints the md5 hashes of all inputs to the console.") orelse false; + if (!make_cli) { + return; + } + + const exe = b.addExecutable(.{ + .name = "Md5-Cli", + .root_source_file = b.path("src/app/cli/Main.zig"), + .target = buildConfig.target, + .optimize = buildConfig.optimize, + }); + exe.root_module.addImport("../../Md5.zig", buildConfig.md5_module); + + const install_step = b.addInstallArtifact(exe, .{}); + b.getInstallStep().dependOn(&install_step.step); +} + +fn add_build_capy(b: *Build) !void { + const make_capy = b.option(bool, "make-capy", "Compile a GUI-based Application that displays the hash for text messages. (Using Capy)") orelse false; + if (!make_capy) { + return; + } + + const exe = b.addExecutable(.{ + .name = "Md5-Capy", + .root_source_file = b.path("src/app/capy-gui/Main.zig"), + .target = buildConfig.target, + .optimize = buildConfig.optimize, + }); + exe.root_module.addImport("../../Md5.zig", buildConfig.md5_module); + + // register dependencies + if (b.lazyDependency("capy", .{ + .target = buildConfig.target, + .optimize = buildConfig.optimize, + .app_name = @as([]const u8, "Md5-Gui-Capy"), + })) |capy| { + exe.root_module.addImport("capy", capy.module("capy")); + } + + const install_step = b.addInstallArtifact(exe, .{}); + b.getInstallStep().dependOn(&install_step.step); + + if (buildConfig.doRun) { + const run_step = b.addRunArtifact(exe); + b.getInstallStep().dependOn(&run_step.step); + } +} + +fn add_build_wasm(b: *Build) !void { + const make_wasm = b.option(bool, "make-wasm", "Compile a Module that can be used in WebAssembly") orelse false; + if (!make_wasm) { + return; + } + + const target = blk: { + if (!std.Target.isWasm(buildConfig.target.result)) { + std.debug.print( + "target {s}-{s} is not a valid target for wasm build. will use wasm32-freestanding\n", + .{ @tagName(buildConfig.target.result.cpu.arch), @tagName(buildConfig.target.result.os.tag) }, + ); + break :blk b.resolveTargetQuery(.{ + .os_tag = std.Target.Os.Tag.freestanding, + .cpu_arch = std.Target.Cpu.Arch.wasm32, + }); + } else { + break :blk buildConfig.target; + } + }; + + const exe = b.addExecutable(.{ + .name = "Md5-Wasm", + .root_source_file = b.path("src/app/wasm/Main.zig"), + .target = target, + .optimize = buildConfig.optimize, + }); + // based on absolutely nothing https://ziggit.dev/t/build-wasm-using-zig-build-system-when-ther-is-no-entry-point-but-function-to-export/4364/2 + exe.rdynamic = true; + exe.entry = .disabled; + exe.import_memory = true; // https://github.com/ziglang/zig/issues/8633 + exe.root_module.addImport("../../Md5.zig", buildConfig.md5_module); + + const install_step = b.addInstallArtifact(exe, .{}); + b.getInstallStep().dependOn(&install_step.step); +} + +fn add_build_pico(b: *Build) !void { + const make_pico = b.option(bool, "make-pico", "Compile firmware for the raspberry pi pico that implements the md5 library as a usb device.") orelse false; + if (!make_pico) { + return; + } + + const MicroZig = @import("microzig/build"); + const rp2040 = @import("microzig/bsp/raspberrypi/rp2040"); + + const mz = MicroZig.init(b, .{}); + const firmware = mz.add_firmware(b, .{ + .name = "Md5-Pico", + .root_source_file = b.path("src/app/pico/Main.zig"), + .target = rp2040.boards.raspberrypi.pico, + .optimize = buildConfig.optimize, + }); + firmware.modules.app.addImport("../../Md5.zig", buildConfig.md5_module); + + mz.install_firmware(b, firmware, .{}); +} + +fn run_tests(b: *Build) !void { + const do_test = b.option(bool, "test", "Run all tests") orelse false; + if (!do_test) { + return; + } + + const tests = b.addTest(.{ + .root_source_file = b.path("src/app/wasm/Main.zig"), + .target = b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .linux }), + }); + tests.root_module.addImport("../../Md5.zig", buildConfig.md5_module); + const run_test = b.addRunArtifact(tests); + b.getInstallStep().dependOn(&run_test.step); +} + +pub fn build(b: *std.Build) !void { + buildConfig = .{ + .md5_module = undefined, + .target = b.standardTargetOptions(.{}), + .optimize = b.standardOptimizeOption(.{ + .preferred_optimize_mode = OptimizeMode.ReleaseSmall, + }), + .doRun = b.option(bool, "run", "run the compiled application(s)") orelse false, + }; + + buildConfig.md5_module = b.addModule("Md5", .{ + .root_source_file = b.path("src/Md5.zig"), + .target = buildConfig.target, + .optimize = buildConfig.optimize, + }); + + try run_tests(b); + + try add_build_lib(b); + try add_build_cli(b); + try add_build_capy(b); + try add_build_wasm(b); + try add_build_pico(b); +} diff --git a/content/posts/2024/08/zig/code/build.zig.zon b/content/posts/2024/08/zig/code/build.zig.zon new file mode 100644 index 0000000..f3713cd --- /dev/null +++ b/content/posts/2024/08/zig/code/build.zig.zon @@ -0,0 +1,18 @@ +.{ + .name = "Md5", + .version = "1.0.0", + .paths = .{""}, + .dependencies = .{ + .capy = .{ + .url = "https://github.com/capy-ui/capy/archive/d55a9ddf39ebaeed7d25279c08922bb8cf4613db.tar.gz", + .hash = "12202d8a1a46e0e78dc366ba3cb9843821b65ed3d0e110cf705bdeaba9eb8f75ac75", + .lazy = true, + }, + .@"microzig/build" = .{ + .path = "../../forks/microzig/build/", + }, + .@"microzig/bsp/raspberrypi/rp2040" = .{ + .path = "../../forks/microzig/bsp/raspberrypi/rp2040/", + }, + }, +} \ No newline at end of file diff --git a/content/posts/2024/08/zig/code/capy-Main.zig b/content/posts/2024/08/zig/code/capy-Main.zig new file mode 100644 index 0000000..2bf9bba --- /dev/null +++ b/content/posts/2024/08/zig/code/capy-Main.zig @@ -0,0 +1,64 @@ +const std = @import("std"); +const capy = @import("capy"); +const md5 = @import("../../Md5.zig"); + +// Override the allocator used by Capy +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +pub const capy_allocator = gpa.allocator(); + +const UiElements = struct { + textInput: *capy.TextField, + outputLabel: *capy.TextField, +}; +var uiElements: UiElements = undefined; + +fn onInputChanged(_: []const u8, _: ?*anyopaque) void { + var hash: [4]u32 = undefined; + md5.computeMd5(uiElements.textInput.text.get(), &hash); + + var hash_str: [32]u8 = undefined; + if (md5.toHexString(&hash, &hash_str)) { + uiElements.outputLabel.text.set(&hash_str); + } else |err| { + const err_str = std.fmt.allocPrint( + capy_allocator, + "failed to format hash. error={s}", + .{@errorName(err)}, + ) catch unreachable; + defer capy_allocator.free(err_str); + uiElements.outputLabel.text.set(err_str); + } +} + +pub fn main() !void { + try capy.backend.init(); + defer _ = gpa.deinit(); + + var window = try capy.Window.init(); + defer window.deinit(); + + uiElements = .{ + .textInput = capy.textField(.{ .text = "Hello, World!" }), + .outputLabel = capy.textField(.{ .text = "", .readOnly = true }), + }; + + _ = try uiElements.textInput.text.addChangeListener(.{ .function = &onInputChanged }); + + try window.set(capy.column(.{}, .{ + capy.row(.{}, .{ + capy.label(.{ .text = "Message (In)" }), + capy.expanded(uiElements.textInput), + }), + capy.row(.{}, .{ + capy.label(.{ .text = "Hash (Out)" }), + capy.expanded(uiElements.outputLabel), + }), + })); + + // compute initial hash + onInputChanged(undefined, undefined); + + window.setPreferredSize(400, 80); + window.show(); + capy.runEventLoop(); +} diff --git a/content/posts/2024/08/zig/code/cli-Main.zig b/content/posts/2024/08/zig/code/cli-Main.zig new file mode 100644 index 0000000..6379317 --- /dev/null +++ b/content/posts/2024/08/zig/code/cli-Main.zig @@ -0,0 +1,16 @@ +const std = @import("std"); +const md5 = @import("../../Md5.zig"); + +pub fn main() !void { + const stdOut = std.io.getStdOut().writer(); + + var arg_iterator = std.process.args(); + var hash: [4]u32 = undefined; + _ = arg_iterator.next(); // skip program name + while (arg_iterator.next()) |arg| { + md5.computeMd5(arg, &hash); + var hash_str: [32]u8 = undefined; + try md5.toHexString(&hash, &hash_str); + try stdOut.print("'{s}' => {s}\n", .{ arg, hash_str }); + } +} diff --git a/content/posts/2024/08/zig/code/pico-Main.zig b/content/posts/2024/08/zig/code/pico-Main.zig new file mode 100644 index 0000000..dc8e5a0 --- /dev/null +++ b/content/posts/2024/08/zig/code/pico-Main.zig @@ -0,0 +1,223 @@ +const std = @import("std"); +const microzig = @import("microzig"); +const md5 = @import("../../Md5.zig"); + +const rp2040 = microzig.hal; +const time = rp2040.time; +const gpio = rp2040.gpio; +const usb = rp2040.usb; + +var ep1_out_cfg: usb.EndpointConfiguration = .{ + .descriptor = &usb.EndpointDescriptor{ + .descriptor_type = usb.DescType.Endpoint, + .endpoint_address = usb.Dir.Out.endpoint(1), + .attributes = @intFromEnum(usb.TransferType.Bulk), + .max_packet_size = 64, + .interval = 0, + }, + .endpoint_control_index = 2, + .buffer_control_index = 3, + .data_buffer_index = 2, + .next_pid_1 = false, +}; + +var ep1_in_cfg: usb.EndpointConfiguration = .{ + .descriptor = &usb.EndpointDescriptor{ + .descriptor_type = usb.DescType.Endpoint, + .endpoint_address = usb.Dir.In.endpoint(1), + .attributes = @intFromEnum(usb.TransferType.Bulk), + .max_packet_size = 64, + .interval = 0, + }, + .endpoint_control_index = 1, + .buffer_control_index = 2, + .data_buffer_index = 3, + .next_pid_1 = false, +}; + +pub fn createDeviceConfig( + out_callback: *const fn (dc: *usb.DeviceConfiguration, data: []const u8) void, + in_callback: *const fn (dc: *usb.DeviceConfiguration, data: []const u8) void, +) usb.DeviceConfiguration { + ep1_out_cfg.callback = out_callback; + ep1_in_cfg.callback = in_callback; + + // construct the device configuration + return usb.DeviceConfiguration{ + .device_descriptor = &.{ + .descriptor_type = usb.DescType.Device, + .bcd_usb = 0x0110, + .device_class = 0, + .device_subclass = 0, + .device_protocol = 0, + .max_packet_size0 = 64, + .vendor = 0, + .product = 1, + .bcd_device = 0, + .manufacturer_s = 1, + .product_s = 2, + .serial_s = 0, + .num_configurations = 1, + }, + .interface_descriptor = &.{ + .descriptor_type = usb.DescType.Interface, + .interface_number = 0, + .alternate_setting = 0, + // We have two endpoints (EP0 IN/OUT don't count) + .num_endpoints = 2, + .interface_class = 0xff, + .interface_subclass = 0, + .interface_protocol = 0, + .interface_s = 0, + }, + .config_descriptor = &.{ + .descriptor_type = usb.DescType.Config, + // This is calculated via the sizes of underlying descriptors contained in this configuration. + // ConfigurationDescriptor(9) + InterfaceDescriptor(9) * 1 + EndpointDescriptor(8) * 2 + .total_length = 34, + .num_interfaces = 1, + .configuration_value = 1, + .configuration_s = 0, + .attributes = 0xc0, + .max_power = 0x32, + }, + .lang_descriptor = "\x04\x03\x09\x04", // length || string descriptor (0x03) || Engl (0x0409) + .descriptor_strings = &.{ + // ugly unicode :| + "R\x00a\x00s\x00p\x00b\x00e\x00r\x00r\x00y\x00 \x00P\x00i\x00", + "P\x00i\x00c\x00o\x00 \x00T\x00e\x00s\x00t\x00 \x00D\x00e\x00v\x00i\x00c\x00e\x00", + }, + // Here we pass all endpoints to the config + // Dont forget to pass EP0_[IN|OUT] in the order seen below! + .endpoints = .{ + &usb.EP0_OUT_CFG, + &usb.EP0_IN_CFG, + &ep1_out_cfg, + &ep1_in_cfg, + }, + }; +} + +const led = gpio.num(25); +const uart = rp2040.uart.num(0); +const baud_rate = 115200; +const uart_tx_pin = gpio.num(0); +const uart_rx_pin = gpio.num(1); + +const MessageError = error{ + MessageTooLong, +}; + +// there is no useful allocator available using microzig. +const Message = struct { + response: [64]u8 = undefined, + hash_buffer: [4]u32 = undefined, + buffer: [4096]u8 = undefined, + num_bytes_received: u32, + num_bytes_remaining: u32, +}; +var message = Message{ + .num_bytes_received = 0, + .num_bytes_remaining = 0, +}; + +fn ep1_in_callback(dc: *usb.DeviceConfiguration, _: []const u8) void { + // prepare to receive the next 64 byte message. + usb.Usb.callbacks.usb_start_rx( + dc.endpoints[2], // EP1_OUT_CFG, + 64, + ); +} + +fn ep1_out_callback(dc: *usb.DeviceConfiguration, data: []const u8) void { + handle_ep1_out_callback(dc, data) catch |err| { + // send error as result message + const fmt_msg = std.fmt.bufPrint(&message.response, "error: {s}", .{@errorName(err)}) catch unreachable; + usb.Usb.callbacks.usb_start_tx( + dc.endpoints[3], // EP1_IN_CFG, + fmt_msg, + ); + }; +} + +fn handle_ep1_out_callback(dc: *usb.DeviceConfiguration, data: []const u8) !void { + if (message.num_bytes_remaining == 0) { + // receiving new request -> first message format is .{ msg_len: u32, msg_part: [60]u8 } + const msg_len: u32 = @as(*const u32, @ptrCast(@alignCast(data[0..4]))).*; + if (msg_len > message.buffer.len) { + const fmt_msg = try std.fmt.bufPrint(&message.response, "msg too long. got len {d}.", .{msg_len}); + usb.Usb.callbacks.usb_start_tx( + dc.endpoints[3], // EP1_IN_CFG, + fmt_msg, + ); + return; + } + + message.num_bytes_remaining = @max(0, @as(i33, msg_len) - 60); + message.num_bytes_received = @min(60, msg_len); + std.mem.copyForwards(u8, &message.buffer, data[4..(4 + message.num_bytes_received)]); + } else { + // receiving request continuation + const input_len = @min(64, message.num_bytes_remaining); + message.num_bytes_remaining -= input_len; + std.mem.copyForwards(u8, message.buffer[message.num_bytes_received..], data[0..input_len]); + message.num_bytes_received += input_len; + } + + if (message.num_bytes_remaining == 0) { + // request data complete, compute hash and prepare to send + var hash: [4]u32 = message.hash_buffer; + md5.computeMd5(message.buffer[0..message.num_bytes_received], &hash); + const response: *[32]u8 = message.response[0..32]; + + try md5.toHexString(&hash, response); + usb.Usb.callbacks.usb_start_tx( + dc.endpoints[3], // EP1_IN_CFG, + response, + ); + } else { + // switch back to receiving data by sending a 0 byte response. + usb.Usb.callbacks.usb_start_tx( + dc.endpoints[3], // EP1_IN_CFG, + &.{}, + ); + } +} + +var device_configuration: usb.DeviceConfiguration = undefined; + +pub fn main() !void { + device_configuration = createDeviceConfig(ep1_out_callback, ep1_in_callback); + + led.set_function(.sio); + led.set_direction(.out); + led.put(1); + + uart.apply(.{ + .baud_rate = baud_rate, + .tx_pin = uart_tx_pin, + .rx_pin = uart_rx_pin, + .clock_config = rp2040.clock_config, + }); + + rp2040.uart.init_logger(uart); + + // First we initialize the USB clock + rp2040.usb.Usb.init_clk(); + // Then initialize the USB device using the configuration defined above + rp2040.usb.Usb.init_device(&device_configuration) catch unreachable; + var old: u64 = time.get_time_since_boot().to_us(); + var new: u64 = 0; + while (true) { + // You can now poll for USB events + rp2040.usb.Usb.task( + false, // debug output over UART [Y/n] + ) catch unreachable; + + new = time.get_time_since_boot().to_us(); + if (new - old > 500000) { + old = new; + led.toggle(); + } + } +} diff --git a/content/posts/2024/08/zig/code/sendData.py b/content/posts/2024/08/zig/code/sendData.py new file mode 100755 index 0000000..29f5f48 --- /dev/null +++ b/content/posts/2024/08/zig/code/sendData.py @@ -0,0 +1,78 @@ +# pip install pyusb + +import usb.core +import usb.util + +def toU32(x): + return bytes([x % 256, (x >> 8) % 256, (x >> 16) % 256, (x >> 24) % 256]) + +def chopMessage(str): + data = toU32(len(str)) + str.encode() + strLen = len(data) + + result = [] + i = 0 + while i < strLen: + end_pos = min(strLen, i + 64) + result.append(data[i:end_pos]) + i = end_pos + return result + +# find our device +dev = usb.core.find(idVendor=0x0000, idProduct=0x0001) + +# was it found? +if dev is None: + raise ValueError('Device not found') + +# get an endpoint instance +cfg = dev.get_active_configuration() +intf = cfg[(0, 0)] + +outep = usb.util.find_descriptor( + intf, + # match the first OUT endpoint + custom_match= \ + lambda e: \ + usb.util.endpoint_direction(e.bEndpointAddress) == \ + usb.util.ENDPOINT_OUT) + +inep = usb.util.find_descriptor( + intf, + # match the first IN endpoint + custom_match= \ + lambda e: \ + usb.util.endpoint_direction(e.bEndpointAddress) == \ + usb.util.ENDPOINT_IN) + +assert inep is not None +assert outep is not None + +# message = "Hello, World! This message exceeds the buffer............................................." +message = "j(R1wzR*y[^GxWJ5B>L{-HLETRD" +# message = "Hello, World!" +message = ''.join([message for i in range(100)]) +# print("send len: ", len(message)) +print(message) + +blocks = chopMessage(message) +for i in range(len(blocks)): + block = blocks[i] + print("sending: '", block, end="' ...", sep="") + outep.write(block) + print(" sent.") + + response = inep.read(64) + response_str = ''.join([chr(x) for x in response]) + if (i + 1) == len(blocks): + # last block sent, expect to receive hash + if len(response) == 0: + raise Exception("Device expected more data that was announced in the hashshake!") + if len(response) != 32: + raise Exception("Device reported an error: " + response_str) + else: + print("hash: " + response_str) + else: + # middle (or first) block, expect to receive 0 byte ack + if len(response) != 0: + raise Exception("Device reported an error: " + response_str) diff --git a/content/posts/2024/08/zig/code/wasm-Main.html.txt b/content/posts/2024/08/zig/code/wasm-Main.html.txt new file mode 100644 index 0000000..b54a504 --- /dev/null +++ b/content/posts/2024/08/zig/code/wasm-Main.html.txt @@ -0,0 +1,59 @@ + + + + + Md5 Wasm Example + + + +
+ + + + + + + +
+ + \ No newline at end of file diff --git a/content/posts/2024/08/zig/code/wasm-Main.zig b/content/posts/2024/08/zig/code/wasm-Main.zig new file mode 100644 index 0000000..f126c5a --- /dev/null +++ b/content/posts/2024/08/zig/code/wasm-Main.zig @@ -0,0 +1,19 @@ +const std = @import("std"); +const md5 = @import("../../Md5.zig"); + +export fn computeMd5(message: [*]const u8, len: u32, hash_buffer: [*]u8) void { + const msg_slice: []const u8 = message[0..len]; + var hash: [4]u32 = undefined; + md5.computeMd5(msg_slice, &hash); + + const buffer: *[32]u8 = hash_buffer[0..32]; + md5.toHexString(&hash, buffer) catch unreachable; +} + +test "hash matches expected" { + const msg: []const u8 = "Hello, World!"; + var buffer: [32]u8 = undefined; + computeMd5(msg.ptr, msg.len, buffer[0..32].ptr); + const expected = "65a8e27d8879283831b664bd8b7f0ad4"; + try std.testing.expectEqualStrings(expected, &buffer); +} diff --git a/content/posts/2024/08/zig/index.md b/content/posts/2024/08/zig/index.md new file mode 100644 index 0000000..f8b363b --- /dev/null +++ b/content/posts/2024/08/zig/index.md @@ -0,0 +1,1177 @@ ++++ +title = "Zig: Build tiny, high performance executables with a modern language" +date = "2024-08-19" +authors = ["eric.jarosch"] +tags = ["Teaching", "EPEA 2024", "Zig", "Cross-Platform"] ++++ + +Zig is a rapidly evolving, modern, general-purpose programming language, most notable for its performance, safety and platform-compatibility. + +In this article, I will cover how to build applications for embedded hardware, Linux/Windows, as well as Web-Browsers.\ +I will also present Benchmark comparisons with C, C++, Rust and Java. + + + +Since its first release in 2017, Zig has received 29.000 commits, and is still receiving several commits per day. + +Despite the language still being in its infancy, there already exist some notable projects written in Zig: +- [bun](https://github.com/oven-sh/bun) - that's right, the "incredibly fast" JS runtime and package manager is written in Zig. +- [xmake](https://github.com/xmake-io/xmake) - xmake is a modern C/C++ build tool that uses `.lua` files to define the build process. It also supports building projects written in Go, Zig, Rust and several other languages. +- [tigerbeetle](https://github.com/tigerbeetle/tigerbeetle) - a database written from scratch, optimized for financial transactions, where safety and performance are critical.\ +The Business-logic is contained in a single `state_machine.zig` file, which - in theory - could be replaced to create a database serving any other purpose. +- [river](https://github.com/riverwm/river) - a dynamic tiling window manager for Wayland.\ +I'm not too familiar with tiling WMs, but as far as I can tell, the key sales points are:\ +*dynamic* = new windows are arranged into the grid automatically\ +*scriptable* = the layouts, rules and appearance are configured through a `riverctl` command + +
+ +## Interesting Language Aspects and Syntax + +### Arbitrary Width Integers + +Signed and unsigned Integers can be declared using `i` and `u`. +The maximum supported width is `65535` bits. + +Examples: +```zig +const u5_min_max = [_]u5{0, 31}; +const i17_min_max = [_]i17{-65536, 65535}; +const invalid: u3 = 8; +``` + +#### Packed Structs + +Packed structs have guaranteed in-memory layout: + +For example, for this packed struct it is guaranteed that 'a' comes before 'b' in memory, and that the entire struct will take up 8 bits. +```zig +packed struct { + a: u3, + b: i5, +} +``` + +#### Converting Between Different Data Types + +There are several options for type conversions: +- Type Coercion + - Is guaranteed to be safe (read: no detail is lost) + - Integer and Float widening (e.g. `const a: u3 = 7;` \n `const b: i4 = a;`) + - Compile-Time Known Numbers (e.g. `const a: u64 = 255;` \n `const b: u8 = a;`) + - There are more Coercion-Rules for Slices, Arrays, Tuples, Pointers, Optionals, Unions and Enums. +- Explicit Casts + - `@as`, performs an explicit type coercion. E.g. `const a = @as(u8, 255)` + - `@bitCast`, e.g. from `f32` to `u32`: `const a: u32 = @bitCast(@as(f32, 8.5));` + - `@truncate`, e.g. `const a: u4 = @truncate(0b1111_0001)` -> `a = 1` + - And many more. In general: if the conversion is deemed unsafe (lossy), you have to use an explicit cast. +- Peer Type Resolution + - Determines a coercion of multiple source types into one target type. + - For example: `a = @as(u3, 4) + @as(u4, 11)` would result in `a: u4 = 15` + +### Tuples + +Tuples are anonymous structs whose fields are named using numbers starting from 0. +Fields are accessed similar to arrays, using indices. + +```zig +const tuple = .{ + @as(u32, 1234), + @as(f64, 12.34), + true, + "hi", +}; + +try expectEqual(tuple[0], 1234); +try expectEqual(tuple[1], 12.34); +try expectEqual(tuple[2], true); +try expectEqual(tuple[3], "hi"); +``` + +### Error Handling + +The Error Handling in Zig is not necessarily unique, but too in-depth to not be mentioned. +Errors in Zig are eerily similar to Exceptions (not `RuntimeExceptions`) in Java, in that any function that can produce errors must declare them in its function definition. +However less like Java and moreso like C or Rust, errors are just return values of functions. + +As such, you might define a function that returns multiple possible errors or a value like this: +```zig +fn parseInt(str: ?[*]u8) error{NullPointer, NotANumber}!i32 +``` +The `!` operator declares an 'error union type', meaning that either an error or a value must be present, but not both at the same time. + +The fundamental syntax for error handling is: +```zig +if(failingFn()) |result| else |err| +``` +This branches on whether the error union contains a value or an error. +Each branch captures the corresponding value using `|x|`. + +The Keywords `catch`, `try` build syntactic sugar on top of it. +Each pair of these lines behaves the same: +```zig +failingFn() catch "defaultValue" +if(failingFn()) |result| result else |_| "defaultValue" + +try failingFn() +if(failingFn()) |result| result else |err| return err +``` + +### Defer + +You may be familiar with this keyword from 'Go', however unlike in Go, deferred expressions are executed at **scope** exit. + +So this will print `123` in order. +```zig +{ + defer print("2", .{}); + print("1", .{}); +} +print("3", .{}); +``` + +Similarly, `errdefer` expressions are executed at scope exit, but only if the function **returned an error from within the scope-block**. + +```zig +fn notExecuted() !void { + { + errdefer print("not printed", .{}); + } + return error.ErrorOutsideScope; +} + +fn isExecuted() !void { + { + errdefer print("is printed", .{}); + return error.ErrorInsideScope; + } +} +``` + +### Comptime + +#### Compile-time Expressions + +Comptime expressions, as the name implies, are evaluated at compile time. + +```zig +fn fibonacci(index: u8) u8 { + if (index < 2) return index; + return fibonacci(index - 1) + fibonacci(index - 2); +} + +pub fn main() !u8 { + return comptime fibonacci(7); +} +``` + +will compile to +```zig +pub fn main() !u8 { + return 13; +} +``` + +#### Generics Using Compile-time Parameters + +Types can be passed to functions as parameters, provided that they are known at compile-time. + +```zig +fn max(comptime T: type, a: T, b: T) T { + if (T == bool) + return (a or b); + + return if (a > b) a else b; +} + +pub fn main() !void { + print("max(true, false)={}\n", .{max(bool, true, false)}); + print("max(5, 15)={}\n", .{max(u4, 5, 15)}); +} +``` + +Since the `type` parameter (T) must be known at Compile-Time, the Compiler is able to generate these two functions: +```zig +fn max(a: bool, b: bool) bool { + return a or b; +} + +fn max(a: u4, b: u4) u4 { + return if (a > b) a else b; +} +``` + +
+ +## Notable design decisions + +### Integer Overflow + +The "normal" arithmetic operators (`+`, `-`, `*`, `/`) treat overflow as an error. +If overflow is detected at compile-time, compilation will abort. +If overflow is detected at runtime, the thread will panic. + +If overflow is desired, there are 'Wrapping' Operands: `+%`, `-%` and `*%`. + +If overflow is allowed, and you want to detect overflow, there are builtin functions: +`@addWithOverflow`, `@subWithOverflow` and `@mulWithOverflow`. +For example: +```zig +const ov = @addWithOverflow(@as(u3, 7), 5); +print("result: {}, didOverflow: {}\n", .{ov[0], ov[1]}); +``` +will print `result: 4, didOverflow: 1` + +### Allocators + +The most notable difference to C is that there is no default allocator (`malloc`). +When writing functions the difference is subtle: if the function needs to allocate memory, it should accept an Allocator as a parameter. +But when calling functions that allocate memory, Zig forces you to consider where you allocate memory and how it is managed. + +To that end, there are several builtin allocators, such as: +- `FixedBufferAllocator` populates the buffer like a stack. Only the most recently allocated block can be freed at a time. +- `ArenaAllocator` wraps another allocator, but all allocated memory is freed at once. +- `GeneralPurposeAllocator` and allocator that can free in any order. The stated performance goals are low fragmentation and low worst-case latency. +- `CAllocator` the default C allocator, exposed only if you are linking LibC. + +You can also create your own allocator by implementing the Allocator interface: +```zig +pub const VTable = struct { + alloc: *const fn (ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8, + + resize: *const fn (ctx: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool, + + free: *const fn (ctx: *anyopaque, buf: []u8, buf_align: u8, ret_addr: usize) void, +}; +``` + +This seems like a good opportunity to look at a simple example of interfaces and generic types. +Suppose you want to define a generic "Mapper" similar to what you're familiar with from functional programming languages. +```zig +fn Mapper(comptime A: type, comptime B: type) type { + return struct { + map: *const fn (a: A) B, + }; +} +``` +`Mapper` is a generic interface that provides a `map` function. +This is possible since `Mapper` is a comptime executable function that returns a type. +You can then use this type as the return type of another function: +```zig +pub fn DoubleIt(comptime T: type) Mapper(T, T) { + const implementation = struct { + fn map(a: T) T { + return a * 2; + } + }; + + return Mapper(T, T){ .map = implementation.map }; +} +``` +`DoubleIt` returns a Mapper implementation that doubles the numerical value of the input. +The compiler will throw an error if a type `T` is passed in for which the `*` operator is not implemented (anything other than integers and floats). + +Naturally, you can also use the Mapper type as a function parameter: +```zig +fn mappedIntegersFromZero(comptime length: usize, comptime Int: type, mapper: Mapper(Int, Int)) [length]Int { + var result: [length]Int = undefined; + for (0..length) |i| { + result[i] = mapper.map(@intCast(i)); + } + return result; +} +``` +When called with `(4, i16, DoubleIt(i16))`, this will return the array `{0, 2, 4, 6}`. + +### Builtin Test Support + +Unlike with most programming languages, Zig has a builtin testing framework and tests are allowed to be part of the Container (`.zig` file) under test. +This has several advantages: tests are easy to find, and you don't have to mirror your source-code directory structure into a separate 'test' directory. Another direct consequence of this, is that many of the Zig source files contain tests which provide always-valid and always-up-to-date code-as-documentation for how to use them. + +Here are some exemplary tests for Language Aspects covered so far: +```zig +const std = @import("std"); +const expectEqual = std.testing.expectEqual; + +test "u5 overflows to 0" { + const val: u5 = 31; + try expectEqual(0, val +% 1); +} + +test "sizeof packed struct {u3, i5} is 1 byte" { + const TestStruct = packed struct { + a: u3, + b: i5, + }; + try expectEqual(1, @sizeOf(TestStruct)); +} + +test "defer executes at scope exit" { + const Buffer = struct { + var data: [3]u8 = undefined; + var i: usize = 0; + + fn push(value: u8) void { + data[i] = value; + i += 1; + } + }; + + { + defer Buffer.push(2); + Buffer.push(1); + } + Buffer.push(3); + + try expectEqual([_]u8{ 1, 2, 3 }, Buffer.data); +} +``` + +
+ +## Performance at a Glance + +### Multithreading + +Zig provides a Multithreading abstraction. +The actual Threading implementation is platform dependent. +There are dedicated implementations for Windows, Linux and WebAssembly. +For other Operating Systems, Posix is used, if available. + +Let's have a look at how to implement a n-producers-to-m-consumers problem. +I first declare some global constants and a data-type for shared data between all threads: +```zig +const buffer_len = 10; +const num_producers = 5; +const num_producer_items = 5; +const num_consumers = 3; + +const SharedData = struct { + buffer: *[buffer_len]u8, + buffer_end: *usize, + buffer_lock: *Mutex, + empty_buffer_slots: *Semaphore, + filled_buffer_slots: *Semaphore, + producers_left: *usize, +}; +``` + +Then I implement the producer Thread: +It generates a random number to push onto the buffer, waits for a slot to become available, pushes the number and finally notifies the consumers that a slot was filled. +```zig +const ProducerInfo = struct { + data: *const SharedData, + index: usize, +}; + +fn producer(info: *ProducerInfo) void { + var rng = Random.init(@intCast(info.index)); + var data = info.data; + for (0..num_producer_items) |_| { + const item: u8 = @truncate(rng.next()); + + data.empty_buffer_slots.wait(); + data.buffer_lock.lock(); + + print("producer {} stores {} in slot {}\n", .{ info.index, item, data.buffer_end.* }); + data.buffer[data.buffer_end.*] = item; + data.buffer_end.* += 1; + + data.buffer_lock.unlock(); + data.filled_buffer_slots.post(); + } + + data.buffer_lock.lock(); + data.producers_left.* -= 1; + data.buffer_lock.unlock(); +} +``` + +I implemented the consumer Thread in a similar fashion. +One noteworthy thing happened here though: it's surprisingly finicky to stop the consumer threads once the workload is complete. +There does not seem to be a way to force-stop threads. +```zig +const ConsumerInfo = struct { + data: *const SharedData, + index: usize, +}; + +fn consumer(info: *ConsumerInfo) void { + var data = info.data; + while (true) { + while (true) { + data.filled_buffer_slots.timedWait(time.ns_per_ms * 10) catch { + if (data.producers_left.* == 0) { + return; + } + continue; + }; + break; // if semaphore released a permit, break waiting loop + } + + data.buffer_lock.lock(); + + data.buffer_end.* -= 1; + const item = data.buffer[data.buffer_end.*]; + print("consumer {} took {} from slot {}\n", .{ info.index, item, data.buffer_end.* + 1 }); + + data.buffer_lock.unlock(); + data.empty_buffer_slots.post(); + + // slow down the consumer threads so that producers run out of buffer space + time.sleep(time.ns_per_s); + } +} +``` + +Finally, we can spawn multiple Threads and point them at these functions. +Here the initialization is a little clunky because arguments are immutable, so the shared data must be passed as a pointer, which in turn must store their data *somewhere* (on the function stack, in this case). +It also would have been possible to use an allocator here. +```zig +pub fn main() !void { + var buffer: [buffer_len]u8 = undefined; + var buffer_end: usize = 0; + var buffer_lock = Mutex{}; + var empty_buffer_slots = Semaphore{ .permits = buffer_len }; + var filled_buffer_slots = Semaphore{ .permits = 0 }; + var producers_left: usize = num_producers; + + var data = SharedData{ + .buffer = &buffer, + .buffer_end = &buffer_end, + .buffer_lock = &buffer_lock, + .empty_buffer_slots = &empty_buffer_slots, + .filled_buffer_slots = &filled_buffer_slots, + .producers_left = &producers_left, + }; + + var producer_infos: [num_producers]ProducerInfo = undefined; + var producer_handles: [num_producers]Thread = undefined; + for (&producer_infos, 0..) |*info, index| { + info.data = &data; + info.index = index; + producer_handles[index] = try Thread.spawn(.{}, producer, .{info}); + } + + var consumer_infos: [num_consumers]ConsumerInfo = undefined; + var consumer_handles: [num_consumers]Thread = undefined; + for (&consumer_infos, 0..) |*info, index| { + info.data = &data; + info.index = index; + consumer_handles[index] = try Thread.spawn(.{}, consumer, .{info}); + } + + for (&producer_handles) |*handle| { + handle.join(); + } + for (&consumer_handles) |*handle| { + handle.join(); + } + const total_items = num_producers * num_producer_items; + print("completed producing {} items\n", .{total_items}); +} +``` + +### Vectors / SIMD + +SIMD instructions are executed by a single CPU Core, so they offer another degree of parallelism on top of Multithreading. + +Zig supports SIMD operations through Vectors of Integers, Floats, booleans and Pointers, provided that the length of the Vector is known at compile-time. +All operations of the underlying type can be used on a Vector, Zig will generate SIMD instructions where possible, otherwise the operation is performed element-wise. + +Example Code: +```zig +fn vectorXor(len: comptime_int, a: @Vector(len, u4), b: @Vector(len, u4)) @Vector(len, u4) { + return a ^ b; +} + +fn arrayXor(len: comptime_int, a: [len]u4, b: [len]u4) [len]u4 { + var result: [len]u4 = undefined; + for (0..len) |i| { + result[i] = a[i] ^ b[i]; + } + return result; +} + +test "vector xor" { + const a = [_]u4{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + const b = [_]u4{0xf} ** 16; + const vec_result = vectorXor(a.len, a, b); + const arr_result = arrayXor(a.len, a, b); + try expectEqual(@as([16]u4, vec_result), arr_result); +} +``` + +### Benchmarking a Simple Algorithm + +To get a feel for the language, I implemented the MD5 hash algorithm in Zig. +Here are some things I would consider worth mentioning. + +We can use container-level expressions (these implicitly are `comptime`) to compute complex data at compile time: +```zig +const s_table: [64]u5 = init: { + const shift_table = .{ + [_]u5{ 7, 12, 17, 22 }, // + [_]u5{ 5, 9, 14, 20 }, // + [_]u5{ 4, 11, 16, 23 }, // + [_]u5{ 6, 10, 15, 21 }, // + }; + + var result: [64]u5 = undefined; + for (0..4) |table_index| { + for (table_index * 16..(table_index + 1) * 16) |i| { + result[i] = shift_table[table_index][i % 4]; + } + } + break :init result; +}; + +const k_table = init: { + var result: [64]u32 = undefined; + for (&result, 0..) |*p, i| { + p.* = @as(u32, @floor((1 << 32) * @abs(@sin(@as(f64, i) + 1)))); + } + break :init result; +}; +``` + +We can also use `inline` and `comptime` to implement this part of the algorithm in a 'DRY', branchless fashion.\ +
+ +
source: https://en.wikipedia.org/wiki/MD5
+
+ +```zig +inline for (0..4) |round| { + inline for (round * 16..(round + 1) * 16) |i| { + const g = comptime switch (round) { + 0 => undefined, + 1 => ((5 * i) + 1) & 0x0f, + 2 => ((3 * i) + 5) & 0x0f, + 3 => (7 * i) & 0x0f, + else => unreachable, + }; + const f = switch (round) { + inline 0 => ((b & c) | (~b & d)) +% buffer[i], + inline 1 => ((d & b) | (~d & c)) +% buffer[g], + inline 2 => (b ^ c ^ d) +% buffer[g], + inline 3 => (c ^ (b | ~d)) +% buffer[g], + else => unreachable, + } +% a +% k_table[i]; + + a = d; + d = c; + c = b; + const shift: u5 = s_table[i]; + b = b +% ((f << shift) | (f >> (~shift +% 1))); + } +} +``` + +Checking with Ghidra, it is clear that the k-table and switch-expressions were evaluated at compile time: +![](md5_ghidra.png) + +I also implemented the algorithm in Rust and Java to get a comparison. +(Note, I'm obviously not proficient in Zig or Rust, so consider this the benchmark-expectation for laymen) + +For the C benchmark I used [this implementation](https://gist.github.com/creationix/4710780) with `gcc -Wall -Wextra -O3 -lm -o md5 md5.c`. +I also tried [this implementation](https://github.com/Zunawe/md5-c), but it was slower. + +For the C++ benchmark I used [this implementation](https://github.com/animetosho/md5-optimisation) which makes use of hand-optimized X86 Assembly. + +Each benchmark computed `2^21` (~2 Million) hashes.\ +I did not bother with implementing multi-threaded benchmarks for C, C++ and Rust. + +
+
+

1 Thread, 512 Input Bytes

+ +
+
+

1 Thread, 16384 Input Bytes

+ +
+
+

16 Threads, 512 Input Bytes

+ +
+
+

16 Threads, 16384 Input Bytes

+ +
+
+ +Unsurprisingly, the implementation using optimized Assembly leads the pack.\ +Slightly more surprising was the performance of the native Java implementation. I was unable to replicate its performance by copying the corresponding source code into my own project, so I have to assume the JVMs C2 compiler applies additional optimizations specific to the native Md5 implementation. + +Lastly, judging by the differences in performance between 512 and 16,384 Input Bytes for my Zig implementation, I expect my memory management to be the primary bottleneck.\ +That said, it seems I shouldn't be too hard on myself, as the builtin Md5 algorithm in Zig is actually ~2% slower for 16384 Byte Messages and 15% (!) slower for 512 Byte Messages.\ +(On my specific hardware, yada, yada...) + +An interesting observation:\ +Changing this +```zig +var buffer = [_]u32{0} ** 16; +inline for (0..16) |dword_i| { + const message_index = (block_index * 64) + (dword_i * 4); + buffer[dword_i] = @as(u32, message[message_index]) + | (@as(u32, message[message_index + 1]) << 8) + | (@as(u32, message[message_index + 2]) << 16) + | (@as(u32, message[message_index + 3]) << 24); +} +``` +to +```zig +const buffer: [*]const u32 = @ptrCast(@alignCast(message[(block_index * 64)..((block_index + 1) * 64)])); +``` +when built with 'Debug' improves the runtime by 32% from 7.8s to 5.9s for 512 Bytes.\ +However using 'ReleaseFast' or even 'ReleaseSmall', the performance difference reduces to <50ms (3% speedup).\ +Guessing blindly, I suspect the loop is not inlined for Debug builds so that the line numbers match up. + +
+ +## The Build System and Dependency Management + +In this section I will guide you through the process of building a fully fledged project.\ +Starting from a basic zig library and making it accessible via CLI, GUI, USB and the Web-Browser. + +For my own convenience, let's say we wanted to publicise an Md5 Library.\ +We'll publish two functions: +```zig +/// computes the Md5 hash of the message and stores it in the hash. +pub fn computeMd5(message: []const u8, hash: *[4]u32) void; + +/// converts the Md5 hash in 'hash' to a String and stores it in 'result'. +pub fn toHexString(hash: *[4]u32, result: *[32]u8) !void; +``` + +If we wanted to build a C library, we could also export this function. +```zig +export fn c_computeMd5(message: [*c]const u8, len: usize, hash: *[4]u32) void; +``` + +For this project, the command to build a library is straightforward:\ +`zig build-lib src/Md5.zig` + +However, there are over 200 arguments that can be passed to the `build-lib` command.\ +Also, your project might have more than one file.\ +So, instead of typing that out every time, or writing a bash script (which Windows users will love), you should probably use: + +### Build Files + +The core idea of Build Files is to store all the arguments you pass to `build-lib` or `build-exe` in a single file: `build.zig`.\ +The Build File is typically placed in the root directory of the project.\ +Since build files are just Zig code, we can also add conditional build artifacts. + +Let's make building the library an optional step of the build process. +```zig {linenos=true} +const std = @import("std"); +const Build = std.Build; +const ResolvedTarget = std.Build.ResolvedTarget; +const OptimizeMode = std.builtin.OptimizeMode; + +const BuildConfig = struct { + target: ResolvedTarget, + optimize: OptimizeMode, +}; +var buildConfig: BuildConfig = undefined; + +fn add_build_lib(b: *Build) !void { + const make_lib = b.option(bool, "make-lib", "Compile a static library that computes md5 hashes.") orelse false; + if (!make_lib) { + return; + } + + const lib = b.addStaticLibrary(.{ + .name = "Md5", + .root_source_file = b.path("src/Md5.zig"), + .target = buildConfig.target, + .optimize = buildConfig.optimize, + }); + const install_step = b.addInstallArtifact(lib, .{}); + b.getInstallStep().dependOn(&install_step.step); +} + +pub fn build(b: *std.Build) !void { + buildConfig = .{ + .target = b.standardTargetOptions(.{}), + .optimize = b.standardOptimizeOption(.{ + .preferred_optimize_mode = OptimizeMode.ReleaseSmall, + }), + }; + + try add_build_lib(b); +} +``` + +The function `add_build_lib` adds a step (building the static library) to the build, only if the option `-Dmake-lib` is provided.\ +Additionally, you can pass the target architecture/os with `-Dtarget=_`, and use `-Drelease` to build with the projects preferred Build Mode. + +Great, we have added 3 additional arguments to a list of 200 others...\ +Is what I would say if Zig wasn't a little more clever than that.\ +If you run `zig build --help` you will notice that Zig now only offers ~70 arguments.\ +But most importantly, the options that we defined are listed in their own section: +``` +Project-Specific Options: + -Dtarget=[string] The CPU architecture, OS, and ABI to build for + -Dcpu=[string] Target CPU features to add or subtract + -Ddynamic-linker=[string] Path to interpreter on the target system + -Drelease=[bool] optimize for end users + -Dmake-lib=[bool] Compile a static library that computes md5 hashes. +``` + +This is the intended way to discover configuration options when building Zig projects from source. + +Now, let's also add the option to build a command-line application.\ +I'll create a Zig file for that `/src/app/cli/Main.zig`: +```zig {linenos=true} +const std = @import("std"); +const md5 = @import("../../Md5.zig"); + +pub fn main() !void { + const stdOut = std.io.getStdOut().writer(); + + var arg_iterator = std.process.args(); + var hash: [4]u32 = undefined; + _ = arg_iterator.next(); // skip program name + while (arg_iterator.next()) |arg| { + md5.computeMd5(arg, &hash); + var hash_str: [32]u8 = undefined; + try md5.toHexString(&hash, &hash_str); + try stdOut.print("'{s}' => {s}\n", .{ arg, hash_str }); + } +} +``` +All it does is iterate over the arguments passed in to the program and output their hash.\ +However, while trying to add a build step for the CLI app, something interesting happens: +``` +src/app/cli/Main.zig:2:21: error: import of file outside module path: '../../Md5.zig' +const md5 = @import("../../Md5.zig"); +``` +Modules are only allowed to look for .zig files on the same level or below. That's pretty reasonable.\ +However, Md5.zig is supposed to be shared across multiple Frontends. To do that, you can import the file into the executable: +```zig {linenos=true} +// extend the BuildConfig struct: +// md5_module: *std.Build.Module, + +pub fn build(b: *std.Build) !void { + // ... + + buildConfig.md5_module = b.addModule("Md5", .{ + .root_source_file = b.path("src/Md5.zig"), + .target = buildConfig.target, + .optimize = buildConfig.optimize, + }); + try add_build_cli(b); +} + +fn add_build_cli(b: *Build) !void { + const make_cli = b.option(bool, "make-cli", "Compile a Command-Line Application that prints the md5 hashes of all inputs to the console.") orelse false; + if (!make_cli) { + return; + } + + const exe = b.addExecutable(.{ + .name = "Md5-Cli", + .root_source_file = b.path("src/app/cli/Main.zig"), + .target = buildConfig.target, + .optimize = buildConfig.optimize, + }); + exe.root_module.addImport("../../Md5.zig", buildConfig.md5_module); + + const install_step = b.addInstallArtifact(exe, .{}); + b.getInstallStep().dependOn(&install_step.step); +} +``` + +Note that on line 27 I am naming the import `../../Md5.zig` so that ZLS knows where to look for the Source file and provides better autocompletion.\ +Other than that, any name would work here. + +Now we can build and run the CLI application: +``` +> zig build -Dmake-cli -Drelease +> ./zig-out/bin/Md5-Cli "Hello, World!" +'Hello, World!' => 65a8e27d8879283831b664bd8b7f0ad4 +``` + +### Depending on Zig Libraries + +Next, we want to create a GUI application.\ +We'll use [Capy](https://github.com/capy-ui/capy) for that, which means we'll have to learn how to use other Zig libraries in our own project.\ +The `build.zig.zon` file is used to declare dependencies (`zon` stands for 'Zig Object Notation') +```zig {linenos=true} +.{ + .name = "Md5", + .version = "1.0.0", + .paths = .{""}, + .dependencies = .{ + .capy = .{ + .url = "https://github.com/capy-ui/capy/archive/d55a9ddf39ebaeed7d25279c08922bb8cf4613db.tar.gz", + .hash = "12202d8a1a46e0e78dc366ba3cb9843821b65ed3d0e110cf705bdeaba9eb8f75ac75", + .lazy = true, + }, + }, +} +``` +Note that `.lazy = true` (lazy dependencies) is a rather new feature.\ +If present, the dependency is only downloaded for the build if the build requests the dependency via `b.lazyDependency("capy", ...)`.\ +This makes sense. No need to download Capy if the user wants to build the CLI app. + +Here's how to add a dependency to an executable: +```zig {linenos=true} + if (b.lazyDependency("capy", .{ + .target = buildConfig.target, + .optimize = buildConfig.optimize, + .app_name = @as([]const u8, "Md5-Gui-Capy"), + })) |capy| { + exe.root_module.addImport("capy", capy.module("capy")); + } +``` +The second parameter of `lazyDependency` specifies the parameters passed to Capy's Build File: `-Dtarget=... -Doptimize=... -Dapp_name=...`.\ +As the name `addImport` suggests, Capy can then be imported with `const capy = @import("capy");` + +Here is all the code required to register a simple Capy GUI: +```zig {linenos=true} + uiElements = .{ + .textInput = capy.textField(.{ .text = "Hello, World!" }), + .outputLabel = capy.textField(.{ .text = "", .readOnly = true }), + }; + + _ = try uiElements.textInput.text.addChangeListener(.{ .function = &onInputChanged }); + + try window.set(capy.column(.{}, .{ + capy.row(.{}, .{ + capy.label(.{ .text = "Message (In)" }), + capy.expanded(uiElements.textInput), + }), + capy.row(.{}, .{ + capy.label(.{ .text = "Hash (Out)" }), + capy.expanded(uiElements.outputLabel), + }), + })); +``` +which produces this output:\ +![Screenshot of the Capy application](app-capy.png) + +### Building for Embedded Systems + +Now we'll get silly: let's compile our Md5 library as the firmware for a Raspberry Pi Pico, which will then expose the library over Uart.\ +For that, we'll use [Microzig](https://github.com/ZigEmbeddedGroup/microzig).\ +This was somewhat painful for several reasons: +- Microzig's latest release is for Zig 0.12.1, however Capy requires Zig 0.13.0 and the API for writing Build Files changed between these two versions. +- Microzig's API is painful to navigate because ZLS does not (yet) provide any autocompletion for it, because the RP2040 implementation of the HAL is obscured behind several dynamic `@import`s. +- You cannot use Zig's heap allocators, because they are not implemented for the RP2040. + +Also, the only code-example showing how to *receive* data on the microcontroller was removed in commit `3af91ea8f9210cb5459b87ebcb63345991060e5b`.\ +Long story short, my firmware only compiles for commit `063ade2a3cfb5373fcfc6a079ecf0734a45952a7`. + +With the code forked, we can now add the dependencies: +```zig {linenos=true} + .@"microzig/build" = .{ + .path = "../../forks/microzig/build/", + }, + .@"microzig/bsp/raspberrypi/rp2040" = .{ + .path = "../../forks/microzig/bsp/raspberrypi/rp2040/", + }, +``` + +The code for handling receiving and sending data looks like this: +```zig {linenos=true} +fn ep1_out_callback(dc: *usb.DeviceConfiguration, data: []const u8) void { + handle_ep1_out_callback(dc, data) catch |err| { + // send error as result message + const fmt_msg = std.fmt.bufPrint(&message.response, "error: {s}", .{@errorName(err)}) catch unreachable; + usb.Usb.callbacks.usb_start_tx( + dc.endpoints[3], // EP1_IN_CFG, + fmt_msg, + ); + }; +} + +fn handle_ep1_out_callback(dc: *usb.DeviceConfiguration, data: []const u8) !void { + if (message.num_bytes_remaining == 0) { + // receiving new request -> first message format is .{ msg_len: u32, msg_part: [60]u8 } + const msg_len: u32 = @as(*const u32, @ptrCast(@alignCast(data[0..4]))).*; + if (msg_len > message.buffer.len) { + const fmt_msg = try std.fmt.bufPrint(&message.response, "msg too long. got len {d}.", .{msg_len}); + usb.Usb.callbacks.usb_start_tx( + dc.endpoints[3], // EP1_IN_CFG, + fmt_msg, + ); + return; + } + + message.num_bytes_remaining = @max(0, @as(i33, msg_len) - 60); + message.num_bytes_received = @min(60, msg_len); + std.mem.copyForwards(u8, &message.buffer, data[4..(4 + message.num_bytes_received)]); + } else { + // receiving request continuation + const input_len = @min(64, message.num_bytes_remaining); + message.num_bytes_remaining -= input_len; + std.mem.copyForwards(u8, message.buffer[message.num_bytes_received..], data[0..input_len]); + message.num_bytes_received += input_len; + } + + if (message.num_bytes_remaining == 0) { + // request data complete, compute hash and prepare to send + var hash: [4]u32 = message.hash_buffer; + md5.computeMd5(message.buffer[0..message.num_bytes_received], &hash); + const response: *[32]u8 = message.response[0..32]; + + try md5.toHexString(&hash, response); + usb.Usb.callbacks.usb_start_tx( + dc.endpoints[3], // EP1_IN_CFG, + response, + ); + } else { + // switch back to receiving data by sending a 0 byte response. + usb.Usb.callbacks.usb_start_tx( + dc.endpoints[3], // EP1_IN_CFG, + &.{}, + ); + } +} +``` +`ep1_out_callback` is called whenever a message is received over the Uart Endpoint (`ep1` stands for Endpoint 1).\ +The maximum amount of bytes that Microzig supports is 64. So for longer messages I implemented a simple protocol that tracks how many bytes are still to be sent.\ +A particular gotcha is that you **must** provide a response with `usb_start_tx`, otherwise the communication just stalls forever. + +Microzig uses a bespoke build api, but the usage is pretty much identical to normal Zig executables: +```zig + const MicroZig = @import("microzig/build"); + const rp2040 = @import("microzig/bsp/raspberrypi/rp2040"); + + const mz = MicroZig.init(b, .{}); + const firmware = mz.add_firmware(b, .{ + .name = "Md5-Pico", + .root_source_file = b.path("src/app/pico/Main.zig"), + .target = rp2040.boards.raspberrypi.pico, + .optimize = buildConfig.optimize, + }); + firmware.modules.app.addImport("../../Md5.zig", buildConfig.md5_module); + + mz.install_firmware(b, firmware, .{}); +``` + +Finally, we can receive hashes over USB using a python script: +```python +# pip install pyusb + +import usb.core +import usb.util + +# ... obtain usb endpoints 'outep' and 'inep' + +blocks = chopMessage(message) +for i in range(len(blocks)): + block = blocks[i] + outep.write(block) + response = inep.read(64) + if (i + 1) == len(blocks): + response_str = ''.join([chr(x) for x in response]) + print("hash: " + response_str) + else: + if len(response) != 0: + raise Exception("Device reported an error: " + response_str) +``` + +### Building for the Web (WebAssembly) + +Finally, we'll finish with something more reasonable.\ +Let's compile the library into a WebAssembly module, so we can use it from JavaScript in a Browser (or other Javascript Engine with WASM support).\ +I think this is actually more reasonable than trying to compute hashes with raw JavaScript. + +On the Zig side, this is a refreshingly simple thing to implement: +```zig +const std = @import("std"); +const md5 = @import("../../Md5.zig"); + +export fn computeMd5(message: [*]const u8, len: u32, hash_buffer: [*]u8) void { + const msg_slice: []const u8 = message[0..len]; + var hash: [4]u32 = undefined; + md5.computeMd5(msg_slice, &hash); + + const buffer: *[32]u8 = hash_buffer[0..32]; + md5.toHexString(&hash, buffer) catch unreachable; +} +``` +One notable thing is that slices (structs containing a pointer and a length) cannot be passed to exported functions.\ +So instead, a 'many-item pointer' (`[*]T`) and a length is passed separately. + +The Build File has a few gotchas, though: +```zig + const exe = b.addExecutable(.{ + .name = "Md5-Wasm", + .root_source_file = b.path("src/app/wasm/Main.zig"), + .target = target, + .optimize = buildConfig.optimize, + }); + // based on absolutely nothing https://ziggit.dev/t/build-wasm-using-zig-build-system-when-ther-is-no-entry-point-but-function-to-export/4364/2 + exe.rdynamic = true; + exe.entry = .disabled; + exe.import_memory = true; // https://github.com/ziglang/zig/issues/8633 + exe.root_module.addImport("../../Md5.zig", buildConfig.md5_module); +``` +- `exe.rdynamic = true;` is required, otherwise the functions don't seem to be exported +- `exe.entry = .disabled;` is required so that zig does not complain about the `main` function being absent +- `exe.import_memory = true;` indicates that the zig code should be allowed to modify the memory that is allocated in JavaScript. + +In JavaScript this can then be simply used like this: +```js +/** @type {{memory: WebAssembly.Memory, computeMd5: function (message: number, len: Number, hash_str: number)}} */ +const wasmObj = { + memory: undefined, + computeMd5: undefined, +} + +/** @return {Promise} */ +function fetchWasmBuffer() { + return fetch("Md5-Wasm.wasm.gz") + .then(data => data.blob()) + .then(wasmGzipBlob => { + let wasmStream = wasmGzipBlob.stream().pipeThrough(new DecompressionStream("gzip")); + return new Response(wasmStream, { headers: {"Content-Type": "application/wasm"} }).arrayBuffer(); + }); +} + +wasmObj.memory = new WebAssembly.Memory({ + initial: 100, maximum: 100, shared: false +}); +fetchWasmBuffer() + .then(wasmBuffer => WebAssembly.instantiate(wasmBuffer, {env: {memory: wasmObj.memory}})) + .then(wasmInstance => { + wasmObj.computeMd5 = wasmInstance.instance.exports.computeMd5; + }); + +function executeMd5() { + if(wasmObj.computeMd5 === undefined) { + console.error("wasmObj was not ready!") + return; + } + + /** @type {String} */ + let message = document.getElementById("message").value; + let encodeResult = new TextEncoder().encodeInto(message, new Uint8Array(wasmObj.memory.buffer, 32)); + wasmObj.computeMd5(32, encodeResult.written, 0); + + let hashView = new DataView(wasmObj.memory.buffer, 0, 32); + document.getElementById("hash").innerText = new TextDecoder().decode(hashView); +} +``` +- `fetchWasmBuffer` retrieves the WebAssembly binary as an ArrayBuffer (this can then be instantiated as a Wasm-Worker) +- Wasm-Workers are single-threaded. Multi-threading is generally done by instantiating multiple workers. +- Please do not ask me why it is possible to hash messages longer than 68 bytes despite the memory being limited to 100 bytes.\ +Surely that should trigger *some* fault protection, but it does not. + +![A Screenshot showing the web interface](app-wasm.png) + +### Build Modes + +Now that we have a bunch of applications to compare, let's talk about build modes. +Zig has four of them: +- Debug : no Optimizations and all safeties. +- ReleaseSafe : Optimizations and safeties. +- ReleaseFast : Optimizations without safeties. +- ReleaseSmall : Size optimizations without safeties. + +We have already seen the subtle performance difference between ReleaseFast and ReleaseSmall in the [benchmarking section](#benchmarking-a-simple-algorithm) + +|App|Debug|ReleaseSafe|ReleaseFast|ReleaseSmall| +|---|---|---|---|---| +|Md5-Lib|2000 KiB|290 KiB|39 KiB|5.8 KiB| +|Md5-Cli|2300 KiB|1900 KiB|1900 KiB|17 KiB| +|Md5-Capy|3100 KiB|2600 KiB|2400 KiB|109 KiB| +|Md5-Pico|459 KiB|37 KiB|34 KiB|25 KiB| +|Md5-Wasm|465 KiB|375 KiB|523 KiB|6.9 KiB| + +I included the binary sizes on a logarithmic scale (left) because the linear scale is nearly indecipherable (right). +
+
+ +
+
+ +
+
+ +If you want to replicate any of this, you can find the source files here (zig 0.13.0 required): +- [build.zig](code/build.zig) +- [build.zig.zon](code/build.zig.zon) +- [/src/Md5.zig](code/Md5.zig) +- [/src/app/cli/Main.zig](code/cli-Main.zig) +- [/src/app/capy/Main.zig](code/capy-Main.zig) +- [/src/app/pico/Main.zig](code/pico-Main.zig) and [sendData.py](code/sendData.py) +- [/src/app/wasm/Main.zig](code/wasm-Main.zig) and [wasm-main.html](code/wasm-Main.html.txt) + +### Interoperability with C + +To finish off the article, let's talk about C.\ +Specifically, what to do if you already have a lot of C/C++ code and want to transition to Zig. + +Zig offers interoperability with C through the C ABI.\ +In theory, all you have to do is extend you Build File to link LibC and include your C files.\ +I have been able to use [my Java 6 interpreter](https://github.com/BlazingTwist/Java_6_Interpreter_in_C) with Zig like this: +```zig + const exeC = b.addExecutable(.{ + .name = "CInterop", + .root_source_file = b.path("src/CInterop.zig"), + .optimize = optimize, + .target = target, + }); + + exeC.addCSourceFiles(.{ + .root = b.path("src/lib/jvm/src/"), + .files = &.{ "interpretedjvm.c", "util/allocator.c", "util/bytereader.c", "util/hex_utils.c", "util/sourcefiledecoder.c", "util/utf8_utils.c", "interpreter/classloader.c", "interpreter/executioncontext.c", "interpreter/executionframe.c", "interpreter/instructionhelper.c", "interpreter/objectinstance.c", "interpreter/opcodes.c", "interpreter/sourcefileinfo.c", "definition/constantpool.c" }, + }); + exeC.addIncludePath(b.path("src/lib/jvm/include/")); + exeC.linkLibC(); +``` +Where the `addIncludePath` points at the directory containing all the header files.\ +And `addCSourceFiles` receives the actual `.c` files. + +Then you can use it pretty much like normal C code: +```zig +const jvm = @cImport({ + @cInclude("interpretedjvm.h"); +}); + +fn printTree(height: c_ulong, symbol: c_ulong) void { + jvm.push_U4(&jvm_instance, height); + jvm.push_U4(&jvm_instance, '*'); + jvm.push_U4(&jvm_instance, symbol); + jvm.push_Method(&jvm_instance, "pgp/ex1/task4/Tree", "printTree", "(ICC)V"); + jvm.executeAllMethods_NoReturn(&jvm_instance); +} +``` +You even get code completion for this!\ +After executing `zig build` the cache contains generated zig files such as: +```zig +pub extern fn push_U4(jvm: [*c]InterpretedJVM, value: c_ulong) void; +``` + +
+ +## Conclusion + +Zig is an impressively well-built language that has been very fun to explore.\ +My only criticism of Zig itself is that the standard library is not always well documented - but it never felt out of reach to just figure it out from the code alone.\ +Other than that I'm left simply gushing over the language. Not what I expected as I'm usually more comfortable in high level languages. + +Currently Zig offers a fantastic opportunity to contribute to Open Source projects. As the language is still in pre-release state, breaking changes are generally accepted if the change is for the better.\ +With 2900 open issues there are plenty of opportunities to contribute. + +
+ +## Further Reading + +- 2022-12-07 https://ziglang.org/news/goodbye-cpp/ - This article touches on how the zig compiler (which is written in zig) can be built from source.\ +Spoiler: It involves multiple compilation phases, custom WASI implementations, and a C Compiler. +- 2024.07.01 https://kristoff.it/blog/improving-your-zls-experience/ - A brief article about the state of convenience tooling (such as code completions and inspections) + +
+ +## Sources + +- The section on the language aspects and syntax is based on the [zig documentation](https://ziglang.org/documentation/0.13.0/) +- all diagrams were generated using [JFreeChart](https://www.jfree.org/jfreechart/) +- all other sources are referenced immediately next to the relevant section (primarily 3rd party source code) \ No newline at end of file diff --git a/content/posts/2024/08/zig/md5_ghidra.png b/content/posts/2024/08/zig/md5_ghidra.png new file mode 100644 index 0000000..ab1b915 Binary files /dev/null and b/content/posts/2024/08/zig/md5_ghidra.png differ diff --git a/content/posts/2024/08/zig/md5_pseudo.png b/content/posts/2024/08/zig/md5_pseudo.png new file mode 100644 index 0000000..b9bd921 Binary files /dev/null and b/content/posts/2024/08/zig/md5_pseudo.png differ diff --git a/data/authors/eric.jarosch.yml b/data/authors/eric.jarosch.yml new file mode 100644 index 0000000..c2db192 --- /dev/null +++ b/data/authors/eric.jarosch.yml @@ -0,0 +1 @@ +name: Eric Jarosch