diff --git a/.gitignore b/.gitignore index 9901a40d..01f65ef9 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,6 @@ build .vscode +# Zig related +.zig-cache +zig-out diff --git a/bindings/zig/examples/zig_api.zig b/bindings/zig/examples/zig_api.zig new file mode 100644 index 00000000..6060a3d1 --- /dev/null +++ b/bindings/zig/examples/zig_api.zig @@ -0,0 +1,108 @@ +const std = @import("std"); +const lm = @import("libremidi"); + + +const EnumeratedPorts = extern struct { + in_ports: [256]?*lm.midi.In.Port = @splat(null), + out_ports: [256]?*lm.midi.Out.Port = @splat(null), + in_port_count: usize = 0, + out_port_count: usize = 0, +}; + +pub fn main() !void { + + std.debug.print("Hello from libremidi Zig(ified) API example!\n", .{}); + std.debug.print("libremidi version: {s}\n\n", .{ lm.getVersion() }); + + var e: EnumeratedPorts = .{}; + + const observer: *lm.Observer = try .init(&.{ + .track_hardware = true, + .track_virtual = true, + .track_any = true, + .input_added = .{ + .context = &e, + .callback = on_input_port_found, + }, + .output_added = .{ + .context = &e, + .callback = on_output_port_found, + }, + }, &.{ + .conf_type = .observer, + .api = .alsa_seq, + }); + defer free_observer(observer, &e); + + try enumerate_ports(observer, &e); + + const midi_in: *lm.midi.In = try .init(&.{ + .version = .midi1, + .port = .{ .input = e.in_ports[0] }, + .msg_callback = .{ .on_midi1_message = .{ .callback = on_midi1_message } }, + }, &.{ + .conf_type = .input, + .api = .alsa_seq, + }); + defer midi_in.free(); + + const midi_out: *lm.midi.Out = try .init(&.{ + .version = .midi1, + .virtual_port = true, + .port_name = "my-app", + }, &.{ + .conf_type = .output, + .api = .alsa_seq, + }); + defer midi_out.free(); + + + for (0..99) |_| std.time.sleep(1e9); // sleep 1s, 100 times +} + +fn free_observer(observer: *lm.Observer, e: *EnumeratedPorts) void { + + for (e.in_ports) |maybe_port| if (maybe_port) |port| port.free(); + for (e.out_ports) |maybe_port| if (maybe_port) |port| port.free(); + + observer.free(); +} + +export fn on_input_port_found(ctx: ?*anyopaque, port: *lm.midi.In.Port) callconv(.C) void { + + std.debug.print("input: {s}\n", .{ port.getName() catch "" }); + + var e: *EnumeratedPorts = @ptrCast(@alignCast(ctx)); + e.in_ports[e.in_port_count] = port.clone() catch null; + e.in_port_count += 1; +} + +export fn on_output_port_found(ctx: ?*anyopaque, port: *lm.midi.Out.Port) callconv(.C) void { + + std.debug.print("output: {s}\n", .{ port.getName() catch "" }); + + var e: *EnumeratedPorts = @ptrCast(@alignCast(ctx)); + e.out_ports[e.out_port_count] = port.clone() catch null; + e.out_port_count += 1; +} + +fn on_midi1_message(ctx: ?*anyopaque, ts: lm.Timestamp, msg: [*]const lm.midi.v1.Symbol, len: usize) callconv(.C) void { + _ = ctx; + _ = ts; + _ = len; + + std.debug.print("0x{x:02} 0x{x:02} 0x{x:02}\n", .{ msg[0], msg[1], msg[2] }); +} + +fn on_midi2_message(ctx: ?*anyopaque, ts: lm.Timestamp, msg: [*]const lm.midi.v2.Symbol, len: usize) callconv(.C) void { + _ = ctx; + _ = ts; + _ = len; + + std.debug.print("0x{x:02} 0x{x:02} 0x{x:02}\n", .{ msg[0], msg[1], msg[2] }); +} + +fn enumerate_ports(observer: *lm.Observer, e: *EnumeratedPorts) !void { + try observer.enumerateInputPorts(e, on_input_port_found); + try observer.enumerateOutputPorts(e, on_output_port_found); +} diff --git a/bindings/zig/libremidi.zig b/bindings/zig/libremidi.zig new file mode 100644 index 00000000..81bce76d --- /dev/null +++ b/bindings/zig/libremidi.zig @@ -0,0 +1,435 @@ +const std = @import("std"); +const c = @import("libremidi-c"); + +const E = std.c.E; +fn errnoFromInt(rc: anytype) E { + return @enumFromInt(-rc); +} + + +extern fn libremidi_get_version() [*:0]const u8; +pub const getVersion = libremidi_get_version; + +pub const Api = enum(c.libremidi_api) { + unspecified = c.UNSPECIFIED, + + coremidi = c.COREMIDI, + alsa_seq = c.ALSA_SEQ, + alsa_raw = c.ALSA_RAW, + jack_midi = c.JACK_MIDI, + windows_mm = c.WINDOWS_MM, + windows_uwp = c.WINDOWS_UWP, + webmidi = c.WEBMIDI, + pipewire = c.PIPEWIRE, + keyboard = c.KEYBOARD, + network = c.NETWORK, + + alsa_raw_ump = c.ALSA_RAW_UMP, + alsa_seq_ump = c.ALSA_SEQ_UMP, + coremidi_ump = c.COREMIDI_UMP, + windows_midi_services = c.WINDOWS_MIDI_SERVICES, + keyboard_ump = c.KEYBOARD_UMP, + network_ump = c.NETWORK_UMP, + jack_ump = c.JACK_UMP, + pipewire_ump = c.PIPEWIRE_UMP, + + dummy = c.DUMMY, + + + extern fn libremidi_api_identifier(self: Api) [*:0]const u8; + pub const getId = libremidi_api_identifier; + + extern fn libremidi_api_display_name(self: Api) [*:0]const u8; + pub const getDisplayName = libremidi_api_display_name; + + extern fn libremidi_get_compiled_api_by_identifier(name: [*:0]const u8) Api; + pub const getById = libremidi_get_compiled_api_by_identifier; + + pub const Config = extern struct { + + api: Api = .unspecified, + + conf_type: enum(@FieldType(c.libremidi_api_configuration, "configuration_type")) { + observer = c.Observer, + input = c.Input, + output = c.Output, + } = .observer, + + data: ?*anyopaque = null, + }; + + fn Callback(comptime P: type) type { + return (?*const fn(ctx: P, api: Api) callconv(.C) void); + } +}; + +pub const Timestamp = extern struct { + inner: c.libremidi_timestamp = 0, + + + pub const Mode = enum(c.enum_libremidi_timestamp_mode) { + no_timestamp = c.NoTimestamp, + + relative = c.Relative, + absolute = c.Absolute, + system_monotonic = c.SystemMonotonic, + audio_frame = c.AudioFrame, + custom = c.Custom, + }; + + fn Callback(comptime P: type) type { + return extern struct { + context: P = null, + callback: (?*const fn(ctx: P, ts: Timestamp) callconv(.C) Timestamp) = null, + }; + } +}; + +pub const Observer = opaque { + const Ctx = ?*anyopaque; + + extern fn libremidi_midi_observer_new(conf: ?*const Config, api: ?*const Api.Config, out: *?*Observer) c_int; + pub fn init(conf: ?*const Config, api: ?*const Api.Config) !*Observer { + + var handle: ?*Observer = undefined; + switch (errnoFromInt(libremidi_midi_observer_new(conf, api, &handle))) { + .SUCCESS => return handle.?, + .INVAL => return error.InvalidArgument, + .IO => return error.InputOutput, + else => unreachable, + } + } + + extern fn libremidi_midi_observer_enumerate_input_ports(self: *Observer, context: Ctx, cb: midi.In.Port.Callback(Ctx)) c_int; + pub fn enumerateInputPorts(self: *Observer, context: Ctx, cb: midi.In.Port.Callback(Ctx)) !void { + switch (errnoFromInt(libremidi_midi_observer_enumerate_input_ports(self, context, cb))) { + .SUCCESS => return, + .INVAL => return error.InvalidArgument, + else => unreachable, + } + } + + extern fn libremidi_midi_observer_enumerate_output_ports(self: *Observer, context: Ctx, cb: midi.Out.Port.Callback(Ctx)) c_int; + pub fn enumerateOutputPorts(self: *Observer, context: Ctx, cb: midi.Out.Port.Callback(Ctx)) !void { + switch (errnoFromInt(libremidi_midi_observer_enumerate_output_ports(self, context, cb))) { + .SUCCESS => return, + .INVAL => return error.InvalidArgument, + else => unreachable, + } + } + + extern fn libremidi_midi_observer_free(self: *Observer) c_int; + pub fn free(self: *Observer) void { + switch (libremidi_midi_observer_free(self)) { + 0 => return, + else => unreachable, + } + } + + pub const Config = extern struct { + + on_error: ErrCallback(Ctx) = .{ .context = null, .callback = null }, + on_warning: ErrCallback(Ctx) = .{ .context = null, .callback = null }, + input_added: InputCallback(Ctx) = .{ .context = null, .callback = null }, + input_removed: InputCallback(Ctx) = .{ .context = null, .callback = null }, + output_added: OutputCallback(Ctx) = .{ .context = null, .callback = null }, + output_removed: OutputCallback(Ctx) = .{ .context = null, .callback = null }, + track_hardware: bool = false, + track_virtual: bool = false, + track_any: bool = false, + notify_in_constructor: bool = false, + + + fn ErrCallback(comptime P: type) type { + return extern struct { + const Loc = ?*const anyopaque; + + context: P = null, + callback: (?*const fn(ctx: P, err: [*:0]const u8, err_len: usize, source_location: Loc) callconv(.C) void) = null, + }; + } + + fn InputCallback(comptime P: type) type { + return extern struct { + context: P = null, + callback: midi.In.Port.Callback(P) = null, + }; + } + + fn OutputCallback(comptime P: type) type { + return extern struct { + context: P = null, + callback: midi.Out.Port.Callback(P) = null, + }; + } + }; +}; + +pub const midi = struct { + + pub const Config = extern struct { + const Ctx = ?*anyopaque; + + version: enum(@FieldType(c.libremidi_midi_configuration, "version")) { + none = 0, + + midi1 = c.MIDI1, midi1_raw = c.MIDI1_RAW, + midi2 = c.MIDI2, midi2_raw = c.MIDI2_RAW, + } = .none, + + port: extern union { + input: ?*midi.In.Port, + output: ?*midi.Out.Port, + } = .{ .input = null }, + + msg_callback: extern union { + on_midi1_message: v1.Callback(Ctx), + on_midi1_raw_data: v1.Callback(Ctx), + on_midi2_message: v2.Callback(Ctx), + on_midi2_raw_data: v2.Callback(Ctx), + } = .{ .on_midi1_message = .{ .context = null, .callback = null } }, + + ts_callback: Timestamp.Callback(Ctx) = .{ .context = null, .callback = null }, + on_error: ErrCallback(Ctx) = .{ .context = null, .callback = null }, + on_warning: ErrCallback(Ctx) = .{ .context = null, .callback = null }, + port_name: ?[*:0]const u8 = null, + virtual_port: bool = false, + ignore_sysex: bool = false, + ignore_timing: bool = false, + ignore_sensing: bool = false, + timestamps: Timestamp.Mode = .no_timestamp, + + + fn ErrCallback(comptime P: type) type { + return extern struct { + const Loc = ?*const anyopaque; + + context: P = null, + callback: (?*const fn(ctx: P, err: [*:0]const u8, err_len: usize, source_location: Loc) callconv(.C) void) = null, + }; + } + }; + + pub const In = opaque { + + extern fn libremidi_midi_in_new(conf: ?*const Config, api: ?*const Api.Config, out: *?*In) c_int; + pub fn init(conf: ?*const Config, api: ?*const Api.Config) !*In { + + var handle: ?*In = undefined; + switch (errnoFromInt(libremidi_midi_in_new(conf, api, &handle))) { + .SUCCESS => return handle.?, + .INVAL => return error.InvalidArgument, + .IO => return error.InputOutput, + else => unreachable, + } + } + + extern fn libremidi_midi_in_is_connected(self: *In) c_int; + pub fn isConnected(self: *In) !bool { + switch (libremidi_midi_in_is_connected(self)) { + 0 => return false, + 1 => return true, + -@intFromEnum(E.INVAL) => return error.InvalidArgument, + else => unreachable, + } + } + + extern fn libremidi_midi_in_absolute_timestamp(self: *In) Timestamp; + pub fn getAbsoluteTimestamp(self: *In) !Timestamp { + switch (libremidi_midi_in_absolute_timestamp(self)) { + -@intFromEnum(E.INVAL) => return error.InvalidArgument, + else => |ts| return ts, + } + } + + extern fn libremidi_midi_in_free(self: *In) c_int; + pub fn free(self: *In) void { + switch (libremidi_midi_in_free(self)) { + 0 => return, + else => unreachable, + } + } + + + pub const Port = opaque { + + extern fn libremidi_midi_in_port_clone(self: *Port, dest: *?*Port) c_int; + pub fn clone(self: *Port) !*Port { + + var handle: ?*Port = undefined; + switch (errnoFromInt(libremidi_midi_in_port_clone(self, &handle))) { + .SUCCESS => return handle.?, + .INVAL => return error.InvalidArgument, + else => unreachable, + } + } + + extern fn libremidi_midi_in_port_free(self: *Port) c_int; + pub fn free(self: *Port) void { + switch (libremidi_midi_in_port_free(self)) { + 0 => return, + else => unreachable, + } + } + + extern fn libremidi_midi_in_port_name(self: *Port, name: *[*:0]const u8, len: *usize) c_int; + pub fn getName(self: *Port) ![:0]const u8 { + + var name: [:0]const u8 = undefined; + switch (errnoFromInt(libremidi_midi_in_port_name(self, &name.ptr, &name.len))) { + .SUCCESS => return name, + .INVAL => return error.InvalidArgument, + else => unreachable, + } + } + + fn Callback(comptime P: type) type { + return (?*const fn(ctx: P, port: *Port) callconv(.C) void); + } + }; + }; + + pub const Out = opaque { + + extern fn libremidi_midi_out_new(conf: ?*const Config, api: ?*const Api.Config, out: *?*Out) c_int; + pub fn init(conf: ?*const Config, api: ?*const Api.Config) !*Out { + + var handle: ?*Out = undefined; + switch (errnoFromInt(libremidi_midi_out_new(conf, api, &handle))) { + .SUCCESS => return handle.?, + .INVAL => return error.InvalidArgument, + .IO => return error.InputOutput, + else => unreachable, + } + } + + extern fn libremidi_midi_out_is_connected(self: *Out) c_int; + pub fn isConnected(self: *Out) !bool { + switch (libremidi_midi_out_is_connected(self)) { + 0 => return false, + 1 => return true, + -@intFromEnum(E.INVAL) => return error.InvalidArgument, + else => unreachable, + } + } + + extern fn libremidi_midi_out_send_message(self: *Out, msg: [*]const v1.Symbol, len: usize) c_int; + pub fn sendMsg(self: *Out, msg: []const v1.Symbol) !void { + switch (errnoFromInt(libremidi_midi_out_send_message(self, msg.ptr, msg.len))) { + .SUCCESS => return, + .INVAL => return error.InvalidArgument, + .IO => return error.InputOutput, + else => unreachable, + } + } + + extern fn libremidi_midi_out_send_ump(self: *Out, msg: [*]const v2.Symbol, len: usize) c_int; + pub fn sendUmp(self: *Out, msg: []const v2.Symbol) !void { + switch (errnoFromInt(libremidi_midi_out_send_ump(self, msg.ptr, msg.len))) { + .SUCCESS => return, + .INVAL => return error.InvalidArgument, + .IO => return error.InputOutput, + else => unreachable, + } + } + + extern fn libremidi_midi_out_schedule_message(self: *Out, ts: Timestamp, msg: [*]const v1.Symbol, len: usize) c_int; + pub fn scheduleMsg(self: *Out, ts: Timestamp, msg: []const v1.Symbol) !void { + switch (errnoFromInt(libremidi_midi_out_schedule_message(self, ts, msg.ptr, msg.len))) { + .SUCCESS => return, + .INVAL => return error.InvalidArgument, + .IO => return error.InputOutput, + else => unreachable, + } + } + + extern fn libremidi_midi_out_schedule_ump(self: *Out, ts: Timestamp, msg: [*]const v2.Symbol, len: usize) c_int; + pub fn scheduleUmp(self: *Out, ts: Timestamp, msg: []const v2.Symbol) !void { + switch (errnoFromInt(libremidi_midi_out_schedule_ump(self, ts, msg.ptr, msg.len))) { + .SUCCESS => return, + .INVAL => return error.InvalidArgument, + .IO => return error.InputOutput, + else => unreachable, + } + } + + extern fn libremidi_midi_out_free(self: *Out) c_int; + pub fn free(self: *Out) void { + switch (libremidi_midi_out_free(self)) { + 0 => return, + else => unreachable, + } + } + + pub const Port = opaque { + + extern fn libremidi_midi_out_port_clone(self: *Port, dest: *?*Port) c_int; + pub fn clone(self: *Port) !*Port { + + var handle: ?*Port = undefined; + switch (errnoFromInt(libremidi_midi_out_port_clone(self, &handle))) { + .SUCCESS => return handle.?, + .INVAL => return error.InvalidArgument, + else => unreachable, + } + } + + extern fn libremidi_midi_out_port_free(self: *Port) c_int; + pub fn free(self: *Port) void { + switch (libremidi_midi_out_port_free(self)) { + 0 => return, + else => unreachable, + } + } + + extern fn libremidi_midi_out_port_name(self: *Port, name: *[*:0]const u8, len: *usize) c_int; + pub fn getName(self: *Port) ![:0]const u8 { + + var name: [:0]const u8 = undefined; + switch (errnoFromInt(libremidi_midi_out_port_name(self, &name.ptr, &name.len))) { + .SUCCESS => return name, + .INVAL => return error.InvalidArgument, + else => unreachable, + } + } + + fn Callback(comptime P: type) type { + return (?*const fn(ctx: P, port: *Port) callconv(.C) void); + } + }; + }; + + pub const v1 = struct { + const Ctx = ?*anyopaque; + + pub const Symbol = c.libremidi_midi1_symbol; + pub const Message = [*]const Symbol; + + extern fn libremidi_midi1_available_apis(ctx: Ctx, cb: Api.Callback(Ctx)) void; + pub const probeAvailableApis = libremidi_midi1_available_apis; + + fn Callback(comptime P: type) type { + return extern struct { + context: P = null, + callback: (?*const fn(ctx: P, ts: Timestamp, msg: Message, len: usize) callconv(.C) void) = null, + }; + } + }; + + pub const v2 = struct { + const Ctx = ?*anyopaque; + + pub const Symbol = c.libremidi_midi2_symbol; + pub const Message = [*]const Symbol; + + extern fn libremidi_midi2_available_apis(ctx: Ctx, cb: Api.Callback(Ctx)) void; + pub const probeAvailableApis = libremidi_midi2_available_apis; + + fn Callback(comptime P: type) type { + return extern struct { + context: P, + callback: (?*const fn(ctx: P, ts: Timestamp, msg: Message, len: usize) callconv(.C) void), + }; + } + }; +}; diff --git a/build.zig b/build.zig new file mode 100644 index 00000000..25a06cc9 --- /dev/null +++ b/build.zig @@ -0,0 +1,513 @@ +const std = @import("std"); + +const Build = std.Build; +const Module = Build.Module; +const Step = Build.Step; +const ResolvedTarget = Build.ResolvedTarget; +const OptimizeMode = std.builtin.OptimizeMode; +const LinkMode = std.builtin.LinkMode; + + +const cpp_flags = .{ "-std=c++20", "-fPIC" }; + +const cpp_examples = [_][]const u8{ + "midiobserve", + "echo", + "cmidiin", + "cmidiin2", + "midiclock_in", + "midiclock_out", + "midiout", + "client", + "midiprobe", + "qmidiin", + "sysextest", + "minimal", + "midi2_echo", + "rawmidiin", + + // "coroutines", + + // "midi2_interop" + + // Add other examples once backends and such are fixed +}; + +const c_examples = [_][]const u8{ + "c_api", // just one for now +}; + +const zig_examples = [_][]const u8{ + "zig_api", // same +}; + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const use_llvm = if (b.option(bool, "no_llvm", "Use Zig self-hosted compiler codegen backend & linker")) |val| + !val else null; + + const use_lld = b.option(bool, "use_lld", "(default: true on Windows, false elsewhere) Force use of LLD or Zig's self-hosted linker. Throws warnings due to a zig compiler bug.") + orelse switch(target.result.os.tag) { + .windows => true, + else => false, + }; + + const config = .{ + .target = target, + .optimize = optimize, + + .linkage = b.option(LinkMode, "linkage", "(default: static) Build libremidi as a static or dynamic/shared library") orelse .static, + .use_llvm = use_llvm, + .use_lld = use_lld, + + .no_coremidi = b.option(bool, "no_coremidi", "Disable CoreMidi back-end") orelse false, + .no_winmm = b.option(bool, "no_winmm", "Disable WinMM back-end") orelse false, + .no_winuwp = b.option(bool, "no_winuwp", "Disable UWP back-end") orelse false, + .no_winmidi = b.option(bool, "no_winmidi", "Disable WinMIDI back-end") orelse false, + + .no_alsa = b.option(bool, "no_alsa", "Disable ALSA back-end") orelse false, + .no_udev = b.option(bool, "no_udev", "Disable udev support for ALSA") orelse false, + .no_jack = b.option(bool, "no_jack", "Disable JACK back-end") orelse false, + .no_pipewire = b.option(bool, "no_pipewire", "Disable PipeWire back-end") orelse false, + .no_network = b.option(bool, "no_network", "Disable Network back-end") orelse false, + .no_keyboard = b.option(bool, "no_keyboard", "Disable Computer keyboard back-end") orelse false, + + .no_exports = b.option(bool, "no_exports", "Disable dynamic symbol exporting") orelse false, + .no_boost = b.option(bool, "no_boost", "Do not use Boost if available") orelse false, + .slim_message = b.option(usize, "slim_message", "Use a fixed-size message format"), + .ni_midi2 = b.option(bool, "ni_midi2", "Enable compatibility with ni-midi2") orelse false, + // .ci = b.option(bool, "ci", "To be enabled only in CI, some tests cannot run there. Also enables -Werror.") orelse false, + }; + + const cpp_lib, const boost, const nimidi2 = addLibremidiCppLibrary(b, config); + b.installArtifact(cpp_lib); + + const c_lib = addLibremidiCLibrary(b, cpp_lib, config); + b.installArtifact(c_lib); // Not sure if we should provide this? + + const libremidi = addLibremidiZigModule(b, c_lib, "libremidi", config); + + // Not sure it is a good idea either + const zig_lib = b.addLibrary(.{ + .name = "libremidi-zig", + .root_module = libremidi, + .linkage = .static, // this is glue code, no sense in dynamically linking to it + .use_lld = config.use_lld, + .use_llvm = config.use_llvm, + }); + b.installArtifact(zig_lib); + + addExamplesStep(b, cpp_lib, c_lib, libremidi, boost, nimidi2, config); +} + +fn addLibremidiCppLibrary(b: *std.Build, config: anytype) struct { *Build.Step.Compile, ?*Build.Module, ?*Build.Module } { + + const cpp_lib = b.addLibrary(.{ + .name = "libremidi", + .root_module = b.createModule(.{ + .target = config.target, + .optimize = config.optimize, + .link_libc = true, + .link_libcpp = true, + }), + .linkage = config.linkage, // If linkage is specified as dynamic this is what user code wants to dynamically link against + .use_llvm = config.use_llvm, + .use_lld = config.use_lld, // Needed to workaround ANOTHER interdependent Zig bug + }); + + cpp_lib.root_module.addIncludePath(b.path("include/")); + cpp_lib.root_module.addCSourceFiles(.{ + .files = &.{ + "include/libremidi/libremidi.cpp", + "include/libremidi/observer.cpp", + "include/libremidi/midi_in.cpp", + "include/libremidi/midi_out.cpp", + "include/libremidi/reader.cpp", + "include/libremidi/writer.cpp", + "include/libremidi/client.cpp", + }, + .flags = &cpp_flags, + }); + cpp_lib.root_module.linkSystemLibrary("pthread", .{ .preferred_link_mode = .static }); // Needed ? + + const boost, const nimidi2 = addLibremidiConfig(b, cpp_lib.root_module, config); + + return .{ cpp_lib, boost, nimidi2 }; +} + +fn addLibremidiCLibrary(b: *std.Build, cpp_lib: *Build.Step.Compile, config: anytype) *Build.Step.Compile { + + const c_lib = b.addLibrary(.{ + .name = "libremidi-c", + .root_module = b.createModule(.{ + .target = config.target, + .optimize = config.optimize, + }), + .linkage = .static, // this is just glue code, makes no sense to link it dynamically + .use_llvm = config.use_llvm, + .use_lld = config.use_lld, // Needed to workaround ANOTHER interdependent Zig bug + }); + + c_lib.root_module.addIncludePath(b.path("include/")); + c_lib.root_module.addCSourceFiles(.{ + .files = &.{ + "include/libremidi/libremidi-c.cpp", + }, + .flags = &cpp_flags, + }); + c_lib.root_module.linkLibrary(cpp_lib); + + return c_lib; +} + +fn addLibremidiZigModule(b: *std.Build, c_lib: *Build.Step.Compile, name: []const u8, config: anytype) *Build.Module { + + const translated_header = b.addTranslateC(.{ + .root_source_file = b.path("include/libremidi/libremidi-c.h"), + .target = b.graph.host, + .optimize = config.optimize, + // Seems to trigger a bug in zig's new translate-c backend relating to include paths + // .use_clang = false, + }); + translated_header.addIncludePath(b.path("include/")); + + const libremidi_c_mod = b.createModule(.{ + .root_source_file = translated_header.getOutput(), + .target = config.target, + .optimize = config.optimize, + }); + libremidi_c_mod.linkLibrary(c_lib); + + + const libremidi_zig_mod = b.addModule(name, .{ + .root_source_file = b.path("bindings/zig/libremidi.zig"), + .target = config.target, + .optimize = config.optimize, + .imports = &.{ + .{ .name = "libremidi-c", .module = libremidi_c_mod }, + }, + }); + + return libremidi_zig_mod; +} + +fn addLibremidiConfig(b: *std.Build, module: *Build.Module, config: anytype) struct { ?*Build.Module, ?*Build.Module} { + + const boost = addBoostConfig(b, module, config); + addSlimMessageConfig(b, module, boost, config); + addExportsConfig(b, module , config); + const nimidi2 = addNiMidi2Config(b, module, config); // Seems to work? + addEmscriptenConfig(b, module, config); // Broken + addWinMMConfig(b, module, config); + addWinUWPConfig(b, module, config); // Unimplemented + addWinMidiConfig(b, module, config); // Unimplemented + addCoremidiConfig(b, module, config); // Unimplemented + addAlsaConfig(b, module, config); + addJackConfig(b, module, config); + addPipewireConfig(b, module, config); // Broken + addKeyboardConfig(b, module, config); + addNetworkConfig(b, module, boost, config); // Broken + + return .{ boost, nimidi2 }; +} + +fn addCMacroNoValue(module: *Build.Module, macro: []const u8) void { + module.addCMacro(macro, ""); +} + +fn addCMacroNumeric(module: *Build.Module, macro: []const u8, value: anytype) void { + + var should_hold_any_int: [50]u8 = undefined; + module.addCMacro(macro, std.fmt.bufPrint(&should_hold_any_int, "{d}", .{value}) catch @panic("MacroTooBig")); +} + +fn addIncludeDirsFromOtherModule(b: *std.Build, module: *Build.Module, other_module: *Build.Module) void { + for (other_module.include_dirs.items) |include_dir| + module.include_dirs.append(b.allocator, include_dir) catch @panic("OOM"); +} + +fn addBoostConfig(b: *std.Build, libremidi_c: *Build.Module, config: anytype) ?*Build.Module { + if (config.no_boost) { + addCMacroNoValue(libremidi_c, "LIBREMIDI_NO_BOOST"); + return null; + } + + addCMacroNoValue(libremidi_c, "LIBREMIDI_USE_BOOST"); + + const boost = b.dependency("boost", .{ .target = config.target, .optimize = config.optimize, .cobalt = true }); + const boost_artifact = boost.artifact("boost"); + + addIncludeDirsFromOtherModule(b, libremidi_c, boost_artifact.root_module); + + libremidi_c.linkLibrary(boost_artifact); + + return boost_artifact.root_module; +} + +fn addSlimMessageConfig(b: *std.Build, libremidi_c: *Build.Module, boost: ?*Build.Module, config: anytype) void { + _ = b; + + if (boost) |_| if (config.slim_message) |size| + addCMacroNumeric(libremidi_c, "SLIM_MESSAGE", size); +} + +fn addExportsConfig(b: *std.Build, libremidi_c: *Build.Module, config: anytype) void { + _ = b; + + if (!config.no_exports) addCMacroNoValue(libremidi_c, "LIBREMIDI_EXPORTS"); +} + +fn addNiMidi2Config(b: *std.Build, libremidi_c: *Build.Module, config: anytype) ?*Build.Module { + if (!config.ni_midi2) return null; + + const nimidi2_dep = b.dependency("ni_midi2", .{}); + + const nimidi2_lib = b.addStaticLibrary(.{ + .name = "ni-midi2", + .root_module = b.createModule(.{ + .target = config.target, + .optimize = config.optimize, + .link_libcpp = true, + }), + }); + + nimidi2_lib.root_module.addIncludePath(nimidi2_dep.path("inc/")); + nimidi2_lib.root_module.addCSourceFiles(.{ + .root = nimidi2_dep.path("src/"), + .files = &.{ + "capability_inquiry.cpp", + "jitter_reduction_timestamps.cpp", + "midi1_byte_stream.cpp", + "sysex.cpp", + "sysex_collector.cpp", + "universal_packet.cpp", + "universal_sysex.cpp", + }, + .flags = &cpp_flags, + }); + + addIncludeDirsFromOtherModule(b, libremidi_c, nimidi2_lib.root_module); + libremidi_c.addCMacro("LIBREMIDI_USE_NI_MIDI2", "1"); + libremidi_c.linkLibrary(nimidi2_lib); + + return nimidi2_lib.root_module; +} + +// TODO: fix +fn addEmscriptenConfig(b: *std.Build, libremidi_c: *Build.Module, config: anytype) void { + if (config.target.result.os.tag != .emscripten) return; + + _ = b; + + addCMacroNoValue(libremidi_c, "LIBREMIDI_EMSCRIPTEN"); +} + +fn addWinMMConfig(b: *std.Build, libremidi_c: *Build.Module, config: anytype) void { + _ = b; + if ((config.no_winmm) or (config.target.result.os.tag != .windows)) return; + + addCMacroNoValue(libremidi_c, "LIBREMIDI_WINMM"); + // Those seem to take out Zig's stack traces, probably best not to enable them + // libremidi_c.addCMacro("UNICODE", "1"); + // libremidi_c.addCMacro("_UNICODE", "1"); + + libremidi_c.linkSystemLibrary("winmm", .{ .preferred_link_mode = .dynamic }); +} + +// TODO: Implement +fn addWinUWPConfig(b: *std.Build, libremidi_c: *Build.Module, config: anytype) void { + _ = b; + _ = libremidi_c; + _ = config; +} + +// TODO: Implement +fn addWinMidiConfig(b: *std.Build, libremidi_c: *Build.Module, config: anytype) void { + _ = b; + _ = libremidi_c; + _ = config; +} + +// TODO: Implement +fn addCoremidiConfig(b: *std.Build, libremidi_c: *Build.Module, config: anytype) void { + _ = b; + _ = libremidi_c; + _ = config; +} + +// TODO: Could it work on some other OSes? +fn addAlsaConfig(b: *std.Build, libremidi_c: *Build.Module, config: anytype) void { + _ = b; // to keep the signatures the same between all add*Config() functions + if ((config.no_alsa) or (config.target.result.os.tag != .linux)) return; + + addCMacroNoValue(libremidi_c, "LIBREMIDI_ALSA"); + libremidi_c.linkSystemLibrary("asound", .{ .preferred_link_mode = .dynamic }); + + if (config.no_udev) return; + + // Libremidi code needs the "1" value, change to that if/once fixed + // addCMacroNoValue(libremidi_c, "LIBREMIDI_HAS_UDEV"); + libremidi_c.addCMacro("LIBREMIDI_HAS_UDEV", "1"); +} + +// TODO: fix weakjack, make work on other OSes? +fn addJackConfig(b: *std.Build, libremidi_c: *Build.Module, config: anytype) void { + _ = b; + if ((config.no_jack) or (config.target.result.os.tag != .linux)) return; + + addCMacroNoValue(libremidi_c, "LIBREMIDI_JACK"); + // libremidi_c.addCMacro("LIBREMIDI_WEAKJACK", "1"); + + libremidi_c.linkSystemLibrary("jack", .{}); +} + +// TODO: fix +fn addPipewireConfig(b: *std.Build, libremidi_c: *Build.Module, config: anytype) void { + if ((config.no_pipewire) or (config.target.result.os.tag != .linux)) return; + + if (true) return; // disable until fixed + + addCMacroNoValue(libremidi_c, "LIBREMIDI_PIPEWIRE"); + + const rwq_path = b.dependency("readerwriterqueue", .{}).path(""); + libremidi_c.addIncludePath(rwq_path); + + // libremidi_c.addSystemIncludePath(.{ .cwd_relative = "/usr/include/pipewire-0.3/" }); + // libremidi_c.addSystemIncludePath(.{ .cwd_relative = "/usr/include/spa-0.2/" }); + + libremidi_c.linkSystemLibrary("pipewire-0.3", .{}); +} + +fn addKeyboardConfig(b: *std.Build, libremidi_c: *Build.Module, config: anytype) void { + _ = b; + + if (!config.no_keyboard) addCMacroNoValue(libremidi_c, "LIBREMIDI_KEYBOARD"); +} + +// TODO: fix +fn addNetworkConfig(b: *std.Build, libremidi_c: *Build.Module, maybe_boost: ?*Build.Module, config: anytype) void { + _ = b; // to keep the signatures the same between all add*Config() functions + + if (config.no_network) return; + const boost = maybe_boost orelse return; + + if (true) return; // disable until fixed + + addCMacroNoValue(libremidi_c, "LIBREMIDI_NETWORK"); + + if (config.target.result.os.tag == .macos) + if (!(config.target.result.os.isAtLeast(.macos, .{ .major = 15, .minor = 0, .patch = 0 }) orelse false)) + boost.addCMacro("BOOST_ASIO_DISABLE_STD_ALIGNED_ALLOC", "1"); + + + boost.addCMacro("BOOST_ASIO_HAS_STD_INVOKE_RESULT", "1"); + + // something something win32 implement +} + +fn addExamplesStep(b: *std.Build, cpp_lib: *Build.Step.Compile, c_lib: *Build.Step.Compile, libremidi: *Build.Module, boost: ?*Build.Module, nimidi2: ?*Build.Module, config: anytype) void { + + const step = b.step("examples", "Build the examples"); + + inline for (cpp_examples) |name| { + + const example_exe = addCppExample(b, cpp_lib, name, config); + if (boost) |boost_mod| addIncludeDirsFromOtherModule(b, example_exe.root_module, boost_mod); + if (nimidi2) |nimidi2_mod| addIncludeDirsFromOtherModule(b, example_exe.root_module, nimidi2_mod); + + const artifact = b.addInstallArtifact(example_exe, .{}); + step.dependOn(&artifact.step); + } + + inline for (c_examples) |name| { + + const example_exe = addCExample(b, c_lib, name, config); + + const artifact = b.addInstallArtifact(example_exe, .{}); + step.dependOn(&artifact.step); + } + + inline for (zig_examples) |name| { + + const example_exe = addZigExample(b, libremidi, name, config); + const artifact = b.addInstallArtifact(example_exe, .{}); + + step.dependOn(&artifact.step); + } +} + +fn addCppExample(b: *std.Build, cpp_lib: *Build.Step.Compile, name: []const u8, config: anytype) *Build.Step.Compile { + + var buf: [512]u8 = undefined; + + const example_exe = b.addExecutable(.{ + .name = name, + .root_module = b.createModule(.{ + .target = config.target, + .optimize = config.optimize, + }), + .use_llvm = config.use_llvm, + .use_lld = config.use_lld, // Needed to workaround a Zig bug (ziglang/zig#20476) + }); + + example_exe.root_module.addIncludePath(b.path("include/")); + example_exe.root_module.addCSourceFiles(.{ + .files = &.{ + std.fmt.bufPrint(&buf, "examples/{s}.cpp", .{name}) catch @panic("BufferTooSmall"), + }, + .flags = &cpp_flags, + }); + example_exe.root_module.linkLibrary(cpp_lib); + + return example_exe; +} + +fn addCExample(b: *std.Build, c_lib: *Build.Step.Compile, name: []const u8, config: anytype) *Build.Step.Compile { + + var buf: [512]u8 = undefined; + + const example_exe = b.addExecutable(.{ + .name = name, + .root_module = b.createModule(.{ + .target = config.target, + .optimize = config.optimize, + }), + .use_llvm = config.use_llvm, + .use_lld = config.use_lld, // Needed to workaround a Zig bug (ziglang/zig#20476) + }); + + example_exe.root_module.addIncludePath(b.path("include/")); + example_exe.root_module.addCSourceFiles(.{ + .files = &.{ + std.fmt.bufPrint(&buf, "examples/{s}.c", .{name}) catch @panic("BufferTooSmall"), + }, + .flags = &.{}, + }); + example_exe.root_module.linkLibrary(c_lib); + + return example_exe; +} + +fn addZigExample(b: *std.Build, libremidi: *Build.Module, name: []const u8, config: anytype) *Build.Step.Compile { + + var buf: [512]u8 = undefined; + + const zig_exe = b.addExecutable(.{ + .name = name, + .root_module = b.createModule(.{ + .root_source_file = b.path(std.fmt.bufPrint(&buf, "bindings/zig/examples/{s}.zig", .{name}) catch @panic("BufferTooSmall")), + .target = config.target, + .optimize = config.optimize, + .imports = &.{ + .{ .name = "libremidi", .module = libremidi }, + }, + }), + .use_llvm = config.use_llvm, + .use_lld = config.use_lld, // Needed to workaround a Zig bug (ziglang/zig#20476) + }); + + + return zig_exe; +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 00000000..770cd60c --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,26 @@ +.{ + .name = .libremidi, + .version = "5.2.0", + .fingerprint = 0x235e7c9bf5e9c9ee, // Changing this has security and trust implications. + .minimum_zig_version = "0.14.0", + + .dependencies = .{ + + .readerwriterqueue = .{ + .url = "git+https://github.com/cameron314/readerwriterqueue#8b2176698e9bdaba653cdc20c32b54737a934b47", + .hash = "N-V-__8AANDHAwArdTVetOBbZZZeKkJ3T0LaZit_KIZGh1iB", + }, + + .ni_midi2 = .{ + .url = "git+https://github.com/midi2-dev/ni-midi2#49961127b2699cda53cc1c4312ad4e722c1c3a1d", + .hash = "N-V-__8AAHVgEACwk3QBmtC2mSHWXXeV7OiaApXX22dpwyx1", + }, + + .boost = .{ + .url = "git+https://github.com/allyourcodebase/boost-libraries-zig#672a0417c90c6dc3bf1bfbda12115baf991304fa", + .hash = "boost_libraries-1.88.0-frkKHMh_AQAu64vDTKQX5I77VurZqF6J5o26U7wsZrMO", + }, + }, + + .paths = .{ "build.zig", "build.zig.zon", "include/", "bindings/zig/libremidi.zig", "LICENSE.md" }, +}