Changes
11 changed files (+1613/-1)
-
-
@@ -23,6 +23,7 @@const std = @import("std"); const CompileVala = @import("./build/CompileVala.zig"); const LibRoon = @import("./build/LibRoon.zig"); const BuildError = error{ NonValaSourceInSourceListError,
-
@@ -64,6 +65,18 @@break :libsood lib; }; const libroon = LibRoon.init(b, .{ .target = target, .optimize = optimize, }); // `zig build emit-libroon` { const step = b.step("emit-libroon", "Emit libroon library files."); step.dependOn(libroon.step); step.dependOn(libroon.install_pretty_headers); } const zig_lib = zig_lib: { const mod = b.createModule(.{ .root_source_file = b.path("src/core/main.zig"),
-
@@ -130,12 +143,13 @@ "libsoup-3.0",}); compile.addPackage("posix"); compile.addPackages(&.{ "plac", "libsood" }); compile.addPackages(&.{ "plac", "libsood", "roon" }); compile.addGResourceXML(b.path("data/gresource.xml")); compile.addVapi(b.path("src/core/plac.vapi")); compile.addVapi(b.path("src/libsood.vapi")); compile.addVapi(libroon.lib.getEmittedIncludeTree().path(b, "roon.vapi")); const step = b.step("valac", "Compile C source code from Vala files for debugging purpose"); step.dependOn(compile.step);
-
@@ -197,6 +211,7 @@ }exe.root_module.addObject(zig_lib); exe.root_module.linkLibrary(libsood); exe.root_module.linkLibrary(libroon.lib); exe.addCSourceFile(.{ .file = gresouce_c });
-
-
build/LibRoon.zig (new)
-
@@ -0,0 +1,113 @@// 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 RoonMsg = @This(); const std = @import("std"); step: *std.Build.Step, lib: *std.Build.Step.Compile, /// Install formatted header and vapi files. /// For debugging. install_pretty_headers: *std.Build.Step, pub const Options = struct { target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, }; pub fn init(b: *std.Build, opts: Options) RoonMsg { const lib = b.addLibrary(.{ .name = "libroon", .root_module = b.createModule(.{ .root_source_file = b.path("src/Roon/lib.zig"), .link_libc = true, .target = opts.target, .optimize = opts.optimize, }), }); const spec_builder = b.addExecutable(.{ .name = "build_api_spec", .root_module = b.createModule(.{ .root_source_file = b.path("src/Roon/build_api_spec.zig"), .target = b.graph.host, .link_libc = true, }), }); const build_spec = b.addRunArtifact(spec_builder); const spec_out = build_spec.addOutputFileArg("spec.json"); const header_builder = b.addExecutable(.{ .name = "build_header", .root_module = b.createModule(.{ .root_source_file = b.path("src/Roon/build_c_header.zig"), .target = b.graph.host, }), }); const build_header = b.addRunArtifact(header_builder); build_header.addFileArg(spec_out); const header_out = build_header.addOutputFileArg("roon.h"); const vapi_builder = b.addExecutable(.{ .name = "build_vapi", .root_module = b.createModule(.{ .root_source_file = b.path("src/Roon/build_vapi.zig"), .target = b.graph.host, }), }); const build_vapi = b.addRunArtifact(vapi_builder); build_vapi.addFileArg(spec_out); const vapi_out = build_vapi.addOutputFileArg("roon.vapi"); lib.installHeader(header_out, "roon.h"); lib.installHeader(vapi_out, "roon.vapi"); const uncrustify_vala = b.addSystemCommand(&.{"uncrustify"}); uncrustify_vala.addArg("-c"); uncrustify_vala.addFileArg(b.path("uncrustify.cfg")); uncrustify_vala.addArgs(&.{ "-l", "VALA" }); uncrustify_vala.setStdIn(.{ .lazy_path = vapi_out }); // Uncrustify outputs to stderr anyways _ = uncrustify_vala.captureStdErr(); const pretty_vapi = uncrustify_vala.captureStdOut(); const uncrustify_c = b.addSystemCommand(&.{"uncrustify"}); uncrustify_c.addArg("-c"); uncrustify_c.addFileArg(b.path("uncrustify.cfg")); uncrustify_c.addArgs(&.{ "-l", "C" }); uncrustify_c.setStdIn(.{ .lazy_path = header_out }); // Uncrustify outputs to stderr anyways _ = uncrustify_c.captureStdErr(); const pretty_header = uncrustify_c.captureStdOut(); const install_pretty_header = b.addInstallHeaderFile(pretty_header, "roon.pretty.h"); const install_pretty_vapi = b.addInstallHeaderFile(pretty_vapi, "roon.pretty.vapi"); install_pretty_vapi.step.dependOn(&install_pretty_header.step); const install = b.addInstallArtifact(lib, .{}); return .{ .step = &install.step, .lib = lib, .install_pretty_headers = &install_pretty_vapi.step, }; }
-
-
src/Roon/ApiSpec.zig (new)
-
@@ -0,0 +1,127 @@// 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"); items: []const Item, pub const CBaseType = union(enum) { int: u16, uint: u16, char: void, bool: void, void: void, custom: []const u8, pub fn fromZigType(comptime T: type) CBaseType { return switch (@typeInfo(T)) { .int => |int| if (int.signedness == .signed) .{ .int = int.bits, } else .{ .uint = int.bits, }, .bool => .bool, .@"enum" => .{ .custom = T.cname }, .@"struct" => .{ .custom = T.cname }, .void => .void, .pointer => |pointer| switch (@typeInfo(pointer.child)) { .@"struct" => .{ .custom = pointer.child.cname }, else => { @compileError( std.fmt.comptimePrint("Pointer to non-struct type {s} is not exportable.", .{@typeName(T)}), ); }, }, else => { @compileError( std.fmt.comptimePrint("Type {s} is not exportable.", .{@typeName(T)}), ); }, }; } }; pub const CType = union(enum) { value: CBaseType, pointer: struct { type: CBaseType, @"const": bool, }, pub fn fromZigType(comptime T: type) CType { return switch (@typeInfo(T)) { .pointer => |pointer| { if (pointer.child == u8 and pointer.is_const and pointer.size == .many and std.builtin.Type.Pointer.sentinel(pointer) == 0) { return .{ .pointer = .{ .type = .char, .@"const" = true, }, }; } return .{ .pointer = .{ .type = CBaseType.fromZigType(pointer.child), .@"const" = pointer.is_const, } }; }, .optional => |optional| fromZigType(optional.child), else => .{ .value = CBaseType.fromZigType(T) }, }; } }; pub const EnumField = struct { name: []const u8, value: c_int, }; pub const FunctionParameter = struct { type: CType, }; pub const Function = struct { name: []const u8, parameters: []const FunctionParameter, return_type: CType, }; pub const StructField = struct { name: []const u8, type: CType, }; pub const Item = union(enum) { @"enum": struct { cname: []const u8, fields: []const EnumField, }, @"struct": struct { cname: []const u8, new_function: ?Function = null, member_functions: []const Function, fields: []const StructField, }, @"opaque": struct { cname: []const u8, new_function: Function, member_functions: []const Function, }, };
-
-
src/Roon/api.zig (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 const browse = @import("./services/browse.zig"); pub const public_api: []const type = &.{ browse.Hierarchy, browse.ItemHint, browse.ItemInputPrompt, browse.Item, browse.ListHint, browse.List, browse.BrowseAction, browse.browse.Request, browse.browse.Response, browse.load.Request, browse.load.Response, };
-
-
-
@@ -0,0 +1,180 @@// 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 ApiSpec = @import("./ApiSpec.zig"); const public_api = @import("./api.zig").public_api; fn CSetterValue(comptime T: type) type { return switch (@typeInfo(T)) { .pointer => |pointer| switch (pointer.size) { .slice => if (pointer.is_const) [*:0]const pointer.child else [*:0]pointer.child, else => @compileError("Non-slice pointer is not supported."), }, .optional => |optional| CSetterValue(optional.child), else => T, }; } pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var args = std.process.ArgIterator.init(); defer args.deinit(); _ = args.skip(); const output = args.next() orelse { return error.NotEnoughArgument; }; if (args.next()) |_| { return error.TooManyArgument; } const items = try allocator.alloc(ApiSpec.Item, public_api.len); errdefer allocator.free(items); inline for (public_api, 0..) |Api, i| { switch (@typeInfo(Api)) { .@"enum" => |info| { const fields = try allocator.alloc(ApiSpec.EnumField, info.fields.len); inline for (info.fields, 0..) |field, j| { fields[j] = .{ .name = try std.ascii.allocUpperString(allocator, field.name), .value = field.value, }; } items[i] = .{ .@"enum" = .{ .cname = Api.cname, .fields = fields, } }; }, .@"struct" => |_| { const self_pointer: ApiSpec.CType = .{ .pointer = .{ .@"const" = false, .type = .{ .custom = Api.Data.cname }, }, }; const ref: ApiSpec.Function = .{ .name = "retain", .parameters = &.{.{ .type = self_pointer }}, .return_type = self_pointer, }; const unref: ApiSpec.Function = .{ .name = "release", .parameters = &.{.{ .type = self_pointer }}, .return_type = .{ .value = .void }, }; const info = @typeInfo(Api.Data).@"struct"; if (info.layout == .@"extern") { // Plain struct or Deserializable const fields = try allocator.alloc(ApiSpec.StructField, info.fields.len); inline for (info.fields, 0..) |field, j| { fields[j] = .{ .name = field.name, .type = .fromZigType(field.type), }; } const from_json: ApiSpec.Function = .{ .name = "from_json", .parameters = &.{ .{ .type = .{ .pointer = .{ .@"const" = true, .type = .char, }, } }, }, .return_type = self_pointer, }; items[i] = .{ .@"struct" = .{ .cname = Api.Data.cname, .fields = fields, .new_function = if (@hasDecl(Api.Data, "deserializable") and Api.Data.deserializable) from_json else null, .member_functions = &.{ ref, unref }, }, }; continue; } // Serializable const init = @typeInfo(@TypeOf(Api.Data.init)).@"fn"; const init_params = try allocator.alloc(ApiSpec.FunctionParameter, init.params.len); inline for (init.params, 0..) |param, j| { init_params[j] = .{ .type = .fromZigType(param.type.?), }; } const methods = try allocator.alloc(ApiSpec.Function, info.fields.len + 3); methods[info.fields.len + 0] = ref; methods[info.fields.len + 1] = unref; methods[info.fields.len + 2] = .{ .name = "to_json", .parameters = &.{.{ .type = self_pointer }}, .return_type = .fromZigType([*:0]const u8), }; inline for (info.fields, 0..) |field, j| { methods[j] = .{ .name = std.fmt.comptimePrint("set_{s}", .{field.name}), .parameters = &.{ .{ .type = self_pointer }, .{ .type = .fromZigType(CSetterValue(field.type)) }, }, .return_type = .{ .value = .void }, }; } items[i] = .{ .@"opaque" = .{ .cname = Api.Data.cname, .new_function = .{ .name = "new", .parameters = init_params, .return_type = self_pointer, }, .member_functions = methods, }, }; }, else => { @compileError(std.fmt.comptimePrint("Cannot export {s}", .{@typeName(Api)})); }, } } var output_file = try std.fs.cwd().createFile(output, .{}); defer output_file.close(); const writer = output_file.writer(); try std.json.stringify(ApiSpec{ .items = items }, .{}, writer); return std.process.cleanExit(); }
-
-
-
@@ -0,0 +1,167 @@// 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 ApiSpec = @import("./ApiSpec.zig"); fn writeBaseType(writer: anytype, t: ApiSpec.CBaseType) !void { switch (t) { .bool, .char, .void => try writer.writeAll(@tagName(t)), .custom => |name| try writer.writeAll(name), .int, .uint => |bits| { try std.fmt.format(writer, "{s}{d}_t", .{ if (t == .int) "int" else "uint", bits, }); }, } } fn writeType(writer: anytype, t: ApiSpec.CType) !void { switch (t) { .pointer => |pointer| { if (pointer.@"const") { try writer.writeAll("const "); } try writeBaseType(writer, pointer.type); try writer.writeAll(" *"); }, .value => |child| try writeBaseType(writer, child), } } fn writeFunction(writer: anytype, cname: []const u8, f: ApiSpec.Function) !void { try writeType(writer, f.return_type); try std.fmt.format(writer, " {s}_{s}(", .{ cname, f.name }); for (f.parameters, 0..) |param, i| { if (i > 0) { try writer.writeAll(", "); } try writeType(writer, param.type); } try writer.writeAll(");\n"); } pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var args = std.process.ArgIterator.init(); defer args.deinit(); _ = args.skip(); const input = args.next() orelse { return error.NoInputFile; }; const output = args.next() orelse { return error.NoOutputFile; }; if (args.next()) |_| { return error.TooManyArgument; } const input_file = try std.fs.cwd().openFile(input, .{}); defer input_file.close(); var source = std.json.reader(allocator, input_file.reader()); const parsed = try std.json.parseFromTokenSource(ApiSpec, allocator, &source, .{}); defer parsed.deinit(); const spec = parsed.value; const output_file = try std.fs.cwd().createFile(output, .{}); defer output_file.close(); const writer = output_file.writer(); try writer.writeAll( \\#ifndef ROON_MSG_H \\#define ROON_MSG_H \\ \\#include <stdbool.h> \\#include <stddef.h> \\#include <stdint.h> \\ \\ ); for (spec.items) |item| { switch (item) { .@"enum" => |info| { try writer.writeAll("typedef enum {\n"); const ns = try std.ascii.allocUpperString(allocator, info.cname); for (info.fields) |field| { try std.fmt.format(writer, "\t{s}_{s} = {d},\n", .{ ns, try std.ascii.allocUpperString(allocator, field.name), field.value, }); } try std.fmt.format(writer, "}} {s};\n\n", .{info.cname}); }, .@"opaque" => |info| { try std.fmt.format(writer, "typedef void {s};\n", .{info.cname}); try writeFunction(writer, info.cname, info.new_function); for (info.member_functions) |member_fn| { try writeFunction(writer, info.cname, member_fn); } try writer.writeByte('\n'); }, .@"struct" => |info| { try writer.writeAll("typedef struct {\n"); // Zig has compiler bug that prevents using tab inside multiline string. try writer.writeAll("\tsize_t __rc;\n"); for (info.fields) |field| { try writer.writeByte('\t'); try writeType(writer, field.type); if (field.type == .pointer and std.mem.endsWith(u8, field.name, "_ptr")) { try writer.writeByte('*'); } try writer.writeByte(' '); try std.fmt.format(writer, "{s};\n", .{field.name}); } try std.fmt.format(writer, "}} {s};\n", .{info.cname}); if (info.new_function) |f| { try writeFunction(writer, info.cname, f); } for (info.member_functions) |f| { try writeFunction(writer, info.cname, f); } try writer.writeByte('\n'); }, } } try writer.writeAll("#endif\n"); }
-
-
src/Roon/build_vapi.zig (new)
-
@@ -0,0 +1,297 @@// 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 ApiSpec = @import("./ApiSpec.zig"); fn writeBaseType(writer: anytype, t: ApiSpec.CBaseType) !void { switch (t) { .bool, .char, .void => try writer.writeAll(@tagName(t)), .custom => |name| try writeCNameAsValaName(writer, name), .int => |bits| try std.fmt.format(writer, "int{d}", .{bits}), .uint => |bits| try std.fmt.format(writer, "uint{d}", .{bits}), } } fn writeType(writer: anytype, t: ApiSpec.CType) !void { switch (t) { .pointer => |pointer| { if (pointer.@"const" and pointer.type == .char) { try writer.writeAll("string"); return; } try writeBaseType(writer, pointer.type); }, .value => |child| try writeBaseType(writer, child), } } fn cnameToNamespaces(allocator: std.mem.Allocator, cname: []const u8) ![]const []const u8 { var ns = std.ArrayList([]const u8).init(allocator); defer ns.deinit(); var iter = std.mem.splitScalar(u8, cname, '_'); while (iter.next()) |token| { const buf = try allocator.dupe(u8, token); if (buf.len == 0) { continue; } buf[0] = std.ascii.toUpper(buf[0]); try ns.append(buf); } return ns.toOwnedSlice(); } fn writeCNameAsValaName(writer: anytype, cname: []const u8) !void { var iter = std.mem.splitScalar(u8, cname, '_'); while (iter.next()) |token| { if (token.len == 0) { continue; } try writer.writeByte(std.ascii.toUpper(token[0])); try writer.writeAll(token[1..]); if (iter.peek()) |_| { try writer.writeByte('.'); } } } const CompactClassOptions = struct { name: []const u8, cname: []const u8, new_function: ?ApiSpec.Function = null, member_functions: []const ApiSpec.Function = &.{}, fields: []const ApiSpec.StructField = &.{}, }; fn writeCompactClass(writer: anytype, opts: CompactClassOptions) !void { try writer.writeAll("[CCode (\n"); try std.fmt.format(writer, "\tcname = \"{s}\",\n", .{opts.cname}); try std.fmt.format(writer, "\tref_function = \"{s}_retain\",\n", .{opts.cname}); try std.fmt.format(writer, "\tunref_function = \"{s}_release\"\n", .{opts.cname}); try writer.writeAll(")]\n[Compact]\n"); try std.fmt.format(writer, "public class {s} {{\n", .{opts.name}); if (opts.new_function) |f| { try std.fmt.format(writer, "[CCode (cname = \"{s}_{s}\")]\n", .{ opts.cname, f.name }); try std.fmt.format(writer, "public {s}", .{opts.name}); if (!std.mem.eql(u8, f.name, "new")) { try std.fmt.format(writer, ".{s}", .{f.name}); } try writer.writeByte('('); for (f.parameters, 0..) |param, i| { if (i > 0) { try writer.writeAll(", "); } try writeType(writer, param.type); try std.fmt.format(writer, " arg{d}", .{i}); } try writer.writeAll(");\n"); } for (opts.fields) |field| { if (std.mem.endsWith(u8, field.name, "_len")) { continue; } if (std.mem.endsWith(u8, field.name, "_ptr") and field.type == .pointer and field.type.pointer.type == .custom) { try std.fmt.format( writer, "[CCode (\n\tcname = \"{s}\",\n\tarray_length_cname = \"{s}_len\", array_length_type = \"size_t\"\n)]\n", .{ field.name, field.name[0 .. field.name.len - 4] }, ); try writer.writeAll("\tpublic "); try writeType(writer, field.type); try std.fmt.format(writer, "[] {s};\n", .{ field.name[0 .. field.name.len - 4], }); continue; } try writer.writeAll("\tpublic "); try writeType(writer, field.type); try std.fmt.format(writer, " {s};\n", .{field.name}); } for (opts.member_functions) |f| { if (std.mem.startsWith(u8, f.name, "set_") and f.return_type == .value and f.return_type.value == .void) { if (f.parameters.len != 2) { std.log.err("Setter {s} on {s} does not have 2 parameters.", .{ f.name, opts.cname }); return error.SetterNotHavingTwoParameters; } const value = f.parameters[1]; try writer.writeAll("public "); try writeType(writer, value.type); try std.fmt.format(writer, " {s} {{\n\t[CCode (cname = \"{s}_{s}\")] set;\n}}\n", .{ f.name[4..], opts.cname, f.name, }); continue; } try std.fmt.format(writer, "[CCode (cname = \"{s}_{s}\")]\n", .{ opts.cname, f.name, }); try writer.writeAll("public "); try writeType(writer, f.return_type); try std.fmt.format(writer, " {s}(", .{f.name}); for (f.parameters[1..], 0..) |param, i| { if (i > 0) { try writer.writeAll(", "); } try writeType(writer, param.type); try std.fmt.format(writer, " arg{d}", .{i}); } try writer.writeAll(");\n"); } try writer.writeAll("}\n"); } pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var args = std.process.ArgIterator.init(); defer args.deinit(); _ = args.skip(); const input = args.next() orelse { return error.NoInputFile; }; const output = args.next() orelse { return error.NoOutputFile; }; if (args.next()) |_| { return error.TooManyArgument; } const input_file = try std.fs.cwd().openFile(input, .{}); defer input_file.close(); var source = std.json.reader(allocator, input_file.reader()); const parsed = try std.json.parseFromTokenSource(ApiSpec, allocator, &source, .{}); defer parsed.deinit(); const spec = parsed.value; const output_file = try std.fs.cwd().createFile(output, .{}); defer output_file.close(); const writer = output_file.writer(); for (spec.items) |item| { switch (item) { .@"enum" => |info| { const components = try cnameToNamespaces(allocator, info.cname); const ns = components[0 .. components.len - 1]; const name = components[components.len - 1]; try writer.writeAll("[CCode (cheader_filename = \"roon.h\")]\n"); for (ns) |token| { try std.fmt.format(writer, "namespace {s} {{\n", .{token}); } try writer.writeAll("[CCode (\n"); try std.fmt.format(writer, "\tcname = \"{s}\",\n", .{info.cname}); try std.fmt.format(writer, "\tcprefix = \"{s}_\",\n", .{ try std.ascii.allocUpperString(allocator, info.cname), }); try writer.writeAll("\thas_type_id = false\n)]\n"); try std.fmt.format(writer, "public enum {s} {{\n", .{name}); for (info.fields) |field| { try std.fmt.format(writer, "\t{s} = {d},\n", .{ try std.ascii.allocUpperString(allocator, field.name), field.value, }); } try writer.writeAll("}\n"); for (ns) |_| { try writer.writeAll("}\n"); } }, .@"struct" => |info| { const components = try cnameToNamespaces(allocator, info.cname); const ns = components[0 .. components.len - 1]; const name = components[components.len - 1]; try writer.writeAll("[CCode (cheader_filename = \"roon.h\")]\n"); for (ns) |token| { try std.fmt.format(writer, "namespace {s} {{\n", .{token}); } try writeCompactClass(writer, .{ .name = name, .cname = info.cname, .fields = info.fields, .new_function = info.new_function, .member_functions = info.member_functions, }); for (ns) |_| { try writer.writeAll("}\n"); } }, .@"opaque" => |info| { const components = try cnameToNamespaces(allocator, info.cname); const ns = components[0 .. components.len - 1]; const name = components[components.len - 1]; try writer.writeAll("[CCode (cheader_filename = \"roon.h\")]\n"); for (ns) |token| { try std.fmt.format(writer, "namespace {s} {{\n", .{token}); } try writeCompactClass(writer, .{ .name = name, .cname = info.cname, .new_function = info.new_function, .member_functions = info.member_functions, }); for (ns) |_| { try writer.writeAll("}\n"); } }, } try writer.writeByte('\n'); } }
-
-
src/Roon/json.zig (new)
-
@@ -0,0 +1,156 @@// 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"); pub const SerializableOptions = struct { field: []const u8 = "data", json_options: std.json.StringifyOptions = .{ .emit_null_optional_fields = false, }, }; pub fn Serializable(comptime T: type, opts: SerializableOptions) type { const to_json = struct { fn to_json(ptr: *const T) callconv(.C) ?[*:0]const u8 { var buf = std.ArrayList(u8).init(std.heap.c_allocator); defer buf.deinit(); const writer = buf.writer(); std.json.stringify( @field(ptr, opts.field), opts.json_options, writer, ) catch return null; const slice = buf.toOwnedSliceSentinel(0) catch return null; return slice.ptr; } }; comptime { if (@typeInfo(T).@"struct".layout == .@"extern") { @extern(&T, .{}); } @export(&to_json.to_json, .{ .name = std.fmt.comptimePrint("{s}_to_json", .{T.cname}), }); const Container = t: { for (@typeInfo(T).@"struct".fields) |field| { if (std.mem.eql(u8, field.name, opts.field)) { break :t field.type; } } @compileError(std.fmt.comptimePrint("Type \"{s}\" does not have \"{s}\" field", .{ @typeName(T), opts.field, })); }; for (@typeInfo(Container).@"struct".fields) |field| { const UnwrappedValue, const optional = switch (@typeInfo(field.type)) { .optional => |optional| .{ optional.child, true }, else => .{ field.type, false }, }; const CValue = switch (@typeInfo(UnwrappedValue)) { .pointer => |pointer| switch (pointer.size) { .slice => [*:0]pointer.child, else => @compileError("Non-slice pointer is not supported."), }, else => UnwrappedValue, }; const setter = struct { fn set(ptr: *T, value: CValue) callconv(.C) void { switch (@typeInfo(UnwrappedValue)) { .pointer => |pointer| { switch (pointer.size) { .slice => { const copy = std.heap.c_allocator.dupe( pointer.child, std.mem.span(value), ) catch { std.log.debug("Failed to copy for {s} in {s}: out of memory", .{ field.name, T.cname, }); return; }; if (optional) { if (@field(@field(ptr, opts.field), field.name)) |current| { std.heap.c_allocator.free(current); } } @field(@field(ptr, opts.field), field.name) = copy; }, else => { @compileError("Non-slice pointer is not supported."); }, } }, else => { @field(@field(ptr, opts.field), field.name) = value; }, } } }; @export(&setter.set, .{ .name = std.fmt.comptimePrint("{s}_set_{s}", .{ T.cname, field.name }), }); } } return T; } pub const DeserializableOptions = struct { type_field: []const u8 = "Data", json_options: std.json.ParseOptions = .{ .ignore_unknown_fields = true, }, }; pub fn Deserializable(comptime T: type, opts: DeserializableOptions) type { const from_json = struct { fn from_json(ptr: [*:0]const u8) callconv(.C) ?*T { const result = std.json.parseFromSlice( @field(T, opts.type_field).Raw, std.heap.c_allocator, std.mem.span(ptr), opts.json_options, ) catch return null; defer result.deinit(); return T.new(@field(T, opts.type_field).init(result.value) catch return null) catch return null; } }; comptime { @export(&from_json.from_json, .{ .name = std.fmt.comptimePrint("{s}_from_json", .{T.cname}), }); } return T; }
-
-
src/Roon/lib.zig (new)
-
@@ -0,0 +1,21 @@// 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 public_api = @import("./api.zig").public_api; comptime { _ = public_api; }
-
-
src/Roon/rc.zig (new)
-
@@ -0,0 +1,121 @@// 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 //! Atomic Reference Counting based on example code on `std.atomic.Value`. //! https://ziglang.org/documentation/0.14.0/std/#std.atomic.Value const std = @import("std"); const Rc = extern struct { count: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), /// Increment reference count by one. pub fn ref(arc: *Rc) callconv(.C) void { _ = arc.count.fetchAdd(1, .monotonic); } /// Decrement reference count by one, returns `true` if no reference is alive. pub fn unref(arc: *Rc) callconv(.C) bool { if (arc.count.fetchSub(1, .release) == 1) { _ = arc.count.load(.acquire); return true; } else { return false; } } }; pub fn RefCounted(comptime T: type) type { const Wrapped = if (@typeInfo(T).@"struct".layout == .@"extern") extern struct { pub const cname = T.cname; pub const Data = T; rc: Rc = .{}, data: T, pub fn new(data: T) !*@This() { const self = try std.heap.c_allocator.create(@This()); self.* = .{ .data = data, }; self.rc.ref(); return self; } pub fn retain(ptr: *@This()) callconv(.C) *@This() { ptr.rc.ref(); return ptr; } pub fn release(ptr: *@This()) callconv(.C) void { if (ptr.rc.unref()) { if (@hasDecl(T, "deinit")) { T.deinit(&ptr.data); } std.heap.c_allocator.destroy(ptr); } } } else struct { pub const cname = T.cname; pub const Data = T; rc: Rc = .{}, data: T, pub fn new(data: T) !*@This() { const self = try std.heap.c_allocator.create(@This()); self.* = .{ .data = data, }; self.rc.ref(); return self; } pub fn retain(ptr: *@This()) callconv(.C) *@This() { ptr.rc.ref(); return ptr; } pub fn release(ptr: *@This()) callconv(.C) void { if (ptr.rc.unref()) { if (@hasDecl(T, "deinit")) { T.deinit(&ptr.data); } std.heap.c_allocator.destroy(ptr); } } }; comptime { @export(&Wrapped.retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{T.cname}), }); @export(&Wrapped.release, .{ .name = std.fmt.comptimePrint("{s}_release", .{T.cname}), }); } return Wrapped; }
-
-
-
@@ -0,0 +1,384 @@// 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 rc = @import("../rc.zig"); const json = @import("../json.zig"); const allocator = std.heap.c_allocator; pub const Hierarchy = enum(c_int) { pub const cname = "roon_browse_Hierarchy"; browse = 0, playlists = 1, settings = 2, internet_radio = 3, albums = 4, artists = 5, genres = 6, composers = 7, search = 8, }; pub const ItemHint = enum(c_int) { pub const cname = "roon_browse_ItemHint"; unknown = 0, action = 1, action_list = 2, list = 3, header = 4, 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, "unknown")) { continue; } if (std.mem.eql(u8, field.name, v)) { return @enumFromInt(field.value); } } return .unknown; }, else => .unknown, }; } }; pub const ItemInputPrompt = rc.RefCounted(extern struct { pub const cname = "roon_browse_ItemInputPrompt"; prompt: [*:0]const u8, action: [*:0]const u8, value: ?[*:0]const u8, is_password: bool, pub fn init(raw: Raw) !@This() { return .{ .prompt = (try allocator.dupeZ(u8, raw.prompt)).ptr, .action = (try allocator.dupeZ(u8, raw.action)).ptr, .value = if (raw.value) |value| (try allocator.dupeZ(u8, value)).ptr else null, .is_password = raw.is_password, }; } pub const Raw = struct { prompt: [:0]const u8, action: [:0]const u8, value: ?[:0]const u8 = null, is_password: bool = false, }; pub fn deinit(self: *const @This()) void { allocator.free(std.mem.span(self.prompt)); allocator.free(std.mem.span(self.action)); if (self.value) |value| { allocator.free(std.mem.span(value)); } } }); pub const Item = rc.RefCounted(extern struct { pub const cname = "roon_browse_Item"; title: [*:0]const u8, subtitle: ?[*:0]const u8, image_key: ?[*:0]const u8, item_key: ?[*:0]const u8, hint: ItemHint, input_prompt: ?*ItemInputPrompt, pub fn init(raw: Raw) !@This() { return .{ .title = (try allocator.dupeZ(u8, raw.title)).ptr, .subtitle = if (raw.subtitle) |subtitle| (try allocator.dupeZ(u8, subtitle)).ptr else null, .image_key = if (raw.image_key) |image_key| (try allocator.dupeZ(u8, image_key)).ptr else null, .item_key = if (raw.item_key) |item_key| (try allocator.dupeZ(u8, item_key)).ptr else null, .hint = raw.hint, .input_prompt = if (raw.input_prompt) |input_prompt| try ItemInputPrompt.new(try .init(input_prompt)) else null, }; } pub const Raw = struct { title: [:0]const u8, subtitle: ?[:0]const u8 = null, image_key: ?[:0]const u8 = null, item_key: ?[:0]const u8 = null, hint: ItemHint = .unknown, input_prompt: ?ItemInputPrompt.Data.Raw = null, }; pub fn deinit(self: *const @This()) void { allocator.free(std.mem.span(self.title)); if (self.subtitle) |subtitle| { allocator.free(std.mem.span(subtitle)); } if (self.image_key) |image_key| { allocator.free(std.mem.span(image_key)); } if (self.item_key) |item_key| { allocator.free(std.mem.span(item_key)); } if (self.input_prompt) |input_prompt| { input_prompt.release(); } } }); pub const ListHint = enum(c_int) { pub const cname = "roon_browse_ListHint"; unknown = 0, action_list = 1, 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, "unknown")) { continue; } if (std.mem.eql(u8, field.name, v)) { return @enumFromInt(field.value); } } return .unknown; }, else => .unknown, }; } }; pub const List = rc.RefCounted(extern struct { pub const cname = "roon_browse_List"; title: [*:0]const u8, count: usize, subtitle: ?[*:0]const u8, image_key: ?[*:0]const u8, level: usize, display_offset: i64, has_display_offset: bool, hint: ListHint, pub fn init(raw: Raw) !@This() { return .{ .title = (try allocator.dupeZ(u8, raw.title)).ptr, .count = raw.count, .subtitle = if (raw.subtitle) |subtitle| (try allocator.dupeZ(u8, subtitle)).ptr else null, .image_key = if (raw.image_key) |image_key| (try allocator.dupeZ(u8, image_key)).ptr else null, .level = raw.level, .display_offset = raw.display_offset orelse 0, .has_display_offset = raw.display_offset != null, .hint = raw.hint, }; } pub const Raw = struct { title: []const u8, count: usize = 0, subtitle: ?[]const u8 = null, image_key: ?[]const u8 = null, level: usize = 0, display_offset: ?i64 = null, hint: ListHint = .unknown, }; pub fn deinit(self: *const @This()) void { allocator.free(std.mem.span(self.title)); if (self.subtitle) |subtitle| { allocator.free(std.mem.span(subtitle)); } if (self.image_key) |image_key| { allocator.free(std.mem.span(image_key)); } } }); pub const BrowseAction = enum(c_int) { pub const cname = "roon_browse_BrowseAction"; message = 0, none = 1, list = 2, replace_item = 3, remove_item = 4, }; pub const browse = struct { pub const Request = json.Serializable(rc.RefCounted(struct { pub const cname = "roon_browse_browse_Request"; hierarchy: Hierarchy, item_key: ?[]const u8 = null, input: ?[]const u8 = null, zone_or_output_id: ?[]const u8 = null, pop_all: ?bool = null, pop_levels: ?usize = null, refresh_list: ?bool = null, pub fn init(hierarchy: Hierarchy) @This() { return .{ .hierarchy = hierarchy }; } pub fn deinit(self: *const @This()) void { if (self.item_key) |item_key| { allocator.free(item_key); } if (self.input) |input| { allocator.free(input); } if (self.zone_or_output_id) |zone_or_output_id| { allocator.free(zone_or_output_id); } } }), .{}); export fn roon_browse_browse_Request_new(hierarchy: Hierarchy) callconv(.C) ?*Request { return Request.new(.init(hierarchy)) catch return null; } pub const Response = json.Deserializable(rc.RefCounted(extern struct { pub const cname = "roon_browse_browse_Response"; pub const deserializable = true; action: BrowseAction, item: ?*Item, list: ?*List, message: ?[*:0]const u8, is_error: bool, pub fn init(raw: Raw) !@This() { return .{ .action = raw.action, .item = if (raw.item) |item| try Item.new(try .init(item)) else null, .list = if (raw.list) |list| try List.new(try .init(list)) else null, .message = if (raw.message) |message| (try allocator.dupeZ(u8, message)).ptr else null, .is_error = raw.is_error orelse false, }; } pub const Raw = struct { action: BrowseAction, item: ?Item.Data.Raw = null, list: ?List.Data.Raw = null, message: ?[]const u8 = null, is_error: ?bool = null, }; pub fn deinit(self: *const @This()) void { if (self.item) |item| { item.release(); } if (self.list) |list| { list.release(); } if (self.message) |message| { allocator.free(std.mem.span(message)); } } }), .{}); }; pub const load = struct { pub const Request = json.Serializable(rc.RefCounted(struct { pub const cname = "roon_browse_load_Request"; set_display_offset: ?i64 = null, level: ?usize = 0, offset: ?i64 = 0, count: ?usize = 0, hierarchy: Hierarchy, multi_session_key: ?[]const u8 = null, pub fn init(hierarchy: Hierarchy) @This() { return .{ .hierarchy = hierarchy }; } pub fn deinit(self: *const @This()) void { if (self.multi_session_key) |multi_session_key| { allocator.free(multi_session_key); } } }), .{}); export fn roon_browse_load_Request_new(hierarchy: Hierarchy) callconv(.C) ?*Request { return Request.new(.init(hierarchy)) catch return null; } pub const Response = json.Deserializable(rc.RefCounted(extern struct { pub const cname = "roon_browse_load_Response"; pub const deserializable = true; items_ptr: [*]const *Item, items_len: usize, offset: i64, list: *List, pub fn init(raw: Raw) !@This() { const items = try allocator.alloc(*Item, raw.items.len); errdefer allocator.free(items); var i: usize = 0; errdefer { for (0..i) |_| { items[i].release(); } } for (raw.items) |item| { items[i] = try Item.new(try .init(item)); i += 1; } return .{ .items_ptr = items.ptr, .items_len = items.len, .offset = raw.offset, .list = try List.new(try .init(raw.list)), }; } pub fn deinit(_: *const @This()) void {} pub const Raw = struct { items: []const Item.Data.Raw, offset: i64 = 0, list: List.Data.Raw, }; }), .{}); };
-