Changes
17 changed files (+2611/-256)
-
-
@@ -30,13 +30,45 @@ pub fn build(b: *std.Build) !void {const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const sood = sood: { const dep = b.dependency("sood", .{}); break :sood dep.module("sood"); }; const moo = moo: { const dep = b.dependency("libmoo", .{}); break :moo dep.module("moo"); }; const websocket = websocket: { const dep = b.dependency("websocket", .{}); break :websocket dep.module("websocket"); }; const core = core: { const dep = b.dependency("plac_core", .{ .target = target, .optimize = optimize, const lib = b.addLibrary(.{ .name = "plac_core", .linkage = .static, .root_module = b.createModule(.{ .root_source_file = b.path("src/core.zig"), .target = target, .optimize = optimize, }), }); break :core dep.artifact("plac_core"); lib.root_module.addImport("sood", sood); lib.root_module.addImport("moo", moo); lib.root_module.addImport("websocket", websocket); lib.linkLibC(); lib.linkSystemLibrary2("glib-2.0", .{ .preferred_link_mode = .dynamic }); lib.installHeader(b.path("src/core.h"), "plac_core.h"); lib.installHeader(b.path("src/core.vapi"), "plac_core.vapi"); break :core lib; }; // Vala source codes to compile.
-
-
-
@@ -22,6 +22,18 @@ .dependencies = .{.plac_core = .{ .path = "../core", }, .sood = .{ .url = "https://git.pocka.jp/libsood.git/archive/8080245c2696cc6404b0628e5c3eb363cb80014f.tar.gz", .hash = "sood-0.0.0-A_jj-7ITAQAPlaaR2AHFXwPvBWzNBCCPTT--OCHnRQ_i", }, .libmoo = .{ .url = "https://git.pocka.jp/libmoo.git/archive/281d63245052c303c8851c4bfcbb428d78f9b52f.tar.gz", .hash = "libmoo-0.0.0-HVqw0sQVAQBtlGvtbF7pEkBkw5FKNr6zBYbGlHBienyE", }, .websocket = .{ .url = "https://github.com/karlseguin/websocket.zig/archive/c1c53b062eab871b95b70409daadfd6ac3d6df61.tar.gz", .hash = "websocket-0.1.0-ZPISdYBIAwB1yO6AFDHRHLaZSmpdh4Bz4dCmaQUqNNWh", }, }, .paths = .{ "build.zig",
-
-
-
@@ -16,18 +16,15 @@ // SPDX-License-Identifier: Apache-2.0namespace PlacGtkAdwaita { public class App : Adw.Application { private PlacCore.App app; public App(){ Object( application_id: "jp.pocka.plac.gtk-adwaita", flags : ApplicationFlags.DEFAULT_FLAGS ); this.app = new PlacCore.App(); } protected override void activate() { var selector = new ServerSelectorWindow(this, app); var selector = new ServerSelector.Window(this); selector.start(); }
-
-
-
@@ -0,0 +1,109 @@// 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 namespace Plac { public class Connection : GLib.Object { private ConnectionRaw conn; private GLib.Thread<void>? thread; private bool is_closed; public Connection(Discovery.Server server) { this.conn = new ConnectionRaw(server); this.thread = null; this.is_closed = false; } public signal void connection_started(); public signal void out_of_memory_error(); public signal void connection_error(ConnectionErrorEvent event); public signal void connected(ConnectedEvent event); public signal void zones_changed(Transport.ZoneListEvent event); public void activate() { if (thread != null) { return; } is_closed = false; thread = new GLib.Thread<void>("connection-loop", () => { GLib.Idle.add(() => { connection_started(); return false; }); while (true) { if (is_closed) { return; } var event = conn.get_event(); if (event == null) { deactivate(); GLib.Idle.add(() => { out_of_memory_error(); return false; }); return; } switch (event.kind) { case ERROR: { deactivate(); GLib.Idle.add(() => { connection_error(event.get_connection_error_event()); return false; }); break; } case CONNECTED: { conn.subscribe_zones(); GLib.Idle.add(() => { connected(event.get_connected_event()); return false; }); break; } case ZONE_LIST: { GLib.Idle.add(() => { zones_changed(event.get_zone_list_event()); return false; }); break; } } } }); } public void deactivate() { if (thread == null) { return; } // Prevent unnecessary read immediately. is_closed = true; // Schedule thread disposal. Calling `thread.join` immediately results in // `join` from the same thread = deadlock. GLib.Idle.add(() => { thread.join(); return false; }); } } }
-
-
-
@@ -29,8 +29,6 @@[GtkChild] private unowned Gtk.ListBox zone_list; private unowned PlacCore.App core; private Gee.HashMap<string, ZoneSelectorRow>zone_rows = new Gee.HashMap<string, ZoneSelectorRow>(); private string? zone_id = null;
-
@@ -39,85 +37,86 @@ public PlaybackToolbar() {Object(); } public void start(PlacCore.App core) { this.core = core; core.on_server_change(() => { subscribe_zone_change(); }); subscribe_zone_change(); core.lock(); render(); core.unlock(); construct { zone_list.row_activated.connect((row) => { zone_id = ((ZoneSelectorRow) row).zone.id; render(); zone_list_popover.popdown(); this.render(); }); this.render(); } private void subscribe_zone_change() { if (core.server != null) { core.server.on_zone_list_loading_change(() => { GLib.Idle.add(() => { core.lock(); render(); core.unlock(); return false; }); }); public void listen(Plac.Connection conn) { conn.zones_changed.connect((event) => { foreach (string id in event.removed) { GLib.log("Plac", LEVEL_DEBUG, "Zone id=%s removed", id); core.server.load_zones(); } } if (zone_rows.has_key(id)) { var existing = zone_rows[id]; zone_rows.unset(id); zone_list.remove(existing); } } foreach (Plac.Transport.Zone zone in event.added) { GLib.log("Plac", LEVEL_DEBUG, "Zone (%s) id=%s added", zone.name, zone.id); if (zone_rows.has_key(zone.id)) { var existing = zone_rows[zone.id]; existing.zone = zone; existing.render(); } else { var row = new ZoneSelectorRow(zone); zone_rows[zone.id] = row; zone_list.append(row); public void render() { if (core.server == null) { return; } if (zone_id == null) { zone_id = zone.id; } } } unowned PlacCore.Zone? zone = null; foreach (Plac.Transport.Zone zone in event.changed) { GLib.log("Plac", LEVEL_DEBUG, "Zone (%s) id=%s changed", zone.name, zone.id); if (zone_rows.has_key(zone.id)) { var existing = zone_rows[zone.id]; existing.zone = zone; existing.render(); } else { var row = new ZoneSelectorRow(zone); zone_rows[zone.id] = row; zone_list.append(row); foreach (unowned PlacCore.Zone z in core.server.zones) { if (zone_id == null) { zone_id = z.id; break; } if (zone_id == null) { zone_id = zone.id; } } } if (zone_id == z.id) { zone = z; } } // Selected zone has been removed. if (zone_id != null && !zone_rows.has_key(zone_id)) { zone_id = null; foreach (unowned PlacCore.Zone z in core.server.zones) { // Zone was removed. if (zone == null) { zone_id = z.id; zone = z; } foreach (var entry in zone_rows) { zone_id = entry.value.zone.id; break; } } if (zone_rows.has_key(z.id)) { // TODO: Separate update logic from rendering // TODO: Handle zone removal (currently segfaults) var row = zone_rows[z.id]; row.zone = z; row.render(); } else { var row = new ZoneSelectorRow(z); row.render(); zone_list.append(row); zone_rows[z.id] = row; } } this.render(); }); } if (zone == null) { private void render() { var row = zone_rows[zone_id]; if (row == null) { play.visible = true; pause.visible = false; return; } switch (zone.playback_state) { switch (row.zone.playback) { case LOADING: case STOPPED: case PAUSED: play.visible = true;
-
@@ -133,21 +132,21 @@ }class ZoneSelectorRow : Gtk.ListBoxRow { private Gtk.Label label; public unowned PlacCore.Zone zone; public ZoneSelectorRow(PlacCore.Zone zone) { Object(); this.zone = zone; public Plac.Transport.Zone zone { get; construct set; } public ZoneSelectorRow(Plac.Transport.Zone zone) { Object(zone: zone); } construct { label = new Gtk.Label(""); label = new Gtk.Label(zone.name); this.child = label; this.activatable = true; } public void render() { this.label.label = this.zone.name; this.label.label = zone.name; } } }
-
-
-
@@ -26,53 +26,46 @@[GtkChild] private unowned PlaybackToolbar playback_toolbar; private unowned PlacCore.App core; private Plac.Discovery.Server server; private Plac.Connection conn; public MainWindow(Gtk.Application app, PlacCore.App core) { public MainWindow(Gtk.Application app, Plac.Discovery.Server server) { (typeof (ServerConnecting)).ensure(); (typeof (PlaybackToolbar)).ensure(); Object(application: app); this.core = core; this.server = server; this.conn = new Plac.Connection(server); } public void start() { this.playback_toolbar.start(core); conn.connection_started.connect(() => { root_stack.visible_child_name = "loading"; }); conn.connected.connect((event) => { root_stack.visible_child_name = "main"; stderr.printf("Token=%s\n", event.token); }); core.on_connection_change(() => { GLib.Idle.add(() => { core.lock(); this.render(); core.unlock(); return false; }); conn.connection_error.connect((event) => { GLib.log("Plac", LEVEL_ERROR, "Failed to connect: %s", event.code.to_string()); // TODO: Display error message }); this.present(); conn.out_of_memory_error.connect(() => { GLib.log("Plac", LEVEL_ERROR, "Failed to connect: out of memory"); // TODO: Display error message? }); core.lock(); this.render(); core.unlock(); } playback_toolbar.listen(conn); private void render() { switch (core.connection) { case PlacCore.ConnectionState.IDLE: if (core.server == null) { return; } conn.activate(); root_stack.visible_child_name = "main"; this.present(); test_label.label = core.server.name; break; case PlacCore.ConnectionState.BUSY: root_stack.visible_child_name = "loading"; break; default: test_label.label = core.connection.to_string(); break; } test_label.label = server.name; } } }
-
-
-
@@ -15,8 +15,9 @@ //// SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { namespace ServerSelector { [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list.ui")] class ServerSelectorWindow : Adw.ApplicationWindow { class Window : Adw.ApplicationWindow { private enum ScanErrorKind { UNEXPECTED_ERROR, NETWORK_ERROR,
-
@@ -39,179 +40,111 @@ private unowned Adw.StatusPage empty;private ulong error_detail_hid; private unowned PlacCore.App core; public ServerSelectorWindow(Gtk.Application app, PlacCore.App core) { public Window(Gtk.Application app) { Object(application: app); this.core = core; } private string? get_and_create_state_dir() { var state_dir = GLib.Environment.get_user_state_dir(); var dir = GLib.Path.build_path(GLib.Path.DIR_SEPARATOR_S, state_dir, application.application_id); var file = GLib.File.new_for_path(dir); if (!file.query_exists()) { try { file.make_directory(); } catch (Error e) { stderr.printf("Unable to create state directory: %s\n", e.message); return null; } } return dir; } private void set_state_file() { var dir = get_and_create_state_dir(); if (dir == null) { return; } var state_path = GLib.Path.build_path(GLib.Path.DIR_SEPARATOR_S, dir, "state.json"); core.set_state_path(state_path, state_path.length); } public void start() { this.set_state_file(); core.on_restore_complete(() => { if (core.connection == PlacCore.ConnectionState.IDLE) { this.start_scanning(); return; } else if (core.connection == PlacCore.ConnectionState.BUSY) { GLib.Idle.add(() => { this.open_restored_window(); return false; }); } }); core.restore_state(); var scan_action = new SimpleAction("scan_servers", null); scan_action.activate.connect(core.server_selector.load); scan_action.activate.connect(this.scan); this.add_action(scan_action); this.present(); } private void open_restored_window() { var window = new MainWindow(application, core); window.start(); this.close(); return; this.scan(); } private void start_scanning() { core.server_selector.on_change(() => { GLib.Idle.add(() => { core.server_selector.lock(); this.render(); core.server_selector.unlock(); return false; }); }); private void scan() { failure_banner.revealed = false; if (error_detail_hid > 0) { failure_banner.disconnect(error_detail_hid); error_detail_hid = 0; } stack.visible_child_name = "loading"; scan_button.sensitive = false; core.server_selector.load(); Plac.Discovery.scan_async.begin((obj, res) => { var result = Plac.Discovery.scan_async.end(res); core.server_selector.lock(); this.render(); core.server_selector.unlock(); } servers_list.remove_all(); private void render() { switch (core.server_selector.state) { case PlacCore.ServerSelectorState.REFRESHING: case PlacCore.ServerSelectorState.LOADING: failure_banner.revealed = false; if (error_detail_hid > 0) { failure_banner.disconnect(error_detail_hid); error_detail_hid = 0; } stack.visible_child_name = "loading"; scan_button.set_sensitive(false); break; case PlacCore.ServerSelectorState.NOT_LOADED: break; case PlacCore.ServerSelectorState.LOADED: servers_list.remove_all(); switch (result.code) { case OK: break; case UNKNOWN: show_error(UNEXPECTED_ERROR, "Unexpected error"); return; case NETWORK_UNAVAILABLE: show_error(NETWORK_ERROR, "Network unavailable"); return; case SOCKET_PERMISSION_DENIED: show_error(NETWORK_ERROR, "No permission to create UDP socket"); return; case SOCKET_ERROR: show_error(NETWORK_ERROR, "Failed to operate on UDP socket"); return; case OUT_OF_MEMORY: show_error(UNEXPECTED_ERROR, "Out of memory"); return; } if (core.server_selector.entries.length == 0) { servers_list.visible = false; empty.visible = true; scan_button.add_css_class("suggested-action"); } else { servers_list.visible = true; empty.visible = false; scan_button.remove_css_class("suggested-action"); } if (result.servers.length == 0) { servers_list.visible = false; empty.visible = true; scan_button.add_css_class("suggested-action"); stack.visible_child_name = "idle"; scan_button.sensitive = true; return; } foreach (unowned PlacCore.ServerSelectorEntry entry in core.server_selector.entries) { var row = new Adw.ActionRow(); row.title = entry.name; row.subtitle = entry.version; servers_list.visible = true; empty.visible = false; // AdwActionRow needs static widget to be activatable. However, if we link // an instance of `MainWindow` here, the application won't close due to there // are still unclosed windows. This workarounds that design limitation. var box = new Gtk.Box(HORIZONTAL, 0); row.activatable_widget = box; foreach (Plac.Discovery.Server server in result.servers) { var row = new Adw.ActionRow(); row.title = server.name; row.subtitle = server.version; row.activated.connect(() => { var main_window = new MainWindow(this.application, core); main_window.start(); core.connect(entry.id, entry.id.length, null, 0); this.close(); }); // AdwActionRow needs static widget to be activatable. However, if we link // an instance of `MainWindow` here, the application won't close due to there // is a still unclosed window. This workarounds that design flaw. var box = new Gtk.Box(HORIZONTAL, 0); row.activatable_widget = box; servers_list.append(row); } row.activated.connect(() => { var window = new MainWindow(application, server); window.start(); this.close(); }); stack.visible_child_name = "idle"; scan_button.set_sensitive(true); servers_list.append(row); } break; case PlacCore.ServerSelectorState.ERR_UNEXPECTED: show_error(ScanErrorKind.UNEXPECTED_ERROR, "Unexpected error"); break; case PlacCore.ServerSelectorState.ERR_NETWORK_UNAVAILABLE: show_error(ScanErrorKind.NETWORK_ERROR, "Network unavailable"); break; case PlacCore.ServerSelectorState.ERR_SOCKET_PERMISSION: show_error(ScanErrorKind.NETWORK_ERROR, "No permission to create UDP socket"); break; case PlacCore.ServerSelectorState.ERR_OUT_OF_MEMORY: show_error(ScanErrorKind.UNEXPECTED_ERROR, "Out of memory"); break; case PlacCore.ServerSelectorState.ERR_SOCKET: show_error(ScanErrorKind.NETWORK_ERROR, "Failed to operate on a socket"); break; case PlacCore.ServerSelectorState.ERR_THREAD_SPAWN: show_error(ScanErrorKind.UNEXPECTED_ERROR, "Unable to spawn a thread"); break; } stack.visible_child_name = "idle"; scan_button.sensitive = true; }); } private void show_error(ScanErrorKind kind, string message) { scan_button.add_css_class("suggested-action"); this.error_detail_hid = failure_banner.button_clicked.connect(() => { switch (kind) { case ScanErrorKind.NETWORK_ERROR: var dialog = new ServerListNetworkErrorDialog(message); dialog.present(this); break; case ScanErrorKind.UNEXPECTED_ERROR: var dialog = new ServerListUnexpectedErrorDialog(message); dialog.present(this); break; } }); error_detail_hid = failure_banner.button_clicked.connect(() => { switch (kind) { case NETWORK_ERROR: var dialog = new ServerListNetworkErrorDialog(message); dialog.present(this); break; case UNEXPECTED_ERROR: var dialog = new ServerListUnexpectedErrorDialog(message); dialog.present(this); break; } }); failure_banner.revealed = true; stack.visible_child_name = "idle"; scan_button.set_sensitive(true); scan_button.sensitive = true; } } } }
-
-
gtk-adwaita/src/core.h (new)
-
@@ -0,0 +1,146 @@/* * 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 * * === * * C99 header file for helper C API for Plac GTK-Adwaita app. * This file is not checked against a generated library file: carefully write and review * definitions and implementations. */ #ifndef PLAC_GTK_ADWAITA_CORE_H #define PLAC_GTK_ADWAITA_CORE_H #include <stddef.h> #include <stdint.h> // discovery.Server typedef struct { void *__pri; } plac_discovery_server; plac_discovery_server *plac_discovery_server_retain(plac_discovery_server*); void plac_discovery_server_release(plac_discovery_server*); char *plac_discovery_server_id(plac_discovery_server*); char *plac_discovery_server_name(plac_discovery_server*); char *plac_discovery_server_version(plac_discovery_server*); uint16_t *plac_discovery_server_http_port(plac_discovery_server*); // discovery.ScanResult.Code typedef enum { PLAC_DISCOVERY_SCAN_RESULT_OK = 0, PLAC_DISCOVERY_SCAN_RESULT_UNKNOWN = 1, PLAC_DISCOVERY_SCAN_RESULT_NETWORK_UNAVAILABLE = 2, PLAC_DISCOVERY_SCAN_RESULT_SOCKET_PERMISSION_DENIED = 3, PLAC_DISCOVERY_SCAN_RESULT_SOCKET_ERROR = 4, PLAC_DISCOVERY_SCAN_RESULT_OUT_OF_MEMORY = 5, } plac_discovery_scan_result_code; // discovery.ScanResult typedef struct { void *__pri; plac_discovery_server **servers_ptr; size_t servers_len; plac_discovery_scan_result_code code; } plac_discovery_scan_result; plac_discovery_scan_result *plac_discovery_scan_result_retain(plac_discovery_scan_result*); void plac_discovery_scan_result_release(plac_discovery_scan_result*); // discovery plac_discovery_scan_result *plac_discovery_scan(); // transport.PlaybackState typedef enum { PLAC_TRANSPORT_PLAYBACK_LOADING = 0, PLAC_TRANSPORT_PLAYBACK_STOPPED = 1, PLAC_TRANSPORT_PLAYBACK_PAUSED = 2, PLAC_TRANSPORT_PLAYBACK_PLAYING = 3, } plac_transport_playback_state; // transport.Zone typedef struct { void *__pri; char *id; char *name; plac_transport_playback_state playback; } plac_transport_zone; plac_transport_zone *plac_transport_zone_retain(plac_transport_zone*); void plac_transport_zone_release(plac_transport_zone*); // transport.ZoneListEvent typedef struct { void *__pri; plac_transport_zone **added_zones_ptr; size_t added_zones_len; plac_transport_zone **changed_zones_ptr; size_t changed_zones_len; char **removed_zone_ids_ptr; size_t removed_zone_ids_len; } plac_transport_zone_list_event; plac_transport_zone_list_event *plac_transport_zone_list_event_retain(plac_transport_zone_list_event*); void plac_transport_zone_list_event_release(plac_transport_zone_list_event*); // connection.ConnectedEvent typedef struct { void *__pri; char *token; } plac_connection_connected_event; plac_connection_connected_event *plac_connection_connected_event_retain(plac_connection_connected_event*); void plac_connection_connected_event_release(plac_connection_connected_event*); // connection.ConnectionError typedef enum { PLAC_CONNECTION_ERROR_UNKNOWN = 0, PLAC_CONNECTION_ERROR_CLOSED_BY_SERVER = 1, PLAC_CONNECTION_ERROR_OUT_OF_MEMORY = 2, PLAC_CONNECTION_ERROR_UNEXPECTED_RESPONSE = 3, PLAC_CONNECTION_ERROR_NETWORK_UNAVAILABLE = 4, PLAC_CONNECTION_ERROR_NETWORK_ERROR = 5, } plac_connection_connection_error; // connection.ConnectionErrorEvent typedef struct { void *__pri; plac_connection_connection_error code; } plac_connection_connection_error_event; plac_connection_connection_error_event *plac_connection_connection_error_event_retain(plac_connection_connection_error_event*); void plac_connection_connection_error_event_release(plac_connection_connection_error_event*); // connection.Event.Kind typedef enum { PLAC_CONNECTION_EVENT_ERROR = 0, PLAC_CONNECTION_EVENT_CONNECTED = 1, PLAC_CONNECTION_EVENT_ZONE_LIST = 10, } plac_connection_event_kind; // connection.Event typedef struct { void *__pri; plac_connection_event_kind kind; } plac_connection_event; plac_connection_event *plac_connection_event_retain(plac_connection_event*); void plac_connection_event_release(plac_connection_event*); plac_connection_connection_error_event *plac_connection_event_get_connection_error_event(plac_connection_event*); plac_connection_connected_event *plac_connection_event_get_connected_event(plac_connection_event*); plac_transport_zone_list_event *plac_connection_event_get_zone_list_event(plac_connection_event*); // connection.Connection typedef struct { void *__pri; } plac_connection; plac_connection *plac_connection_make(plac_discovery_server*); plac_connection *plac_connection_retain(plac_connection*); void plac_connection_release(plac_connection*); plac_connection_event *plac_connection_get_event(plac_connection*); void plac_connection_subscribe_zones(plac_connection*); #endif
-
-
-
@@ -0,0 +1,272 @@// 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 { namespace Discovery { [CCode ( cname = "plac_discovery_server", ref_function = "plac_discovery_server_retain", unref_function = "plac_discovery_server_release" )] [Compact] public class Server { [CCode (cname = "plac_discovery_server_retain")] public void @ref (); [CCode (cname = "plac_discovery_server_release")] public void unref (); public string id { [CCode (cname = "plac_discovery_server_id")] get; } public string name { [CCode (cname = "plac_discovery_server_name")] get; } public string version { [CCode (cname = "plac_discovery_server_version")] get; } public uint16 http_port { [CCode (cname = "plac_discovery_server_http_port")] get; } } [CCode ( cname = "plac_discovery_scan_result_code", cprefix = "PLAC_DISCOVERY_SCAN_RESULT_", has_type_id = false )] public enum ScanResultCode { OK = 0, UNKNOWN = 1, NETWORK_UNAVAILABLE = 2, SOCKET_PERMISSION_DENIED = 3, SOCKET_ERROR = 4, OUT_OF_MEMORY = 5, } [CCode ( cname = "plac_discovery_scan_result", ref_function = "plac_discovery_scan_result_retain", unref_function = "plac_discovery_scan_result_release" )] [Compact] public class ScanResult { [CCode (cname = "plac_discovery_scan_result_retain")] public void @ref (); [CCode (cname = "plac_discovery_scan_result_release")] public void unref (); [CCode ( cname = "servers_ptr", array_length_cname = "servers_len", array_length_type = "size_t" )] public Server[] servers; public ScanResultCode code; } [CCode (cname = "plac_discovery_scan")] public ScanResult? scan(); public async ScanResult? scan_async() { GLib.SourceFunc callback = scan_async.callback; ScanResult? result = null; new GLib.Thread<void>("server-scanner", () => { result = Plac.Discovery.scan(); GLib.Idle.add((owned) callback); }); yield; return (owned) result; } } namespace Transport { [CCode ( cname = "plac_transport_playback_state", cprefix = "PLAC_TRANSPORT_PLAYBACK_", has_type_id = false )] public enum PlaybackState { LOADING = 0, STOPPED = 1, PAUSED = 2, PLAYING = 3, } [CCode ( cname = "plac_transport_zone", ref_function = "plac_transport_zone_retain", unref_function = "plac_transport_zone_release" )] [Compact] public class Zone { public string id; public string name; public PlaybackState playback; [CCode (cname = "plac_transport_zone_retain")] public void @ref (); [CCode (cname = "plac_transport_zone_release")] public void unref (); } [CCode ( cname = "plac_transport_zone_list_event", ref_function = "plac_transport_zone_list_event_retain", unref_function = "plac_transport_zone_list_event_release" )] [Compact] public class ZoneListEvent { [CCode (cname = "plac_transport_zone_list_event_retain")] public void @ref (); [CCode (cname = "plac_transport_zone_list_event_release")] public void unref (); [CCode ( cname = "added_zones_ptr", array_length_cname = "added_zones_len", array_length_type = "size_t" )] public Zone[] added; [CCode ( cname = "changed_zones_ptr", array_length_cname = "changed_zones_len", array_length_type = "size_t" )] public Zone[] changed; [CCode ( cname = "removed_zone_ids_ptr", array_length_cname = "removed_zone_ids_len", array_length_type = "size_t" )] public string[] removed; } } [CCode ( cname = "plac_connection_connection_error", cprefix = "PLAC_CONNECTION_ERROR_", has_type_id = false )] public enum ConnectionError { UNKNOWN = 0, CLOSED_BY_SERVER = 1, OUT_OF_MEMORY = 2, UNEXPECTED_RESPONSE = 3, NETWORK_UNAVAILABLE = 4, NETWORK_ERROR = 5, } [CCode ( cname = "plac_connection_connection_error_event", ref_function = "plac_connection_connection_error_event_retain", unref_function = "plac_connection_connection_error_event_release" )] [Compact] public class ConnectionErrorEvent { [CCode (cname = "plac_connection_connection_error_event_retain")] public void @ref (); [CCode (cname = "plac_connection_connection_error_event_release")] public void unref (); public ConnectionError code; } [CCode ( cname = "plac_connection_connected_event", ref_function = "plac_connection_connected_event_retain", unref_function = "plac_connection_connected_event_release" )] [Compact] public class ConnectedEvent { [CCode (cname = "plac_connection_connected_event_retain")] public void @ref (); [CCode (cname = "plac_connection_connected_event_release")] public void unref (); public string token; } [CCode ( cname = "plac_connection_event", ref_function = "plac_connection_event_retain", unref_function = "plac_connection_event_release" )] [Compact] private class ConnectionEvent { [CCode (cname = "plac_connection_event_retain")] public void @ref (); [CCode (cname = "plac_connection_event_release")] public void unref (); [CCode ( cname = "plac_connection_event_kind", cprefix = "PLAC_CONNECTION_EVENT_", has_type_id = false )] public enum Kind { ERROR = 0, CONNECTED = 1, ZONE_LIST = 10, } public Kind kind; [CCode (cname = "plac_connection_event_get_connection_error_event")] public ConnectionErrorEvent get_connection_error_event(); [CCode (cname = "plac_connection_event_get_connected_event")] public ConnectedEvent get_connected_event(); [CCode (cname = "plac_connection_event_get_zone_list_event")] public Transport.ZoneListEvent get_zone_list_event(); } [CCode ( cname = "plac_connection", ref_function = "plac_connection_retain", unref_function = "plac_connection_release" )] [Compact] private class ConnectionRaw { [CCode (cname = "plac_connection_make")] public ConnectionRaw(Discovery.Server server); [CCode (cname = "plac_connection_retain")] public void @ref (); [CCode (cname = "plac_connection_release")] public void unref (); [CCode (cname = "plac_connection_get_event")] public ConnectionEvent get_event(); [CCode (cname = "plac_connection_subscribe_zones")] public void subscribe_zones(); } }
-
-
gtk-adwaita/src/core.zig (new)
-
@@ -0,0 +1,72 @@// 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 const std = @import("std"); const connection = @import("./core/connection.zig"); const discovery = @import("./core/discovery.zig"); const transport = @import("./core/transport.zig"); const glib = @cImport({ @cInclude("glib.h"); }); pub const std_options: std.Options = .{ .log_level = .debug, .logFn = log, }; pub fn log( comptime level: std.log.Level, comptime _: @Type(.enum_literal), comptime format: []const u8, args: anytype, ) void { var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); defer arena.deinit(); const allocator = arena.allocator(); var buffer = std.ArrayList(u8).init(allocator); std.fmt.format(buffer.writer(), format, args) catch return; const message = buffer.toOwnedSliceSentinel(0) catch return; const g_level: glib.GLogLevelFlags = switch (level) { .debug => glib.G_LOG_LEVEL_DEBUG, .info => glib.G_LOG_LEVEL_INFO, .warn => glib.G_LOG_LEVEL_WARNING, .err => glib.G_LOG_LEVEL_ERROR, }; glib.g_log("Plac", g_level, message.ptr); } comptime { @export(&discovery.Server.retain, .{ .name = "plac_discovery_server_retain" }); @export(&discovery.Server.release, .{ .name = "plac_discovery_server_release" }); @export(&discovery.Server.getId, .{ .name = "plac_discovery_server_id" }); @export(&discovery.Server.getName, .{ .name = "plac_discovery_server_name" }); @export(&discovery.Server.getVersion, .{ .name = "plac_discovery_server_version" }); @export(&discovery.Server.getHttpPort, .{ .name = "plac_discovery_server_http_port" }); @export(&discovery.ScanResult.retain, .{ .name = "plac_discovery_scan_result_retain" }); @export(&discovery.ScanResult.release, .{ .name = "plac_discovery_scan_result_release" }); @export(&discovery.scan, .{ .name = "plac_discovery_scan" }); connection.export_capi(); transport.export_capi(); }
-
-
-
@@ -0,0 +1,749 @@// 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 const std = @import("std"); const moo = @import("moo"); const websocket = @import("websocket"); const discovery = @import("./discovery.zig"); const extension = @import("./extension.zig").extension; const ping = @import("./services/ping.zig"); const registry = @import("./services/registry.zig"); const TransportService = @import("./services/transport.zig").TransportService; const transport = @import("./transport.zig"); pub const ConnectionError = enum(c_int) { unknown = 0, closed_by_server = 1, out_of_memory = 2, unexpected_response = 3, network_unavailable = 4, network_error = 5, }; pub const ConnectionErrorEvent = extern struct { const cname = "plac_connection_connection_error_event"; const allocator = std.heap.c_allocator; internal: *Internal, code: ConnectionError, const Internal = struct { ref_count: i64 = 1, }; pub fn make(code: ConnectionError) std.mem.Allocator.Error!*ConnectionErrorEvent { const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); internal.* = .{}; const self = try allocator.create(ConnectionErrorEvent); errdefer allocator.destroy(self); self.* = .{ .internal = internal, .code = code, }; return self; } pub fn retain(ptr: ?*ConnectionErrorEvent) callconv(.C) *ConnectionErrorEvent { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.ref_count += 1; return self; } pub fn release(ptr: ?*ConnectionErrorEvent) callconv(.C) void { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.ref_count -= 1; if (self.internal.ref_count == 0) { std.log.debug("Releasing {*}...", .{self}); allocator.destroy(self.internal); allocator.destroy(self); } else if (self.internal.ref_count < 0) { std.log.warn("Over deref detected {*}, count={d}", .{ self, self.internal.ref_count }); } } pub fn export_capi() void { @export(&retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{cname}) }); @export(&release, .{ .name = std.fmt.comptimePrint("{s}_release", .{cname}) }); } }; pub const ConnectedEvent = extern struct { const cname = "plac_connection_connected_event"; const allocator = std.heap.c_allocator; internal: *Internal, token: [*:0]const u8, pub const Internal = struct { token: [:0]const u8, ref_count: i64 = 1, }; /// This function takes ownership of `token`. pub fn make(token: [:0]const u8) std.mem.Allocator.Error!*ConnectedEvent { const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); internal.* = .{ .token = token, }; const self = try allocator.create(ConnectedEvent); errdefer allocator.destroy(self); self.* = .{ .internal = internal, .token = token.ptr, }; return self; } pub fn retain(ptr: ?*ConnectedEvent) callconv(.C) *ConnectedEvent { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.ref_count += 1; return self; } pub fn release(ptr: ?*ConnectedEvent) callconv(.C) void { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.ref_count -= 1; if (self.internal.ref_count == 0) { std.log.debug("Releasing {*}...", .{self}); allocator.free(self.internal.token); allocator.destroy(self.internal); allocator.destroy(self); } else if (self.internal.ref_count < 0) { std.log.warn("Over deref detected {*}, count={d}", .{ self, self.internal.ref_count }); } } pub fn export_capi() void { @export(&retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{cname}) }); @export(&release, .{ .name = std.fmt.comptimePrint("{s}_release", .{cname}) }); } }; pub const Event = extern struct { const cname = "plac_connection_event"; const allocator = std.heap.c_allocator; pub const Kind = enum(c_int) { connection_error = 0, connected = 1, zone_list = 10, }; internal: *Internal, kind: Kind, pub const Internal = struct { const Payload = union(Kind) { connection_error: *ConnectionErrorEvent, connected: *ConnectedEvent, zone_list: *transport.ZoneListEvent, }; payload: Payload, ref_count: i64 = 1, }; pub fn makeConnectionError(err: anyerror) std.mem.Allocator.Error!*Event { const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); const connection_error = try ConnectionErrorEvent.make(switch (err) { error.OutOfMemory => .out_of_memory, error.NetworkUnavailable => .network_unavailable, error.SocketError, error.SocketPermissionDenied => .network_error, error.RequestIdMismatch => .unexpected_response, error.ClosedByServer, error.ReadClosedConnection => .closed_by_server, else => .unknown, }); errdefer connection_error.release(); internal.* = .{ .payload = .{ .connection_error = connection_error.retain() }, }; const self = try allocator.create(Event); errdefer allocator.destroy(self); self.* = .{ .internal = internal, .kind = .connection_error, }; return self; } /// This function takes ownership of `token`. pub fn makeConnected(token: [:0]const u8) std.mem.Allocator.Error!*Event { const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); const connected = try ConnectedEvent.make(token); errdefer connected.release(); internal.* = .{ .payload = .{ .connected = connected.retain() }, }; const self = try allocator.create(Event); errdefer allocator.destroy(self); self.* = .{ .internal = internal, .kind = .connected, }; return self; } pub fn makeZoneListFromInitial( res: *const TransportService.SubscribeZoneChanges.InitialResponse, ) std.mem.Allocator.Error!*Event { const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); const zone_list = try transport.ZoneListEvent.makeFromInitial(res); errdefer zone_list.release(); internal.* = .{ .payload = .{ .zone_list = zone_list.retain() }, }; const self = try allocator.create(Event); errdefer allocator.destroy(self); self.* = .{ .internal = internal, .kind = .zone_list, }; return self; } pub fn makeZoneListFromChanges( res: *const TransportService.SubscribeZoneChanges.Response, ) std.mem.Allocator.Error!*Event { const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); const zone_list = try transport.ZoneListEvent.makeFromChanges(res); errdefer zone_list.release(); internal.* = .{ .payload = .{ .zone_list = zone_list.retain() }, }; const self = try allocator.create(Event); errdefer allocator.destroy(self); self.* = .{ .internal = internal, .kind = .zone_list, }; return self; } pub fn retain(ptr: ?*Event) callconv(.C) *Event { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.ref_count += 1; return self; } pub fn release(ptr: ?*Event) callconv(.C) void { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.ref_count -= 1; if (self.internal.ref_count == 0) { std.log.debug("Releasing {*}...", .{self}); switch (self.internal.payload) { .connection_error => |ev| ev.release(), .connected => |ev| ev.release(), .zone_list => |ev| ev.release(), } allocator.destroy(self.internal); allocator.destroy(self); } else if (self.internal.ref_count < 0) { std.log.warn("Over deref detected {*}, count={d}", .{ self, self.internal.ref_count }); } } fn exportEventCastFunction(comptime TargetEvent: type, comptime kind: Kind) void { const Caster = struct { pub const fn_name = std.fmt.comptimePrint("{s}_get_{s}_event", .{ cname, @tagName(kind) }); pub fn cast(ptr: ?*Event) callconv(.C) *TargetEvent { const self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}", .{fn_name}), ); if (self.internal.payload == kind) { return @field(self.internal.payload, @tagName(kind)); } std.log.err("{s} called on {s}", .{ fn_name, @tagName(self.internal.payload) }); unreachable; } }; @export(&Caster.cast, .{ .name = Caster.fn_name }); } pub fn export_capi() void { @export(&retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{cname}) }); @export(&release, .{ .name = std.fmt.comptimePrint("{s}_release", .{cname}) }); Event.exportEventCastFunction(ConnectionErrorEvent, .connection_error); Event.exportEventCastFunction(ConnectedEvent, .connected); Event.exportEventCastFunction(transport.ZoneListEvent, .zone_list); } }; pub const Connection = extern struct { const cname = "plac_connection"; const allocator = std.heap.c_allocator; internal: *Internal, pub const Internal = struct { server: *discovery.Server, ws: ?websocket.Client = null, request_id: i64 = 0, subscription_id: u64 = 0, host: []const u8, zone_subscription_request_id: ?i64 = null, ref_count: i64 = 1, fn init(server: *discovery.Server) !Internal { var addr = std.ArrayList(u8).init(allocator); defer addr.deinit(); try addr.writer().print("{}", .{server.internal.address}); const addr_string = try addr.toOwnedSlice(); defer allocator.free(addr_string); // Zig std always prints "<addr>:<port>" for IPv4 and IPv6 const port_start = std.mem.lastIndexOfScalar(u8, addr_string, ':') orelse { unreachable; }; const host = try allocator.dupe(u8, addr_string[0..port_start]); errdefer allocator.free(host); return .{ .server = server.retain(), .host = host, }; } fn deinit(self: *Internal) void { if (self.ws) |*ws| { ws.deinit(); } allocator.free(self.host); self.server.release(); } }; pub fn make(server_ptr: ?*discovery.Server) callconv(.C) ?*Connection { const server = server_ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); const self = allocator.create(Connection) catch return null; const internal = allocator.create(Internal) catch { allocator.destroy(self); return null; }; internal.* = Internal.init(server) catch |err| { std.log.err("Unable to establish connection to Roon Server: {s}", .{@errorName(err)}); allocator.destroy(internal); allocator.destroy(self); return null; }; self.* = .{ .internal = internal }; return self; } pub fn retain(ptr: ?*Connection) callconv(.C) *Connection { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.ref_count += 1; return self; } pub fn release(ptr: ?*Connection) callconv(.C) void { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.ref_count -= 1; if (self.internal.ref_count == 0) { std.log.debug("Releasing {*}...", .{self}); self.internal.deinit(); allocator.destroy(self.internal); allocator.destroy(self); } else if (self.internal.ref_count < 0) { std.log.warn("Over deref detected {*}, count={d}", .{ self, self.internal.ref_count }); } } pub fn getEvent(ptr: ?*Connection) callconv(.C) ?*Event { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); var ws = self.internal.ws orelse { // TODO: Pass saved token const new_token = self.connect(null) catch |err| { std.log.err("Unable to connect: {s}", .{@errorName(err)}); return Event.makeConnectionError(err) catch |make_err| { std.log.err("Unable to compose {s} event: {s}", .{ @tagName(Event.Kind.connection_error), @errorName(make_err), }); return null; }; }; return Event.makeConnected(new_token) catch |err| { std.log.err("Unable to compose {s} event: {s}", .{ @tagName(Event.Kind.connected), @errorName(err), }); return null; }; }; while (true) { const meta, const header_ctx, const msg = readMessage(&ws) catch |err| { std.log.err("Failed to read a message: {s}", .{@errorName(err)}); return Event.makeConnectionError(err) catch |make_err| { std.log.err("Unable to compose {s} event: {s}", .{ @tagName(Event.Kind.connection_error), @errorName(make_err), }); return null; }; }; defer ws.done(msg); const header: moo.NoBodyHeaders, _ = moo.NoBodyHeaders.parse(msg.data, header_ctx) catch |err| { std.log.err("Invalid MOO message header: {s}", .{@errorName(err)}); continue; }; if (self.internal.zone_subscription_request_id) |req_id| { if (header.request_id == req_id) { if (std.mem.eql(u8, meta.service, "Subscribed")) { const res = TransportService.SubscribeZoneChanges.initialResponse( allocator, header_ctx, msg.data, ) catch |err| { std.log.err( "Received unexpected zone subscription response: {s}", .{@errorName(err)}, ); continue; }; defer res.deinit(); return Event.makeZoneListFromInitial(res.value) catch |make_err| { std.log.err("Unable to compose {s} event: {s}", .{ @tagName(Event.Kind.zone_list), @errorName(make_err), }); return null; }; } if (std.mem.eql(u8, meta.service, "Changed")) { const res = TransportService.SubscribeZoneChanges.response( allocator, header_ctx, msg.data, ) catch |err| { std.log.err( "Received unexpected zone change response: {s}", .{@errorName(err)}, ); continue; }; defer res.deinit(); return Event.makeZoneListFromChanges(res.value) catch |err| { std.log.err("Unable to compose {s} event: {s}", .{ @tagName(Event.Kind.zone_list), @errorName(err), }); return null; }; } if (std.mem.eql(u8, meta.service, "Unsubscribed")) { self.internal.zone_subscription_request_id = null; // TODO: Return unsubscribed event continue; } std.log.warn("Unknown response received for zone subscription: {s}", .{ meta.service, }); continue; } } std.log.debug("Got message on {s}", .{meta.service}); // TODO: Return an event } } const ReadError = error{ ClosedByServer, ReadClosedConnection, RequestIdMismatch, }; /// Returns a token. Caller owns the returned memory. fn connect(self: *Connection, token: ?[]const u8) ![:0]const u8 { std.log.debug( "Establishing WebSocket connection to {}...", .{self.internal.server.internal.address}, ); var ws = try websocket.Client.init(allocator, .{ .host = self.internal.host, .port = self.internal.server.internal.address.getPort(), }); errdefer ws.deinit(); std.log.debug("Performing WebSocket handshake...", .{}); try ws.handshake("/api", .{ .timeout_ms = 1_000 }); var request_id: i64 = 0; { { std.log.debug("Checking server status...", .{}); const req = try registry.RegistryService.Info.request(allocator, request_id); defer allocator.free(req); try ws.writeBin(req); } _, const header_ctx, const msg = try readMessage(&ws); defer ws.done(msg); const header: moo.NoBodyHeaders, _ = try moo.NoBodyHeaders.parse(msg.data, header_ctx); if (header.request_id != request_id) { return ReadError.RequestIdMismatch; } const res = try registry.RegistryService.Info.response( allocator, header_ctx, msg.data, ); defer res.deinit(); request_id += 1; } const new_token = new_token: { { std.log.debug("Registering extension...", .{}); const req = try registry.RegistryService.Register.request( allocator, request_id, &extension, token, ); defer allocator.free(req); try ws.writeBin(req); } const meta, const header_ctx, const msg = try readMessage(&ws); defer ws.done(msg); const header: moo.NoBodyHeaders, _ = try moo.NoBodyHeaders.parse(msg.data, header_ctx); if (header.request_id != request_id) { return ReadError.RequestIdMismatch; } const res = try registry.RegistryService.Register.response( allocator, &meta, header_ctx, msg.data, ); defer res.deinit(); request_id += 1; break :new_token try allocator.dupeZ(u8, res.value.token); }; self.internal.request_id = request_id; self.internal.ws = ws; return new_token; } /// Caller is responsible for closing message by calling `ws.done()`. fn readMessage(ws: *websocket.Client) !struct { moo.Metadata, moo.HeaderParsingContext, websocket.Message, } { while (true) { const msg = ws.read() catch |err| { switch (err) { error.Closed => return ReadError.ReadClosedConnection, else => { std.log.warn("Unable to read WebSocket message: {s}", .{@errorName(err)}); continue; }, } } orelse unreachable; switch (msg.type) { // NOTE: roon-node-api does not check whether message is binaryType. .text, .binary => { const meta, const header_ctx = moo.Metadata.parse(msg.data) catch |err| { std.log.warn("Failed to parse MOO metadata: {s}", .{@errorName(err)}); continue; }; if (std.mem.eql(u8, ping.PingService.ping_id, meta.service)) { writePong(ws, header_ctx, msg.data) catch |err| { std.log.warn( "Failed to respond to ping request: {s}", .{@errorName(err)}, ); }; continue; } return .{ meta, header_ctx, msg }; }, .ping => ws.writePong(msg.data) catch |err| { std.log.warn("Failed to respond to ping: {s}", .{@errorName(err)}); }, .pong => {}, .close => return ReadError.ClosedByServer, } } } fn writePong( ws: *websocket.Client, header_ctx: moo.HeaderParsingContext, message: []const u8, ) !void { var buffer = std.ArrayList(u8).init(allocator); defer buffer.deinit(); try ping.PingService.ping(buffer.writer(), header_ctx, message); try ws.writeBin(buffer.items); } pub fn subscribeZones(ptr: ?*Connection) callconv(.C) void { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); var ws = self.internal.ws orelse { std.log.err("{s}_{s} called, but WebSocket connection is not ready", .{ cname, @src().fn_name }); return; }; const req_id = self.internal.request_id; self.internal.request_id += 1; const sub_id = self.internal.subscription_id; self.internal.subscription_id += 1; { std.log.debug("Subscribing to zone changes...", .{}); const req = TransportService.SubscribeZoneChanges.request( allocator, req_id, sub_id, ) catch |err| { std.log.err("Unable to compose zone subscription request: {s}", .{@errorName(err)}); return; }; defer allocator.free(req); ws.writeBin(req) catch |err| { std.log.err("Unable to write subscription request: {s}", .{@errorName(err)}); return; }; } self.internal.zone_subscription_request_id = req_id; } pub fn export_capi() void { @export(&make, .{ .name = std.fmt.comptimePrint("{s}_make", .{cname}) }); @export(&retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{cname}) }); @export(&release, .{ .name = std.fmt.comptimePrint("{s}_release", .{cname}) }); @export(&getEvent, .{ .name = std.fmt.comptimePrint("{s}_get_event", .{cname}) }); @export(&subscribeZones, .{ .name = std.fmt.comptimePrint("{s}_subscribe_zones", .{cname}) }); } }; pub fn export_capi() void { ConnectionErrorEvent.export_capi(); ConnectedEvent.export_capi(); Event.export_capi(); Connection.export_capi(); }
-
-
-
@@ -0,0 +1,387 @@// 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 const std = @import("std"); const sood = @import("sood"); const udp_dst = std.net.Address.initIp4( sood.discovery.multicast_ipv4_address, sood.discovery.udp_port, ); const udp_send_tries = 4; const udp_receive_window_ms = 1300; pub const Server = extern struct { internal: *Internal, pub const Internal = struct { id: [:0]const u8, name: [:0]const u8, version: [:0]const u8, address: std.net.Address, ref_count: i64 = 1, }; pub fn make(resp: *const sood.discovery.Response, addr: *const std.net.Address) !*Server { const allocator = std.heap.c_allocator; const self = try allocator.create(Server); errdefer allocator.destroy(self); const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); const id = try allocator.dupeZ(u8, resp.unique_id); errdefer allocator.free(id); const name = try allocator.dupeZ(u8, resp.name); errdefer allocator.free(name); const version = try allocator.dupeZ(u8, resp.display_version); errdefer allocator.free(version); var address = addr.*; address.setPort(resp.http_port); internal.* = .{ .id = id, .name = name, .version = version, .address = address, }; self.* = .{ .internal = internal, }; return self; } pub fn retain(ptr: ?*Server) callconv(.C) *Server { var self = ptr orelse @panic("Received null pointer on Server.retain"); self.internal.ref_count += 1; return self; } pub fn release(ptr: ?*Server) callconv(.C) void { var self = ptr orelse @panic("Received null pointer on Server.release"); self.internal.ref_count -= 1; if (self.internal.ref_count == 0) { std.log.debug("Releasing {*}...", .{self}); const allocator = std.heap.c_allocator; allocator.free(self.internal.id); allocator.free(self.internal.name); allocator.free(self.internal.version); allocator.destroy(self.internal); allocator.destroy(self); } else if (self.internal.ref_count < 0) { std.log.warn("Over deref detected {*}, count={d}", .{ self, self.internal.ref_count }); } } pub fn getId(ptr: ?*const Server) callconv(.C) [*:0]const u8 { const self = ptr orelse @panic("Received null pointer on Server.id"); return self.internal.id.ptr; } pub fn getName(ptr: ?*const Server) callconv(.C) [*:0]const u8 { const self = ptr orelse @panic("Received null pointer on Server.name"); return self.internal.name.ptr; } pub fn getVersion(ptr: ?*const Server) callconv(.C) [*:0]const u8 { const self = ptr orelse @panic("Received null pointer on Server.version"); return self.internal.version.ptr; } pub fn getHttpPort(ptr: ?*const Server) callconv(.C) u16 { const self = ptr orelse @panic("Received null pointer on Server.httpPort"); return self.internal.address.getPort(); } }; pub const ScanResult = extern struct { internal: *Internal, servers_ptr: [*]*Server, servers_len: usize, code: Code = .unknown, pub const Code = enum(c_int) { ok = 0, unknown = 1, network_unavailable = 2, socket_permission_denied = 3, socket_error = 4, out_of_memory = 5, }; pub const Internal = struct { servers: []*Server = &.{}, ref_count: i64 = 1, }; pub fn make() !*ScanResult { const allocator = std.heap.c_allocator; const self = try allocator.create(ScanResult); errdefer allocator.destroy(self); const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); const servers: []*Server = &.{}; internal.* = .{}; self.* = .{ .internal = internal, .servers_ptr = servers.ptr, .servers_len = servers.len, }; return self; } pub fn setServers(self: *ScanResult, input: *const std.StringHashMap(*Server)) !void { const allocator = std.heap.c_allocator; const servers = try allocator.alloc(*Server, input.count()); var i: usize = 0; var iter = input.valueIterator(); while (iter.next()) |server| { std.log.debug("Found server ({s})", .{server.*.getName()}); servers[i] = server.*; i += 1; } self.internal.servers = servers; self.servers_ptr = servers.ptr; self.servers_len = servers.len; } pub fn retain(ptr: ?*ScanResult) callconv(.C) *ScanResult { var self = ptr orelse @panic("Received null pointer on ScanResult.retain"); self.internal.ref_count += 1; return self; } pub fn release(ptr: ?*ScanResult) callconv(.C) void { var self = ptr orelse @panic("Received null pointer on ScanResult.release"); self.internal.ref_count -= 1; if (self.internal.ref_count == 0) { std.log.debug("Releasing {*}...", .{self}); const allocator = std.heap.c_allocator; for (self.internal.servers) |server| { server.release(); } allocator.free(self.internal.servers); allocator.destroy(self.internal); allocator.destroy(self); } else if (self.internal.ref_count < 0) { std.log.warn("Over deref detected {*}, count={d}", .{ self, self.internal.ref_count }); } } }; pub fn scan() callconv(.C) ?*ScanResult { const result = ScanResult.make() catch { return null; }; var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); defer arena.deinit(); scanInternal(arena.allocator(), result) catch |err| { result.code = switch (err) { error.OutOfMemory => .out_of_memory, ScanError.NetworkUnavailable => .network_unavailable, ScanError.SocketPermissionDenied => .socket_permission_denied, ScanError.SocketError => .socket_error, }; }; return result; } const ScanError = error{ NetworkUnavailable, SocketPermissionDenied, SocketError, } || std.mem.Allocator.Error; fn scanInternal(allocator: std.mem.Allocator, result: *ScanResult) !void { const sockfd = try createSocket(); defer std.posix.close(sockfd); var servers = std.StringHashMap(*Server).init(allocator); errdefer servers.deinit(); for (0..udp_send_tries) |_| { try sendDiscoveryQuery(sockfd); while (true) { std.log.debug("Waiting for UDP message...", .{}); // 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 => { std.log.debug("UDP read timeout.", .{}); break; }, std.posix.RecvFromError.MessageTooBig => { std.log.warn("Unable to read UDP message (message too big)", .{}); continue; }, else => return ScanError.SocketError, }; std.log.debug("Got UDP message.", .{}); const response = sood.discovery.Response.parse(received[0..size]) catch |err| { std.log.warn( "Unable to parse received UDP message as SOOD message: {s}", .{@errorName(err)}, ); // Non-SOOD message. Unlikely but technically possible. continue; }; const stale = servers.getPtr(response.unique_id); defer if (stale) |server| { server.*.release(); }; var server = try Server.make(&response, &src); errdefer server.release(); try servers.put(server.internal.id, server); } } try result.setServers(&servers); result.code = .ok; } fn createSocket() !std.posix.socket_t { std.log.debug("Opening UDP socket...", .{}); const sockfd = 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.SystemResources => ScanError.OutOfMemory, else => ScanError.SocketError, }; }; errdefer std.posix.close(sockfd); std.posix.setsockopt( sockfd, 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.SocketError, }; const sec = comptime std.math.divFloor(u32, udp_receive_window_ms, 1_000) catch |err| { @compileError( std.fmt.comptimePrint( "Cannot divide udp_receive_window_ms ({d}) by 1,000: {s}", .{ udp_receive_window_ms, @errorName(err) }, ), ); }; const usec = comptime usec: { break :usec @min( std.math.maxInt(i32), 1_000 * (std.math.rem(u32, udp_receive_window_ms, 1_000) catch |err| { @compileError( std.fmt.comptimePrint( "Cannot get reminder of udp_receive_window_ms ({d}): {s}", .{ udp_receive_window_ms, @errorName(err) }, ), ); }), ); }; std.log.debug("Setting UDP read timeout to {d}ms ({d}sec, {d}usec)", .{ udp_receive_window_ms, sec, usec, }); 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.SocketError, }; return sockfd; } fn sendDiscoveryQuery(sockfd: std.posix.socket_t) !void { std.log.debug("Sending server discovery message to {}", .{udp_dst}); _ = std.posix.sendto( sockfd, sood.discovery.Query.prebuilt, 0, &udp_dst.any, udp_dst.getOsSockLen(), ) catch |err| { std.log.err("Failed to send discovery message: {s}", .{@errorName(err)}); return switch (err) { std.posix.SendToError.NetworkSubsystemFailed, std.posix.SendToError.NetworkUnreachable, => ScanError.NetworkUnavailable, else => ScanError.SocketError, }; }; }
-
-
-
@@ -0,0 +1,30 @@// 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 const Extension = @import("./services/registry.zig").Extension; const PingService = @import("./services/ping.zig").PingService; const TransportService = @import("./services/transport.zig").TransportService; pub const extension = Extension{ .id = "jp.pocka.plac", .display_name = "Plac", .version = "0.0.0-dev", .publisher = "Shota FUJI", .email = "pockawoooh@gmail.com", .required_services = &.{TransportService.id}, .optional_services = &.{}, .provided_services = &.{PingService.id}, };
-
-
-
@@ -0,0 +1,39 @@// 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 const std = @import("std"); const moo = @import("moo"); pub const PingService = struct { pub const id = "com.roonlabs.ping:1"; pub const ping_id = id ++ "/ping"; pub fn ping(writer: anytype, parser_ctx: moo.HeaderParsingContext, message: []const u8) !void { const req_header, _ = try moo.NoBodyHeaders.parse(message, parser_ctx); const meta = moo.Metadata{ .service = "Success", .verb = "COMPLETE", }; const body = moo.NoBody{}; const header = body.getHeader(req_header.request_id); try moo.encode(writer, meta, header, body); } };
-
-
-
@@ -0,0 +1,178 @@// 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 const std = @import("std"); const moo = @import("moo"); pub const Extension = struct { id: []const u8, display_name: []const u8, version: []const u8, publisher: []const u8, email: []const u8, required_services: []const []const u8, optional_services: []const []const u8, provided_services: []const []const u8, }; /// Extension object to send to Roon Server. const ExtensionRegistration = struct { static: *const Extension, token: ?[]const u8 = null, pub fn jsonStringify(self: *const @This(), jws: anytype) !void { try jws.beginObject(); try jws.objectField("extension_id"); try jws.write(self.static.id); try jws.objectField("display_name"); try jws.write(self.static.display_name); try jws.objectField("display_version"); try jws.write(self.static.version); try jws.objectField("publisher"); try jws.write(self.static.publisher); try jws.objectField("email"); try jws.write(self.static.email); if (self.token) |token| { try jws.objectField("token"); try jws.write(token); } try jws.objectField("required_services"); try jws.write(self.static.required_services); try jws.objectField("optional_services"); try jws.write(self.static.optional_services); try jws.objectField("provided_services"); try jws.write(self.static.provided_services); try jws.endObject(); } }; pub const RegistryService = struct { const id = "com.roonlabs.registry:1"; pub const Info = struct { pub const Response = struct { core_id: []const u8, display_name: []const u8, display_version: []const u8, }; /// Caller owns the returned memory pub fn request(allocator: std.mem.Allocator, request_id: i64) ![]u8 { const meta = moo.Metadata{ .service = id ++ "/info", .verb = "REQUEST", }; const body = moo.NoBody{}; const header = body.getHeader(request_id); const buf = try allocator.alloc( u8, meta.getEncodeSize() + header.getEncodeSize() + body.getEncodeSize(), ); errdefer allocator.free(buf); var fbs = std.io.fixedBufferStream(buf); try moo.encode(fbs.writer(), meta, header, body); return buf; } /// Caller is responsible to release the returned resource by calling `.deinit()`. pub fn response( allocator: std.mem.Allocator, header_ctx: moo.HeaderParsingContext, message: []const u8, ) !moo.JsonBody(Response) { _, const body_ctx = try moo.WellKnownHeaders.parse(message, header_ctx); return try moo.JsonBody(Response).parse(allocator, message, body_ctx, .{ .ignore_unknown_fields = true, .allocate = .alloc_always, }); } }; pub const Register = struct { pub const Response = struct { core_id: []const u8, token: []const u8, }; pub const Error = error{ NonSuccessResponse, }; /// Caller owns the returned memory pub fn request( allocator: std.mem.Allocator, request_id: i64, extension: *const Extension, token: ?[]const u8, ) ![]u8 { const meta = moo.Metadata{ .service = id ++ "/register", .verb = "REQUEST", }; const body = moo.JsonBody(ExtensionRegistration).init(&.{ .static = extension, .token = token, }); const header = body.getHeader(request_id); const buf = try allocator.alloc( u8, meta.getEncodeSize() + header.getEncodeSize() + body.getEncodeSize(), ); errdefer allocator.free(buf); var fbs = std.io.fixedBufferStream(buf); try moo.encode(fbs.writer(), meta, header, body); return buf; } /// Caller is responsible to release the returned resource by calling `.deinit()`. pub fn response( allocator: std.mem.Allocator, meta: *const moo.Metadata, header_ctx: moo.HeaderParsingContext, message: []const u8, ) !moo.JsonBody(Response) { if (!std.mem.eql(u8, meta.service, "Registered")) { std.log.err("Expected \"Registered\" for /register endpoint, got \"{s}\"\n", .{meta.service}); return Error.NonSuccessResponse; } _, const body_ctx = try moo.WellKnownHeaders.parse(message, header_ctx); return try moo.JsonBody(Response).parse(allocator, message, body_ctx, .{ .ignore_unknown_fields = true, .allocate = .alloc_always, }); } }; };
-
-
-
@@ -0,0 +1,122 @@// 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 const std = @import("std"); const moo = @import("moo"); pub const TransportService = struct { pub const id = "com.roonlabs.transport:2"; pub const PlaybackState = enum { playing, paused, loading, stopped, pub fn jsonParseFromValue( _: std.mem.Allocator, value: std.json.Value, _: std.json.ParseOptions, ) std.json.ParseFromValueError!@This() { return switch (value) { .string => |v| { inline for (@typeInfo(@This()).@"enum".fields) |field| { if (std.mem.eql(u8, field.name, v)) { return @enumFromInt(field.value); } } return std.json.ParseFromValueError.InvalidEnumTag; }, else => std.json.ParseFromValueError.UnexpectedToken, }; } }; pub const Zone = struct { zone_id: []const u8, display_name: []const u8, state: PlaybackState, }; pub const SubscribeZoneChanges = struct { pub const Request = struct { subscription_key: []const u8, }; pub fn request(allocator: std.mem.Allocator, request_id: i64, subscription_id: u64) ![]u8 { const meta = moo.Metadata{ .service = id ++ "/subscribe_zones", .verb = "REQUEST", }; var sub_id_buf: [std.fmt.count("{}", .{std.math.maxInt(u64)})]u8 = undefined; var sub_id_fbs = std.io.fixedBufferStream(&sub_id_buf); try std.fmt.format(sub_id_fbs.writer(), "{}", .{subscription_id}); const body = moo.JsonBody(Request).init(&.{ .subscription_key = sub_id_fbs.getWritten(), }); const header = body.getHeader(request_id); const buf = try allocator.alloc( u8, meta.getEncodeSize() + header.getEncodeSize() + body.getEncodeSize(), ); errdefer allocator.free(buf); var fbs = std.io.fixedBufferStream(buf); try moo.encode(fbs.writer(), meta, header, body); return buf; } pub const InitialResponse = struct { zones: []const Zone, }; pub fn initialResponse( allocator: std.mem.Allocator, header_ctx: moo.HeaderParsingContext, message: []const u8, ) !moo.JsonBody(InitialResponse) { _, const body_ctx = try moo.WellKnownHeaders.parse(message, header_ctx); return try moo.JsonBody(InitialResponse).parse(allocator, message, body_ctx, .{ .ignore_unknown_fields = true, .allocate = .alloc_always, }); } pub const Response = struct { zones_removed: []const []const u8 = &.{}, zones_added: []const Zone = &.{}, zones_changed: []const Zone = &.{}, }; pub fn response( allocator: std.mem.Allocator, header_ctx: moo.HeaderParsingContext, message: []const u8, ) !moo.JsonBody(Response) { _, const body_ctx = try moo.WellKnownHeaders.parse(message, header_ctx); return try moo.JsonBody(Response).parse(allocator, message, body_ctx, .{ .ignore_unknown_fields = true, .allocate = .alloc_always, }); } }; };
-
-
-
@@ -0,0 +1,285 @@// 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 const std = @import("std"); const TransportService = @import("./services/transport.zig").TransportService; pub const PlaybackState = enum(c_int) { loading = 0, stopped = 1, paused = 2, playing = 3, }; pub const Zone = extern struct { const cname = "plac_transport_zone"; const allocator = std.heap.c_allocator; internal: *Internal, id: [*:0]const u8, name: [*:0]const u8, playback: PlaybackState, pub const Internal = struct { id: [:0]const u8, name: [:0]const u8, ref_count: i64 = 1, }; pub fn make(src: *const TransportService.Zone) std.mem.Allocator.Error!*Zone { const id = try allocator.dupeZ(u8, src.zone_id); errdefer allocator.free(id); const name = try allocator.dupeZ(u8, src.display_name); errdefer allocator.free(name); const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); internal.* = .{ .id = id, .name = name, }; const self = try allocator.create(Zone); errdefer allocator.destroy(self); self.* = .{ .internal = internal, .id = id.ptr, .name = name.ptr, .playback = switch (src.state) { .loading => .loading, .stopped => .stopped, .paused => .paused, .playing => .playing, }, }; return self; } pub fn retain(ptr: ?*Zone) callconv(.C) *Zone { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.ref_count += 1; return self; } pub fn release(ptr: ?*Zone) callconv(.C) void { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.ref_count -= 1; if (self.internal.ref_count == 0) { std.log.debug("Releasing {*}...", .{self}); allocator.free(self.internal.id); allocator.free(self.internal.name); allocator.destroy(self.internal); allocator.destroy(self); } else if (self.internal.ref_count < 0) { std.log.warn("Over deref detected {*}, count={d}", .{ self, self.internal.ref_count }); } } pub fn export_capi() void { @export(&retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{cname}) }); @export(&release, .{ .name = std.fmt.comptimePrint("{s}_release", .{cname}) }); } }; pub const ZoneListEvent = extern struct { const cname = "plac_transport_zone_list_event"; const allocator = std.heap.c_allocator; internal: *Internal, added_zones_ptr: [*]const *Zone, added_zones_len: usize, changed_zones_ptr: [*]const *Zone, changed_zones_len: usize, removed_zone_ids_ptr: [*]const [*:0]const u8, removed_zone_ids_len: usize, pub const Internal = struct { added_zones: []const *Zone, changed_zones: []const *Zone, removed_zone_ids: []const [*:0]const u8, ref_count: i64 = 1, }; pub fn makeFromChanges(event: *const TransportService.SubscribeZoneChanges.Response) std.mem.Allocator.Error!*ZoneListEvent { const added_zones = try allocator.alloc(*Zone, event.zones_added.len); errdefer allocator.free(added_zones); var added_i: usize = 0; errdefer { for (0..added_i) |i| { added_zones[i].release(); } } for (event.zones_added) |src| { added_zones[added_i] = try Zone.make(&src); added_i += 1; } const changed_zones = try allocator.alloc(*Zone, event.zones_changed.len); errdefer allocator.free(changed_zones); var changed_i: usize = 0; errdefer { for (0..changed_i) |i| { changed_zones[i].release(); } } for (event.zones_changed) |src| { changed_zones[changed_i] = try Zone.make(&src); changed_i += 1; } const removed_zone_ids = try allocator.alloc([*:0]const u8, event.zones_removed.len); errdefer allocator.free(removed_zone_ids); var removed_i: usize = 0; errdefer { for (0..removed_i) |i| { allocator.free(std.mem.span(removed_zone_ids[i])); } } for (event.zones_removed) |id| { removed_zone_ids[removed_i] = (try allocator.dupeZ(u8, id)).ptr; removed_i += 1; } const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); internal.* = .{ .added_zones = added_zones, .changed_zones = changed_zones, .removed_zone_ids = removed_zone_ids, }; const self = try allocator.create(ZoneListEvent); errdefer allocator.destroy(self); self.* = .{ .internal = internal, .added_zones_ptr = added_zones.ptr, .added_zones_len = added_zones.len, .changed_zones_ptr = changed_zones.ptr, .changed_zones_len = changed_zones.len, .removed_zone_ids_ptr = removed_zone_ids.ptr, .removed_zone_ids_len = removed_zone_ids.len, }; return self; } pub fn makeFromInitial( event: *const TransportService.SubscribeZoneChanges.InitialResponse, ) std.mem.Allocator.Error!*ZoneListEvent { const added_zones = try allocator.alloc(*Zone, event.zones.len); errdefer allocator.free(added_zones); var added_i: usize = 0; errdefer { for (0..added_i) |i| { added_zones[i].release(); } } for (event.zones) |src| { added_zones[added_i] = try Zone.make(&src); added_i += 1; } const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); internal.* = .{ .added_zones = added_zones, .changed_zones = &.{}, .removed_zone_ids = &.{}, }; const self = try allocator.create(ZoneListEvent); errdefer allocator.destroy(self); self.* = .{ .internal = internal, .added_zones_ptr = added_zones.ptr, .added_zones_len = added_zones.len, .changed_zones_ptr = undefined, .changed_zones_len = 0, .removed_zone_ids_ptr = undefined, .removed_zone_ids_len = 0, }; return self; } pub fn retain(ptr: ?*ZoneListEvent) callconv(.C) *ZoneListEvent { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.ref_count += 1; return self; } pub fn release(ptr: ?*ZoneListEvent) callconv(.C) void { var self = ptr orelse @panic( std.fmt.comptimePrint("Received null pointer on {s}_{s}", .{ cname, @src().fn_name }), ); self.internal.ref_count -= 1; if (self.internal.ref_count == 0) { std.log.debug("Releasing {*}...", .{self}); for (self.internal.added_zones) |zone| { zone.release(); } allocator.free(self.internal.added_zones); for (self.internal.changed_zones) |zone| { zone.release(); } allocator.free(self.internal.changed_zones); for (self.internal.removed_zone_ids) |id| { allocator.free(std.mem.span(id)); } allocator.free(self.internal.removed_zone_ids); allocator.destroy(self.internal); allocator.destroy(self); } else if (self.internal.ref_count < 0) { std.log.warn("Over deref detected {*}, count={d}", .{ self, self.internal.ref_count }); } } pub fn export_capi() void { @export(&retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{cname}) }); @export(&release, .{ .name = std.fmt.comptimePrint("{s}_release", .{cname}) }); } }; pub fn export_capi() void { Zone.export_capi(); ZoneListEvent.export_capi(); }
-