diff --git a/src/async_cache.zig b/src/async_cache.zig index 68d53c6..9dce991 100644 --- a/src/async_cache.zig +++ b/src/async_cache.zig @@ -8,6 +8,7 @@ const std = @import("std"); const xl_imports = @import("xl_imports.zig"); const xl = xl_imports.xl; +const xl_helpers = @import("xl_helpers.zig"); const allocator = std.heap.c_allocator; @@ -57,14 +58,21 @@ pub const AsyncCache = struct { // If key already exists, just update the value if (self.map.getEntry(key)) |entry| { + if (entry.value_ptr.xloper != result.xloper) { + xl_helpers.destroyDllOwnedXloper(allocator, entry.value_ptr.xloper); + } entry.value_ptr.* = result; return; } // New key — dupe it so the cache owns the string - const owned_key = allocator.dupe(u8, key) catch return; + const owned_key = allocator.dupe(u8, key) catch { + xl_helpers.destroyDllOwnedXloper(allocator, result.xloper); + return; + }; self.map.put(owned_key, result) catch { allocator.free(owned_key); + xl_helpers.destroyDllOwnedXloper(allocator, result.xloper); }; } @@ -74,6 +82,7 @@ pub const AsyncCache = struct { defer self.unlock(); var it = self.map.iterator(); while (it.next()) |entry| { + xl_helpers.destroyDllOwnedXloper(allocator, entry.value_ptr.xloper); allocator.free(@constCast(entry.key_ptr.*)); } self.map.clearAndFree(); diff --git a/src/async_cache_tests.zig b/src/async_cache_tests.zig index 8cb96b8..86bd3b3 100644 --- a/src/async_cache_tests.zig +++ b/src/async_cache_tests.zig @@ -14,10 +14,6 @@ fn makeNumericXloper(val: f64) *xl.XLOPER12 { return ptr; } -fn freeXloper(ptr: *xl.XLOPER12) void { - allocator.destroy(ptr); -} - test "cache miss returns null" { var cache = async_cache.AsyncCache.init(); defer cache.clear(); @@ -30,7 +26,6 @@ test "put and get" { defer cache.clear(); const xloper = makeNumericXloper(42.0); - defer freeXloper(xloper); cache.put("key1", .{ .xloper = xloper, .completed = true }); @@ -45,9 +40,7 @@ test "put overwrites existing key" { defer cache.clear(); const xloper1 = makeNumericXloper(1.0); - defer freeXloper(xloper1); const xloper2 = makeNumericXloper(2.0); - defer freeXloper(xloper2); cache.put("key", .{ .xloper = xloper1, .completed = false }); cache.put("key", .{ .xloper = xloper2, .completed = true }); @@ -63,7 +56,6 @@ test "contains" { defer cache.clear(); const xloper = makeNumericXloper(0.0); - defer freeXloper(xloper); try std.testing.expect(!cache.contains("key")); cache.put("key", .{ .xloper = xloper, .completed = false }); @@ -74,9 +66,7 @@ test "clear removes all entries" { var cache = async_cache.AsyncCache.init(); const xloper1 = makeNumericXloper(1.0); - defer freeXloper(xloper1); const xloper2 = makeNumericXloper(2.0); - defer freeXloper(xloper2); cache.put("a", .{ .xloper = xloper1, .completed = true }); cache.put("b", .{ .xloper = xloper2, .completed = true }); @@ -92,9 +82,7 @@ test "in-progress then completed" { defer cache.clear(); const pending = makeNumericXloper(0.0); - defer freeXloper(pending); const done = makeNumericXloper(99.0); - defer freeXloper(done); // Mark in-progress cache.put("calc|5", .{ .xloper = pending, .completed = false }); @@ -113,7 +101,6 @@ test "concurrent reads and writes" { defer cache.clear(); const xloper = makeNumericXloper(42.0); - defer freeXloper(xloper); cache.put("shared", .{ .xloper = xloper, .completed = true }); const Reader = struct { diff --git a/src/async_infra.zig b/src/async_infra.zig index 9029852..07bfa59 100644 --- a/src/async_infra.zig +++ b/src/async_infra.zig @@ -315,14 +315,14 @@ fn asyncValueToXloper(val: AsyncValue) *xl.XLOPER12 { }; }, .string => |v| { - const xv = XLValue.fromUtf8String(allocator, v) catch { + var xv = XLValue.fromUtf8String(allocator, v) catch { ptr.* = .{ .xltype = xl.xltypeErr | xl.xlbitDLLFree, .val = .{ .err = xl.xlerrValue }, }; return ptr; }; - ptr.* = xv.m_val; + ptr.* = xv.intoXLOPER12(); ptr.xltype |= xl.xlbitDLLFree; }, } diff --git a/src/excel_function.zig b/src/excel_function.zig index cd22180..915d1d2 100644 --- a/src/excel_function.zig +++ b/src/excel_function.zig @@ -625,8 +625,8 @@ pub fn ExcelFunction(comptime meta: anytype) type { }); } else if (T == []const u8 or T == []u8) { defer allocator.free(result); - const val = XLValue.fromUtf8String(allocator, result) catch return makeErrorValue(); - return heapXloper(val.m_val); + var val = XLValue.fromUtf8String(allocator, result) catch return makeErrorValue(); + return heapXloper(val.intoXLOPER12()); } else if (T == [][]const f64 or T == [][]f64) { defer { for (result) |row| { @@ -634,8 +634,8 @@ pub fn ExcelFunction(comptime meta: anytype) type { } allocator.free(result); } - const val = XLValue.fromMatrix(allocator, result) catch return makeErrorValue(); - return heapXloper(val.m_val); + var val = XLValue.fromMatrix(allocator, result) catch return makeErrorValue(); + return heapXloper(val.intoXLOPER12()); } else if (T == *xl.XLOPER12) { return result; } else { diff --git a/src/framework_entry.zig b/src/framework_entry.zig index 3be36c6..0a60432 100644 --- a/src/framework_entry.zig +++ b/src/framework_entry.zig @@ -317,14 +317,7 @@ pub fn xlAutoFree12(pxFree: ?*xl.XLOPER12) callconv(.c) void { // Only free if xlbitDLLFree is set (means we allocated it) if ((oper.xltype & xl.xlbitDLLFree) != 0) { - // Free string data if present - if ((oper.xltype & xl.xltypeStr) != 0) { - if (oper.val.str) |str_ptr| { - const len = @as(usize, @intCast(str_ptr[0])); - // Free: length prefix (1) + string chars (len) + null terminator (1) - excel_allocator.free(str_ptr[0 .. len + 2]); - } - } + xl_helpers.freeDllOwnedPayload(excel_allocator, oper); // Free the XLOPER12 structure itself excel_allocator.destroy(oper); } diff --git a/src/lua.zig b/src/lua.zig index 3d21e0c..365edb3 100644 --- a/src/lua.zig +++ b/src/lua.zig @@ -156,12 +156,6 @@ fn unlockSharedStore() void { shared_store_mutex.unlock(io); } -fn sharedGet(key: []const u8) ?SharedValue { - lockSharedStore(); - defer unlockSharedStore(); - return shared_store.get(key); -} - fn sharedSet(key: []const u8, value: ?SharedValue) void { lockSharedStore(); defer unlockSharedStore(); @@ -202,7 +196,10 @@ fn luaXllGet(L: ?*c.lua_State) callconv(.c) c_int { }; const key = ptr[0..len]; - if (sharedGet(key)) |val| { + lockSharedStore(); + defer unlockSharedStore(); + + if (shared_store.get(key)) |val| { switch (val) { .number => |n| c.lua_pushnumber(state, n), .boolean => |b| c.lua_pushboolean(state, if (b) 1 else 0), @@ -271,6 +268,16 @@ pub fn getPoolSize() usize { pub fn init() !void { if (pool_initialized) return; + errdefer { + for (&state_pool) |*slot| { + if (slot.L) |L| { + c.lua_close(L); + slot.L = null; + } + slot.in_use = std.atomic.Value(bool).init(false); + } + } + for (&state_pool) |*slot| { slot.L = createState() catch return error.LuaInitFailed; } diff --git a/src/lua_function.zig b/src/lua_function.zig index ef124a2..792f7e5 100644 --- a/src/lua_function.zig +++ b/src/lua_function.zig @@ -154,12 +154,12 @@ pub fn LuaFunction(comptime meta: anytype) type { return makeErrorValue(); } const str = ptr.?[0..len]; - const xlval = XLValue.fromUtf8String(allocator, str) catch { + var xlval = XLValue.fromUtf8String(allocator, str) catch { lua.lua_pop(L, 1); return makeErrorValue(); }; lua.lua_pop(L, 1); - return heapXloper(xlval.m_val); + return heapXloper(xlval.intoXLOPER12()); }, lua.LUA_TBOOLEAN => { const b = lua.lua_toboolean(L, -1); @@ -381,6 +381,7 @@ pub fn LuaFunction(comptime meta: anytype) type { freeXloperCopies(&pack.xloper_copies); allocator.free(pack.key); allocator.destroy(pack); + async_infra.storeResult(key, makeErrorValue()); } return rtd_result; diff --git a/src/rtd.zig b/src/rtd.zig index a983556..8a7dcb9 100644 --- a/src/rtd.zig +++ b/src/rtd.zig @@ -422,7 +422,8 @@ pub fn RtdServer(comptime Handler: type, comptime config: RtdConfig) type { // ---- server state (non-extern, allocated on heap) ---- const ServerState = struct { - ref_count: ULONG = 1, + ref_count: ULONG = 0, + terminated: bool = false, ctx: RtdContext = .{}, handler: Handler = .{}, }; @@ -494,10 +495,7 @@ pub fn RtdServer(comptime Handler: type, comptime config: RtdConfig) type { s.ref_count -= 1; const rc = s.ref_count; if (rc == 0) { - s.handler.onTerminate(&s.ctx); - if (s.ctx.update_event) |evt| { - _ = evt.vtable.Release(@ptrCast(evt)); - } + terminateOnce(s); s.ctx.deinit(); std.heap.c_allocator.destroy(s); std.heap.c_allocator.destroy(obj); @@ -752,13 +750,20 @@ pub fn RtdServer(comptime Handler: type, comptime config: RtdConfig) type { debugLog("ServerTerminate", .{}); const s = getObj(self_opaque).getState(); + terminateOnce(s); + return S_OK; + } + + fn terminateOnce(s: *ServerState) void { + if (s.terminated) return; + s.terminated = true; + s.handler.onTerminate(&s.ctx); if (s.ctx.update_event) |evt| { _ = evt.vtable.Release(@ptrCast(evt)); s.ctx.update_event = null; } - return S_OK; } fn connectDataStub(_: *anyopaque, _: LONG, _: **SAFEARRAY, _: *c_short, _: *VARIANT) callconv(.winapi) HRESULT { @@ -813,7 +818,13 @@ pub fn RtdServer(comptime Handler: type, comptime config: RtdConfig) type { obj.* = .{ .state = @ptrCast(s) }; _ = @atomicRmw(i32, &g_object_count, .Add, 1, .monotonic); - return queryInterface(@ptrCast(obj), riid, ppv); + const hr = queryInterface(@ptrCast(obj), riid, ppv); + if (hr != S_OK) { + std.heap.c_allocator.destroy(s); + std.heap.c_allocator.destroy(obj); + _ = @atomicRmw(i32, &g_object_count, .Sub, 1, .monotonic); + } + return hr; } fn cfLockServer(_: *anyopaque, fLock: c_int) callconv(.winapi) HRESULT { diff --git a/src/xl_helpers.zig b/src/xl_helpers.zig index df6fbc0..4135cb9 100644 --- a/src/xl_helpers.zig +++ b/src/xl_helpers.zig @@ -43,3 +43,34 @@ pub fn debugLogRuntime(msg: []const u8) void { pub inline fn xlFree(oper: *xl.XLOPER12) void { _ = xl.Excel12f(xl.xlFree, null, 1, oper); } + +fn xloperBaseType(xltype: @TypeOf(@as(xl.XLOPER12, undefined).xltype)) @TypeOf(@as(xl.XLOPER12, undefined).xltype) { + return xltype & 0xFFF; +} + +pub fn freeDllOwnedPayload(allocator: std.mem.Allocator, oper: *xl.XLOPER12) void { + switch (xloperBaseType(oper.xltype)) { + xl.xltypeStr => { + if (oper.val.str) |str_ptr| { + const len = @as(usize, @intCast(str_ptr[0])); + allocator.free(str_ptr[0 .. len + 2]); + } + }, + xl.xltypeMulti => { + const rows = @as(usize, @intCast(oper.val.array.rows)); + const cols = @as(usize, @intCast(oper.val.array.columns)); + const cells = oper.val.array.lparray[0 .. rows * cols]; + for (cells) |*cell| { + freeDllOwnedPayload(allocator, cell); + } + allocator.free(cells); + }, + else => {}, + } +} + +pub fn destroyDllOwnedXloper(allocator: std.mem.Allocator, oper: *xl.XLOPER12) void { + if ((oper.xltype & xl.xlbitDLLFree) == 0) return; + freeDllOwnedPayload(allocator, oper); + allocator.destroy(oper); +} diff --git a/src/xlvalue.zig b/src/xlvalue.zig index 0168c3c..26d9466 100644 --- a/src/xlvalue.zig +++ b/src/xlvalue.zig @@ -101,8 +101,16 @@ pub const XLValue = struct { } pub fn fromMatrix(allocator: std.mem.Allocator, data: []const []const f64) !XLValue { + if (data.len > std.math.maxInt(i32)) return error.MatrixTooLarge; + + const cols = if (data.len > 0) data[0].len else 0; + if (cols > std.math.maxInt(i32)) return error.MatrixTooLarge; + for (data) |row| { + if (row.len != cols) return error.RaggedMatrix; + } + const num_rows: i32 = @intCast(data.len); - const num_cols: i32 = if (data.len > 0) @intCast(data[0].len) else 0; + const num_cols: i32 = @intCast(cols); // Allocate multi array - be explicit about the type const array_size = @as(usize, @intCast(num_rows)) * @as(usize, @intCast(num_cols)); @@ -312,16 +320,17 @@ pub const XLValue = struct { const num_cols = self.columns(); var result = try self.allocator.alloc([]const f64, num_rows); + var initialized_rows: usize = 0; errdefer { - for (result, 0..) |row, i| { - if (i > 0) self.allocator.free(row); + for (result[0..initialized_rows]) |row| { + self.allocator.free(row); } self.allocator.free(result); } for (0..num_rows) |r| { var row = try self.allocator.alloc(f64, num_cols); - errdefer if (r > 0) self.allocator.free(row); + errdefer self.allocator.free(row); for (0..num_cols) |c| { const cell = try self.get_cell(r, c); @@ -342,6 +351,7 @@ pub const XLValue = struct { } result[r] = row; + initialized_rows += 1; } return result; @@ -356,20 +366,42 @@ pub const XLValue = struct { return &self.m_val; } + pub fn intoXLOPER12(self: *XLValue) xl.XLOPER12 { + const result = self.m_val; + self.m_owns_memory = false; + return result; + } + // Cleanup + fn baseType(xltype: @TypeOf(@as(xl.XLOPER12, undefined).xltype)) @TypeOf(@as(xl.XLOPER12, undefined).xltype) { + return xltype & 0xFFF; + } + + fn free_xloper_payload(self: *XLValue, oper: *xl.XLOPER12) void { + switch (baseType(oper.xltype)) { + xl.xltypeStr => { + const str_ptr = oper.val.str; + const len = @as(usize, @intCast(str_ptr[0])); + self.allocator.free(str_ptr[0 .. len + 2]); + }, + xl.xltypeMulti => { + const rows_count = @as(usize, @intCast(oper.val.array.rows)); + const cols_count = @as(usize, @intCast(oper.val.array.columns)); + const total = rows_count * cols_count; + const cells = oper.val.array.lparray[0..total]; + for (cells) |*cell| { + self.free_xloper_payload(cell); + } + self.allocator.free(cells); + }, + else => {}, + } + } + fn free_memory(self: *XLValue) void { if (!self.m_owns_memory) return; - if (self.is_str()) { - const str_ptr = self.m_val.val.str; - const len = @as(usize, @intCast(str_ptr[0])); - self.allocator.free(str_ptr[0 .. len + 2]); // Pascal string: 1 wchar for length prefix + len wchars for data + 1 null terminator - } else if (self.is_multi()) { - const rows_count = @as(usize, @intCast(self.m_val.val.array.rows)); - const cols_count = @as(usize, @intCast(self.m_val.val.array.columns)); - const total = rows_count * cols_count; - self.allocator.free(self.m_val.val.array.lparray[0..total]); - } + self.free_xloper_payload(&self.m_val); self.m_owns_memory = false; } @@ -762,6 +794,55 @@ test "fromXLOPER12 with raw multi array (with ownership)" { try std.testing.expectEqual(@as(f64, 40.0), try cell.as_double()); } +test "fromXLOPER12 owned multi frees nested string cells" { + const allocator = std.testing.allocator; + + var first = try allocator.alloc(u16, 4); + first[0] = 2; + first[1] = 'o'; + first[2] = 'k'; + first[3] = 0; + + var second = try allocator.alloc(u16, 5); + second[0] = 3; + second[1] = 'y'; + second[2] = 'e'; + second[3] = 's'; + second[4] = 0; + + var cells = try allocator.alloc(xl.XLOPER12, 2); + cells[0] = .{ .xltype = xl.xltypeStr, .val = .{ .str = first.ptr } }; + cells[1] = .{ .xltype = xl.xltypeStr, .val = .{ .str = second.ptr } }; + + const raw: xl.XLOPER12 = .{ + .xltype = xl.xltypeMulti, + .val = .{ .array = .{ + .lparray = cells.ptr, + .rows = 1, + .columns = 2, + } }, + }; + + var val = XLValue.fromXLOPER12(allocator, raw, true); + defer val.deinit(); + + try std.testing.expect(val.is_multi()); +} + +test "fromMatrix rejects ragged rows" { + const allocator = std.testing.allocator; + + try std.testing.expectError(error.RaggedMatrix, XLValue.fromMatrix(allocator, &.{ + &.{ 1.0, 2.0 }, + &.{ 3.0, 4.0, 5.0 }, + })); + + try std.testing.expectError(error.RaggedMatrix, XLValue.fromMatrix(allocator, &.{ + &.{ 1.0, 2.0 }, + &.{3.0}, + })); +} + test "fromXLOPER12 with mixed type array" { const allocator = std.testing.allocator;