|
| 1 | +const std = @import("std"); |
| 2 | +const zigplug = @import("root.zig"); |
| 3 | + |
| 4 | +const Plugin = @This(); |
| 5 | + |
| 6 | +pub const Log = struct { |
| 7 | + pub const LogFn = *const fn ( |
| 8 | + context: *anyopaque, |
| 9 | + level: std.log.Level, |
| 10 | + text: [:0]const u8, |
| 11 | + ) void; |
| 12 | + |
| 13 | + context: *anyopaque, |
| 14 | + logFn: LogFn = default, |
| 15 | + allocator: std.mem.Allocator, |
| 16 | + |
| 17 | + pub fn default(_: *anyopaque, level: std.log.Level, text: [:0]const u8) void { |
| 18 | + switch (level) { |
| 19 | + .err => std.log.err("{s}", .{text}), |
| 20 | + .warn => std.log.warn("{s}", .{text}), |
| 21 | + .info => std.log.info("{s}", .{text}), |
| 22 | + .debug => std.log.debug("{s}", .{text}), |
| 23 | + } |
| 24 | + } |
| 25 | + |
| 26 | + const stack_size = 512; |
| 27 | + |
| 28 | + pub inline fn log(self: *const Log, comptime level: std.log.Level, comptime format: []const u8, args: anytype) void { |
| 29 | + if (comptime std.log.defaultLogEnabled(level)) { |
| 30 | + var stack_allocator = std.heap.stackFallback(stack_size, self.allocator); |
| 31 | + const allocator = stack_allocator.get(); |
| 32 | + const text = std.fmt.allocPrintSentinel(allocator, format, args, 0) catch return; |
| 33 | + defer allocator.free(text); |
| 34 | + self.logFn(self.context, level, text); |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + pub inline fn debug(self: *const Log, comptime format: []const u8, args: anytype) void { |
| 39 | + self.log(.debug, format, args); |
| 40 | + } |
| 41 | + |
| 42 | + pub inline fn err(self: *const Log, comptime format: []const u8, args: anytype) void { |
| 43 | + self.log(.err, format, args); |
| 44 | + } |
| 45 | + |
| 46 | + pub inline fn info(self: *const Log, comptime format: []const u8, args: anytype) void { |
| 47 | + self.log(.info, format, args); |
| 48 | + } |
| 49 | + |
| 50 | + pub inline fn warn(self: *const Log, comptime format: []const u8, args: anytype) void { |
| 51 | + self.log(.warn, format, args); |
| 52 | + } |
| 53 | +}; |
| 54 | + |
| 55 | +context: *anyopaque, |
| 56 | +vtable: struct { |
| 57 | + deinit: *const fn (*anyopaque) void, |
| 58 | + process: *const fn (*anyopaque, zigplug.ProcessBlock, ?*const anyopaque) anyerror!void, |
| 59 | +}, |
| 60 | + |
| 61 | +allocator: std.mem.Allocator, |
| 62 | +log: *Log, |
| 63 | +parameters: ?zigplug.parameters.State, |
| 64 | +sample_rate_hz: u32 = 0, |
| 65 | + |
| 66 | +pub fn init(comptime T: type) !Plugin { |
| 67 | + if (!@hasDecl(T, "meta") or @TypeOf(T.meta) != zigplug.Meta) |
| 68 | + @compileError( |
| 69 | + \\Plugin is missing a metadata object. |
| 70 | + \\ |
| 71 | + \\Add one to your root plugin struct: |
| 72 | + \\`pub const meta = @import("zigplug").Meta{...};` |
| 73 | + ); |
| 74 | + |
| 75 | + validateFunction(T, "init", &.{}, anyerror!T); |
| 76 | + validateFunction(T, "deinit", &.{*T}, void); |
| 77 | + validateFunction(T, "allocator", &.{*T}, std.mem.Allocator); |
| 78 | + validateFunction( |
| 79 | + T, |
| 80 | + "process", |
| 81 | + if (@hasDecl(T, "Parameters")) |
| 82 | + &.{ *T, zigplug.ProcessBlock, *const T.Parameters } |
| 83 | + else |
| 84 | + &.{ *T, zigplug.ProcessBlock }, |
| 85 | + anyerror!void, |
| 86 | + ); |
| 87 | + |
| 88 | + const context = try std.heap.page_allocator.create(T); |
| 89 | + context.* = try T.init(); |
| 90 | + |
| 91 | + const allocator = context.allocator(); |
| 92 | + |
| 93 | + const log = try allocator.create(Log); |
| 94 | + log.* = .{ |
| 95 | + // SAFETY: only accessed when logFn is different than defaultLog |
| 96 | + .context = undefined, |
| 97 | + .allocator = allocator, |
| 98 | + }; |
| 99 | + |
| 100 | + return .{ |
| 101 | + .context = context, |
| 102 | + .vtable = .{ |
| 103 | + .deinit = @ptrCast(&T.deinit), |
| 104 | + .process = @ptrCast(&T.process), |
| 105 | + }, |
| 106 | + .allocator = allocator, |
| 107 | + .log = log, |
| 108 | + .parameters = if (@hasDecl(T, "Parameters")) blk: { |
| 109 | + const Parameters = T.Parameters; |
| 110 | + switch (@typeInfo(Parameters)) { |
| 111 | + .@"struct" => |info| { |
| 112 | + inline for (info.fields) |field| { |
| 113 | + if (field.type != zigplug.Parameter) |
| 114 | + @compileError("`Parameters` struct field '" ++ field.name ++ "' is not of type `zigplug.Parameter`"); |
| 115 | + if (field.defaultValue() == null) |
| 116 | + @compileError("`Parameters` struct field '" ++ field.name ++ "' has no default value"); |
| 117 | + } |
| 118 | + }, |
| 119 | + else => @compileError("`Parameters` is not a struct"), |
| 120 | + } |
| 121 | + |
| 122 | + const parameters_context = try allocator.create(Parameters); |
| 123 | + parameters_context.* = .{}; |
| 124 | + const fields = @typeInfo(Parameters).@"struct".fields; |
| 125 | + |
| 126 | + var parameters_slice = try allocator.alloc(*zigplug.Parameter, fields.len); |
| 127 | + |
| 128 | + inline for (fields, 0..) |field, i| { |
| 129 | + const param = &@field(parameters_context, field.name); |
| 130 | + switch (param.*) { |
| 131 | + inline else => |*p| p.options.id = p.options.id orelse field.name, |
| 132 | + } |
| 133 | + parameters_slice[i] = param; |
| 134 | + } |
| 135 | + |
| 136 | + break :blk .{ |
| 137 | + .context = parameters_context, |
| 138 | + .log = log, |
| 139 | + .slice = parameters_slice, |
| 140 | + .allocator = allocator, |
| 141 | + }; |
| 142 | + } else null, |
| 143 | + }; |
| 144 | +} |
| 145 | + |
| 146 | +pub inline fn deinit(self: *Plugin, comptime P: type) void { |
| 147 | + self.allocator.destroy(self.log); |
| 148 | + |
| 149 | + if (@hasDecl(P, "Parameters")) { |
| 150 | + const params = self.parameters.?; |
| 151 | + self.allocator.free(params.slice); |
| 152 | + const ptr: *P.Parameters = @ptrCast(@alignCast(params.context)); |
| 153 | + self.allocator.destroy(ptr); |
| 154 | + } |
| 155 | + |
| 156 | + self.vtable.deinit(self.context); |
| 157 | + |
| 158 | + const plugin: *P = @ptrCast(@alignCast(self.context)); |
| 159 | + std.heap.page_allocator.destroy(plugin); |
| 160 | +} |
| 161 | + |
| 162 | +pub inline fn process(self: *Plugin, block: zigplug.ProcessBlock, params: ?*const anyopaque) !void { |
| 163 | + try self.vtable.process(self.context, block, params); |
| 164 | +} |
| 165 | + |
| 166 | +pub fn setLog(self: *Plugin, context: *anyopaque, logFn: Log.LogFn) void { |
| 167 | + self.log.context = context; |
| 168 | + self.log.logFn = logFn; |
| 169 | +} |
| 170 | + |
| 171 | +// error unions make `==` type comparison wonky so we have to compare arg and return types manually |
| 172 | +fn validateFunction(comptime Container: type, comptime name: []const u8, comptime args: []const type, Return: type) void { |
| 173 | + const ExpectedType = @Type(.{ .@"fn" = .{ |
| 174 | + .params = comptime blk: { |
| 175 | + var params: [args.len]std.builtin.Type.Fn.Param = undefined; |
| 176 | + for (args, 0..) |Arg, i| |
| 177 | + params[i] = .{ |
| 178 | + .type = Arg, |
| 179 | + .is_generic = false, |
| 180 | + .is_noalias = false, |
| 181 | + }; |
| 182 | + break :blk ¶ms; |
| 183 | + }, |
| 184 | + .return_type = Return, |
| 185 | + .calling_convention = .auto, |
| 186 | + .is_var_args = false, |
| 187 | + .is_generic = false, |
| 188 | + } }); |
| 189 | + |
| 190 | + if (!@hasDecl(Container, name)) |
| 191 | + @compileError("Plugin is missing method '" ++ name ++ "' of type '" ++ @typeName(ExpectedType) ++ "'"); |
| 192 | + |
| 193 | + const Fn = @TypeOf(@field(Container, name)); |
| 194 | + const info = @typeInfo(Fn).@"fn"; |
| 195 | + const msg = "Wrong signature for method '" ++ name ++ "': expected '" ++ @typeName(ExpectedType) ++ "', found '" ++ @typeName(Fn) ++ "'"; |
| 196 | + |
| 197 | + const Actual = info.return_type orelse void; |
| 198 | + |
| 199 | + switch (@typeInfo(Actual)) { |
| 200 | + .error_union => |actual_error_union| { |
| 201 | + switch (@typeInfo(Return)) { |
| 202 | + .error_union => |expected_error_union| { |
| 203 | + if (actual_error_union.payload != expected_error_union.payload) |
| 204 | + @compileError(msg); |
| 205 | + }, |
| 206 | + else => @compileError(msg), |
| 207 | + } |
| 208 | + }, |
| 209 | + else => if (Actual != Return) @compileError(msg), |
| 210 | + } |
| 211 | + |
| 212 | + const params = info.params; |
| 213 | + if (params.len != args.len) |
| 214 | + @compileError(msg); |
| 215 | + |
| 216 | + inline for (params, args) |param, Arg| { |
| 217 | + if (param.type != Arg) |
| 218 | + @compileError(msg); |
| 219 | + } |
| 220 | +} |
0 commit comments