Changes
8 changed files (+591/-10)
-
-
@@ -59,7 +59,11 @@ .optimize = optimize,}), }); lib.root_module.addImport("sood", sood); lib.linkLibC(); lib.installHeader(b.path("src/lib.h"), "plac_core.h"); lib.installHeader(b.path("src/lib.vapi"), "plac_core.vapi"); b.installArtifact(lib); }
-
-
-
@@ -25,14 +25,66 @@#ifndef PLAC_CORE_H #define PLAC_CORE_H #ifdef __cplusplus extern "C" { #endif #include <sys/socket.h> /* Server */ typedef struct { struct sockaddr sockaddr; socklen_t const addrlen; char const * const id; unsigned int const id_len; char const * const name; unsigned int const name_len; const char * const version; unsigned int const version_len; } plac_server; plac_server *plac_server_dupe(plac_server*); void plac_server_free(plac_server*); /* ScanOptions */ typedef struct { unsigned int count; unsigned int receive_window_ms; } plac_server_scan_options; int adder(int a, int b); void plac_server_scan_options_init(plac_server_scan_options*); #ifdef __cplusplus } #endif /* ScanResultCode */ typedef enum { PLAC_SCAN_OK = 0, PLAC_SCAN_UNKNOWN_ERROR = 1, PLAC_SCAN_OUT_OF_MEMORY = 2, PLAC_SCAN_SOCKET_SETUP_ERROR = 3, PLAC_SCAN_UDP_SEND_ERROR = 4, PLAC_SCAN_UDP_RECV_ERROR = 5, PLAC_SCAN_NULL_POINTER_ARGS = 6, PLAC_SCAN_SOCKET_PERMISSION_ERROR = 7, PLAC_SCAN_TOO_MANY_SOCKET_ERROR = 8, PLAC_SCAN_INVALID_RECEIVE_WINDOW = 9, PLAC_SCAN_NETWORK_UNAVAILABLE = 10, } plac_scan_result_code; /* ScanResult */ typedef struct { plac_scan_result_code const code; void * const __servers; unsigned int const __i; } plac_scan_result; plac_server *plac_scan_result_next(plac_scan_result*); void plac_scan_result_reset(plac_scan_result*); void plac_scan_result_free(plac_scan_result*); /* ServerScanner */ typedef struct { const int *__sockfd; } plac_server_scanner; plac_server_scanner *plac_server_scanner_make(); void plac_server_scanner_free(plac_server_scanner*); plac_scan_result *plac_server_scanner_scan(plac_server_scanner*, plac_server_scan_options*); #endif
-
-
core/src/lib.vapi (new)
-
@@ -0,0 +1,82 @@/* * Copyright 2025 Shota FUJI * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 */ [CCode (cheader_filename = "plac_core.h")] namespace Plac { [CCode (cname = "plac_server", free_function = "plac_server_free")] [Compact] public class Server { public Posix.SockAddr sockaddr; public Posix.socklen_t addrlen; public string id; [CCode (cname = "name")] public string name; public string version; [CCode (cname = "plac_server_dupe")] public Server dup (); } [CCode (cname = "plac_server_scan_options", destroy_function = "", has_type_id = false)] public struct ScanOptions { uint count; uint receive_window_ms; [CCode (cname = "plac_server_scan_options_init")] public ScanOptions (); } [CCode (cname = "plac_scan_result_code", cprefix = "PLAC_SCAN_", has_type_id = false)] public enum ScanResultCode { OK, UNKNOWN_ERROR, OUT_OF_MEMORY, SOCKET_SETUP_ERROR, UDP_SEND_ERROR, UDP_RECV_ERROR, NULL_POINTER_ARGS, SOCKET_PERMISSION_ERROR, TOO_MANY_SOCKET_ERROR, INVALID_RECEIVE_WINDOW, NETWORK_UNAVAILABLE, } [CCode (cname = "plac_scan_result", free_function = "plac_scan_result_free")] [Compact] public class ScanResult { public ScanResultCode code; [CCode (cname = "plac_scan_result_next")] public Server? next (); [CCode (cname = "plac_scan_result_reset")] public void reset (); } [CCode (cname = "plac_server_scanner", free_function = "plac_server_scanner_free")] [Compact] public class ServerScanner { [CCode (cname = "plac_server_scanner_make")] public ServerScanner (); [CCode (cname = "plac_server_scanner_scan")] public ScanResult? scan (ScanOptions opts); } }
-
-
-
@@ -16,7 +16,386 @@ // SPDX-License-Identifier: Apache-2.0//! This is an entrypoint of the static library. // TODO: Remove after implementing actual exports. export fn adder(a: c_int, b: c_int) c_int { return a + b; const std = @import("std"); const sood = @import("sood"); const core = @import("./mod.zig"); const allocator = std.heap.c_allocator; comptime { @export(&Server.dupe, .{ .name = "plac_server_dupe" }); @export(&Server.free, .{ .name = "plac_server_free" }); } pub const Server = extern struct { /// Network address of the server. sockaddr: std.posix.sockaddr, addrlen: std.posix.socklen_t, /// Unique ID of the server. It won't change even on server reboot. id: [*:0]const u8, id_len: usize, /// Display name of the server. Users who can configure the server can change the name. name: [*:0]const u8, name_len: usize, /// Free-format version string. version: [*:0]const u8, version_len: usize, /// Returns `null` on OOM. pub fn dupe(ptr: ?*const Server) callconv(.C) ?*Server { if (ptr) |server| { var new = allocator.create(Server) catch return null; new.id = allocator.dupeZ(u8, server.id[0..server.id_len]) catch return null; new.id_len = server.id_len; new.name = allocator.dupeZ(u8, server.name[0..server.name_len]) catch return null; new.name_len = server.name_len; new.version = allocator.dupeZ(u8, server.version[0..server.version_len]) catch return null; new.version_len = server.version_len; new.sockaddr = server.sockaddr; new.addrlen = server.addrlen; return new; } else { return null; } } pub fn free(ptr: ?*Server) callconv(.C) void { if (ptr) |server| { allocator.free(server.id[0..server.id_len]); allocator.free(server.name[0..server.name_len]); allocator.free(server.version[0..server.version_len]); allocator.destroy(server); } } pub fn jsonStringify(self: *const Server, jws: anytype) !void { try jws.beginObject(); try jws.objectField("unique_id"); try jws.write(self.id[0..self.id_len]); try jws.objectField("name"); try jws.write(self.name[0..self.name_len]); try jws.objectField("version"); try jws.write(self.version[0..self.version_len]); try jws.objectField("address"); try jws.print("\"{}\"", .{self.address}); try jws.endObject(); } }; comptime { @export(&ScanOptions.init, .{ .name = "plac_server_scan_options_init" }); } pub const ScanOptions = extern struct { /// How many times a scanner sends request UDP message? count: c_uint, /// How long will a scanner wait for response UDP message? receive_window_ms: c_uint, /// Set default option values. pub fn init(ptr: ?*ScanOptions) callconv(.C) void { if (ptr) |opts| { opts.count = 1; opts.receive_window_ms = 1_000; } } }; pub const ScanResultCode = enum(u8) { ok = 0, unknown_error = 1, out_of_memory = 2, socket_setup_error = 3, udp_send_error = 4, udp_recv_error = 5, null_pointer_args = 6, socket_permission_error = 7, too_many_socket_error = 8, invalid_receive_window = 9, network_unavailable = 10, }; comptime { @export(&ScanResult.next, .{ .name = "plac_scan_result_next" }); @export(&ScanResult.reset, .{ .name = "plac_scan_result_reset" }); @export(&ScanResult.free, .{ .name = "plac_scan_result_free" }); } pub const ScanResult = extern struct { code: ScanResultCode, servers: ?*const []*Server = null, i: usize = 0, /// Returns `null` when the first argument is `null`, `code` field is /// not "ok", or iterator reaches to the end. /// Caller owns the returned `Server`. /// Call `.free()` after use. pub fn next(self_ptr: ?*ScanResult) callconv(.C) ?*Server { const self = self_ptr orelse return null; const servers = self.servers orelse return null; if (self.i >= servers.len) { return null; } const found = servers.*[self.i]; self.i += 1; return found.dupe(); } /// Reset internal iterator. pub fn reset(self_ptr: ?*ScanResult) callconv(.C) void { if (self_ptr) |self| { self.i = 0; } } /// Call to this function does NOT invalidate "Servers" previously returned by `next()`. pub fn free(self_ptr: ?*ScanResult) callconv(.C) void { if (self_ptr) |self| { if (self.servers) |servers| { for (servers.*) |server| { server.free(); } } allocator.destroy(self); } } }; comptime { @export(&ServerScanner.make, .{ .name = "plac_server_scanner_make" }); @export(&ServerScanner.free, .{ .name = "plac_server_scanner_free" }); @export(&ServerScanner.scan, .{ .name = "plac_server_scanner_scan" }); } const ServerScanner = extern struct { sockfd: ?*std.posix.socket_t = null, /// Returns `null` on OOM. pub fn make() callconv(.C) ?*ServerScanner { const scanner = allocator.create(ServerScanner) catch return null; scanner.* = .{}; return scanner; } pub fn free(self_ptr: ?*ServerScanner) callconv(.C) void { ServerScanner.close(self_ptr); } const udp_dst = std.net.Address.initIp4( sood.discovery.multicast_ipv4_address, sood.discovery.udp_port, ); const ScanError = error{ Unknown, SocketSetupError, UDPSendError, UDPRecvError, SocketPermissionDenied, TooManySocketError, InvalidReceiveWindow, NetworkUnavailable, } || std.mem.Allocator.Error; /// Returns `null` on OOM or when an argument is a null pointer. pub fn scan( self_ptr: ?*ServerScanner, opts_ptr: ?*const ScanOptions, ) callconv(.C) ?*ScanResult { const self = self_ptr orelse return null; const result = allocator.create(ScanResult) catch return null; const opts = opts_ptr orelse { result.* = .{ .code = .null_pointer_args, }; return result; }; self.scan_internal(opts, result) catch |err| { result.* = .{ .code = switch (err) { ScanError.Unknown => .unknown_error, ScanError.OutOfMemory => .out_of_memory, ScanError.SocketSetupError => .socket_setup_error, ScanError.UDPSendError => .udp_send_error, ScanError.UDPRecvError => .udp_recv_error, ScanError.SocketPermissionDenied => .socket_permission_error, ScanError.TooManySocketError => .too_many_socket_error, ScanError.InvalidReceiveWindow => .invalid_receive_window, ScanError.NetworkUnavailable => .network_unavailable, }, }; }; return result; } fn scan_internal(self: *ServerScanner, opts: *const ScanOptions, result: *ScanResult) ScanError!void { const sockfd = self.sockfd orelse sockfd: { const fd = try allocator.create(std.posix.socket_t); errdefer allocator.destroy(fd); fd.* = std.posix.socket(std.posix.AF.INET, std.posix.SOCK.DGRAM, 0) catch |err| { return switch (err) { std.posix.SocketError.PermissionDenied => ScanError.SocketPermissionDenied, std.posix.SocketError.SystemFdQuotaExceeded, std.posix.SocketError.ProcessFdQuotaExceeded => ScanError.TooManySocketError, std.posix.SocketError.SystemResources => ScanError.OutOfMemory, else => ScanError.SocketSetupError, }; }; errdefer std.posix.close(fd.*); std.posix.setsockopt( fd.*, std.posix.SOL.SOCKET, std.posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)), ) catch |err| return switch (err) { std.posix.SetSockOptError.PermissionDenied => ScanError.SocketPermissionDenied, std.posix.SetSockOptError.SystemResources => ScanError.OutOfMemory, else => ScanError.SocketSetupError, }; self.sockfd = fd; break :sockfd fd; }; const sec = std.math.divFloor(u32, opts.receive_window_ms, 1_000) catch { return ScanError.InvalidReceiveWindow; }; const usec = 1_000 * (std.math.rem(u32, opts.receive_window_ms, 1_000) catch { return ScanError.InvalidReceiveWindow; }); const timeout = std.posix.timeval{ .sec = sec, .usec = usec }; std.posix.setsockopt( sockfd.*, std.posix.SOL.SOCKET, std.posix.SO.RCVTIMEO, &std.mem.toBytes(timeout), ) catch |err| return switch (err) { std.posix.SetSockOptError.PermissionDenied => ScanError.SocketPermissionDenied, std.posix.SetSockOptError.SystemResources => ScanError.OutOfMemory, else => ScanError.SocketSetupError, }; var servers = std.StringHashMap(*Server).init(allocator); defer servers.deinit(); for (0..opts.count) |_| { _ = std.posix.sendto( sockfd.*, sood.discovery.Query.prebuilt, 0, &udp_dst.any, udp_dst.getOsSockLen(), ) catch |err| return switch (err) { std.posix.SendToError.NetworkSubsystemFailed, std.posix.SendToError.NetworkUnreachable => ScanError.NetworkUnavailable, else => ScanError.UDPSendError, }; while (true) { // Discovery response from servers usually fits under 300 bytes. // Extra bytes for safety. var received: [512]u8 = undefined; var src: std.net.Address = undefined; var src_len: std.posix.socklen_t = udp_dst.getOsSockLen(); const size = std.posix.recvfrom( sockfd.*, &received, 0, &src.any, &src_len, ) catch |err| switch (err) { std.posix.RecvFromError.WouldBlock => break, std.posix.RecvFromError.MessageTooBig => continue, else => return ScanError.UDPRecvError, }; const response = sood.discovery.Response.parse(received[0..size]) catch { // Non-SOOD message. Unlikely but technically possible. continue; }; const stale = servers.get(response.unique_id); defer if (stale) |server| { server.free(); }; var ip_addr = src; ip_addr.setPort(response.http_port); const unique_id = try allocator.dupeZ(u8, response.unique_id); errdefer allocator.free(unique_id); const name = try allocator.dupeZ(u8, response.name); errdefer allocator.free(name); const version = try allocator.dupeZ(u8, response.display_version); errdefer allocator.free(version); const entry = try allocator.create(Server); errdefer allocator.destroy(entry); entry.* = .{ .sockaddr = ip_addr.any, .addrlen = ip_addr.getOsSockLen(), .id = unique_id.ptr, .id_len = unique_id.len, .name = name.ptr, .name_len = name.len, .version = version.ptr, .version_len = version.len, }; try servers.put(response.unique_id, entry); } } const servers_slice = try allocator.create([]*Server); errdefer allocator.destroy(servers_slice); servers_slice.* = try allocator.alloc(*Server, servers.count()); errdefer allocator.free(servers_slice.*); var i: usize = 0; var iter = servers.valueIterator(); while (iter.next()) |server| { servers_slice.*[i] = server.*; i += 1; } result.* = .{ .code = .ok, .servers = servers_slice, }; } pub fn close(self_ptr: ?*ServerScanner) callconv(.C) void { if (self_ptr) |self| { if (self.sockfd) |sockfd| { std.posix.close(sockfd.*); allocator.destroy(sockfd); } } } };
-
-
-
@@ -14,6 +14,7 @@ // limitations under the License.// // SPDX-License-Identifier: Apache-2.0 //! [Deprecated] Migrate to lib.h and let CLI test C API. //! This is an entrypoint of Zig module. const std = @import("std");
-
-
-
@@ -30,6 +30,15 @@ pub fn build(b: *std.Build) !void {const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const core = core: { const dep = b.dependency("plac_core", .{ .target = target, .optimize = optimize, }); break :core dep.artifact("plac_core"); }; // Vala source codes to compile. As Vala does not have module system, // we have to list every source code files to include. const vala_sources = [_][]const u8{
-
@@ -52,10 +61,16 @@// Tell Vala compiler to emit C rather than compile using system C compiler. valac.addArg("--ccode"); valac.addArg("--vapidir"); valac.addDirectoryArg(core.getEmittedIncludeTree()); // Tell Vala what system libraries to use. Perhaps type checking things? for (system_libraries) |lib| { valac.addArgs(&.{ "--pkg", lib }); } valac.addArgs(&.{ "--pkg", "posix" }); valac.addArgs(&.{ "--pkg", "plac_core" }); // Tell Vala to emit C source files under the output directory. // The directory usually is Zig cache directory (like ".zig-cache/o/xxx").
-
@@ -88,6 +103,8 @@ // linker.for (system_libraries) |lib| { exe.linkSystemLibrary(lib); } exe.linkLibrary(core); // At this point, build does not run yet—we can't enumerate C source // directory. Since we already have a list of Vala source code, we can
-
-
gtk/build.zig.zon (new)
-
@@ -0,0 +1,31 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 .{ .name = .plac_gtk, .version = "0.0.0", .fingerprint = 0xd79b7f79b9f2bab4, .minimum_zig_version = "0.14.0", .dependencies = .{ .plac_core = .{ .path = "../core", }, }, .paths = .{ "build.zig", "build.zig.zon", "src/", }, }
-
-
-
@@ -24,6 +24,21 @@ );} protected override void activate() { // TODO: Write this to GUI var opts = Plac.ScanOptions(); stderr.printf("count=%u, window=%u\n", opts.count, opts.receive_window_ms); var scanner = new Plac.ServerScanner(); var result = scanner.scan(opts); while (true) { var server = result.next(); if (server == null) { break; } stderr.printf("%s\n", server.name); } var main_window = new Gtk.ApplicationWindow(this) { default_height = 300, default_width = 300,
-