Changes
6 changed files (+263/-12)
-
-
@@ -8,10 +8,70 @@ // 2.0 at <https://www.apache.org/licenses/LICENSE-2.0>// // SPDX-License-Identifier: 0BSD OR Apache-2.0 //! This example searches Roon Core on computer's subnet then print found one's information on //! stderr. //! //! Since there is no simple cross-platform way to enumerate network interface in Zig, broadcasting //! is omitted. If you do the same, test by yourself whether multicast is sufficient. //! //! In real world application, you should use non-blocking receive and/or configure timeout using //! actual measurements and usecases. const std = @import("std"); const sood = @import("sood"); pub fn main() void { std.debug.print("{d}\n", .{sood.add(1, 2)}); pub fn main() !void { // Prepare multicast address and SOOD port to pass them to system. const send_addr = std.net.Address.initIp4(sood.discovery.multicast_ipv4_address, sood.discovery.udp_port); // Creates a UDP socket (INET = IPv4, DGRAM = Datagrams) const sockfd = try std.posix.socket(std.posix.AF.INET, std.posix.SOCK.DGRAM, 0); defer std.posix.close(sockfd); // Allow reuse of socket address. Doing the same as what Node.js API does. try std.posix.setsockopt(sockfd, std.posix.SOL.SOCKET, std.posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1))); // Cancel receive call at 5 seconds. Repeat send->recv if timeout happened. const timeout = std.posix.timeval{ .sec = 5, .usec = 0 }; try std.posix.setsockopt(sockfd, std.posix.SOL.SOCKET, std.posix.SO.RCVTIMEO, &std.mem.toBytes(timeout)); // We're not setting these socket options here: // * this example only sends multicast, so `SOL_SOCKET, SO_BROADCAST, 1` is not required. // * Zig seems not to have `IP_MULTICAST_TTL`, so `IPPROTO_IP, IP_MULTICAST_TTL, 1` is not here. while (true) { // Sends a prebuilt query bytes to the mutlicast address. _ = try std.posix.sendto( sockfd, sood.discovery.Query.prebuilt, 0, &send_addr.any, send_addr.getOsSockLen(), ); // 512 bytes ... reasonable limit. In my testing, message size was less than 300 bytes. var received: [512]u8 = undefined; if (std.posix.recv(sockfd, &received, 0)) |received_size| { const received_msg = try sood.Message.parse(received[0..received_size]); // `discovery.Response` struct handles known key retrieval, necessary type casting and // error-handlings. For known fields, use of this struct is recommended. const response = try sood.discovery.Response.init(received_msg); std.debug.print("Name: \t\t{s}\nVersion: \t{s}\nUnique ID: \t{s}\n", .{ try response.getName() orelse "<empty>", try response.getDisplayVersion() orelse "<empty>", try response.getUniqueId(), }); std.debug.print("HTTP port: \t{d}\n", .{try response.getHttpPort()}); return; } else |err| switch (err) { std.posix.RecvFromError.WouldBlock => { std.debug.print("Retrying...\n", .{}); continue; }, else => return err, } } }
-
-
-
@@ -12,8 +12,6 @@ // ===// // C API. const sood = @import("lib.zig"); export fn sood_add(a: i32, b: i32) i32 { return sood.add(a, b); return a + b; }
-
-
src/discovery.zig (new)
-
@@ -0,0 +1,140 @@// Copyright 2025 Shota FUJI // // Licensed under the Zero-Clause BSD License or the Apache License, Version 2.0, at your option. // You may not use, copy, modify, or distribute this file except according to those terms. You can // find a copy of the Zero-Clause BSD License at LICENSES/0BSD.txt, and a copy of the Apache License, // Version 2.0 at LICENSES/Apache-2.0.txt. You may also obtain a copy of the Apache License, Version // 2.0 at <https://www.apache.org/licenses/LICENSE-2.0> // // SPDX-License-Identifier: 0BSD OR Apache-2.0 //! This module contains constants and concret type on top of Message struct. const std = @import("std"); const Message = @import("Message.zig"); /// Target UDP port to use for sending IP multicast and broadcast. /// Though you can also listen for this UDP port to receive broadcast from another client, /// this library does not provide API for handling that case. pub const udp_port = 9003; /// IPv4 address for IP multicast. Send SOOD message to this IP address on `udp_port`. pub const multicast_ipv4_address = [4]u8{ 239, 255, 90, 90 }; pub const Query = struct { pub const header = Message.Header{ .kind = .query }; /// A required property for a discovery query. Node.js API uses hard-coded ID /// thus this library uses the same one. pub const query_service_id = Message.Property{ .key = "query_service_id", .value = "00720724-5143-4a9b-abac-0e50cba674bb", }; pub const properties = [_]Message.Property{ query_service_id, }; /// Premade bytes for minimum discovery query message. pub const prebuilt = "SOOD\x02Q\x10query_service_id\x00\x2400720724-5143-4a9b-abac-0e50cba674bb"; }; test "Query.prebuilt should equal to one built by `write`" { const built = try Message.write( std.testing.allocator, Query.header, &Query.properties, ); defer std.testing.allocator.free(built); try std.testing.expectEqualSlices(u8, built, Query.prebuilt); } pub const Response = struct { message: Message, pub const InitError = error{ /// Message's type (kind) is not "response" (`R`) NonResponseKindMessage, }; pub fn init(message: Message) InitError!@This() { if (message.header.kind != .response) { return InitError.NonResponseKindMessage; } return .{ .message = message }; } pub const GetPropertyError = error{ /// Response message does not contain required property. MissingRequiredProperty, /// Cannot convert property value to desired type. UnexpectedPropertyValue, } || Message.Properties.ParseError; /// Iterates over properties and returns a property of the same `key`. /// Returns `null` if no properties matched to the `key` . /// /// This function converts the found value into `T`. It currently supports: /// * `[]const u8` ... returns as-is. /// * Integers (e.g. `u16`) ... returns the result of `std.fmt.parseInt`. /// Returns an error if that conversion failed. inline fn getProperty(self: @This(), comptime T: type, key: []const u8) GetPropertyError!?T { var iter = self.message.body.iterator(); while (try iter.next()) |prop| { if (std.mem.eql(u8, key, prop.key)) { switch (T) { []const u8 => return prop.value, else => return std.fmt.parseInt(T, prop.value, 10) catch GetPropertyError.UnexpectedPropertyValue, } } } return null; } /// Same as `getProperty`, except returns an error in place of `null`. inline fn getRequiredProperty(self: @This(), comptime T: type, key: []const u8) GetPropertyError!T { return try self.getProperty(T, key) orelse GetPropertyError.MissingRequiredProperty; } /// Returns "Roon Server name" (`name` property). /// This string is what user see in Settings/General page and can be changed at Settings/Setup /// page. pub fn getName(self: @This()) GetPropertyError!?[]const u8 { return self.getProperty([]const u8, "name"); } /// Returns Roon server version strings (`display_version` property). /// As the name suggests, this value is for display purpose. Format is not defined and there is /// no stability guarantee, therefore parsing of this value is not recommended. pub fn getDisplayVersion(self: @This()) GetPropertyError!?[]const u8 { return self.getProperty([]const u8, "display_version"); } /// Returns the Roon server's unique ID string. pub fn getUniqueId(self: @This()) GetPropertyError![]const u8 { return self.getRequiredProperty([]const u8, "unique_id"); } pub fn getServiceId(self: @This()) GetPropertyError!?[]const u8 { return self.getProperty([]const u8, "service_id"); } pub fn getTcpPort(self: @This()) GetPropertyError!?u16 { return self.getProperty(u16, "tcp_port"); } /// Returns TCP port number for WebSocket communication. pub fn getHttpPort(self: @This()) GetPropertyError!u16 { return self.getRequiredProperty(u16, "http_port"); } pub fn getHttpsPort(self: @This()) GetPropertyError!?u16 { return self.getProperty(u16, "https_port"); } };
-
-
src/ipv4_subnet.zig (new)
-
@@ -0,0 +1,55 @@// Copyright 2025 Shota FUJI // // Licensed under the Zero-Clause BSD License or the Apache License, Version 2.0, at your option. // You may not use, copy, modify, or distribute this file except according to those terms. You can // find a copy of the Zero-Clause BSD License at LICENSES/0BSD.txt, and a copy of the Apache License, // Version 2.0 at LICENSES/Apache-2.0.txt. You may also obtain a copy of the Apache License, Version // 2.0 at <https://www.apache.org/licenses/LICENSE-2.0> // // SPDX-License-Identifier: 0BSD OR Apache-2.0 //! This module contains cross-platform, pure Zig functions for computing a broadcast address in //! IPv4 subnet. const std = @import("std"); /// Helper function to get a broadcast address for a subnet. /// If your platform have this functionality, use that instead. pub inline fn getBroadcastAddressU32(ipv4_addr: u32, netmask: u32) u32 { return ipv4_addr | (~netmask); } test getBroadcastAddressU32 { // Addresses are borrowed from here: <https://en.wikipedia.org/wiki/Broadcast_address> try std.testing.expectEqual( 0xac1fffff, getBroadcastAddressU32(0xac100000, 0xfff00000), ); } /// Helper function to get a broadcast address for a subnet. /// This variant operates on an array. /// If your platform have this functionality, use that instead. pub inline fn getBroadcastAddress(ipv4_addr: *const [4]u8, netmask: *const [4]u8) [4]u8 { var result: [4]u8 = undefined; std.mem.writeInt(u32, &result, getBroadcastAddressU32( std.mem.readInt(u32, ipv4_addr, .big), std.mem.readInt(u32, netmask, .big), ), .big); return result; } test getBroadcastAddress { try std.testing.expectEqualDeep( &[4]u8{ 192, 168, 1, 255 }, &getBroadcastAddress(&.{ 192, 168, 1, 1 }, &.{ 255, 255, 255, 0 }), ); // Addresses are borrowed from here: <https://en.wikipedia.org/wiki/Broadcast_address> try std.testing.expectEqualDeep( &[4]u8{ 172, 31, 255, 255 }, &getBroadcastAddress(&.{ 172, 16, 0, 0 }, &.{ 255, 240, 0, 0 }), ); }
-
-
-
@@ -13,11 +13,11 @@ //// Zig API. pub const Message = @import("Message.zig"); pub fn add(a: i32, b: i32) i32 { return a + b; } pub const discovery = @import("discovery.zig"); pub const ipv4_subnet = @import("ipv4_subnet.zig"); test { _ = @import("Message.zig"); _ = @import("discovery.zig"); _ = @import("ipv4_subnet.zig"); }
-
-
-
@@ -12,8 +12,6 @@ // ===// // WebAssembly API. const sood = @import("lib.zig"); export fn sood_add(a: i32, b: i32) i32 { return sood.add(a, b); return a + b; }
-