Skip to content

Commit cbdc09b

Browse files
committed
lua rtd fixes and simplify plus missing test
1 parent ede86bf commit cbdc09b

10 files changed

Lines changed: 431 additions & 91 deletions

File tree

docs/lua-functions.md

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ function my_func(x, y) ... end
8787
|---|---|
8888
| `---` line | Function description (first `---` line) |
8989
| `@param name [type] [description]` | Parameter. Type is `number` (default), `string`, or `boolean`. |
90+
| `@rtd` | RTD subscription function. Return values are `prog_id, topic1, topic2, ...`. Automatically non-thread-safe. |
9091
| `@async` | Run on worker thread with result caching via RTD |
9192
| `@thread_safe false` | Disable multi-threaded recalculation (default: thread-safe) |
9293
| `@category name` | Excel function category (default: `"Lua Functions"`) |
@@ -323,41 +324,39 @@ end
323324

324325
## RTD subscriptions from Lua
325326

326-
Lua functions can subscribe to an RTD server and return a live-updating cell value, just like Zig wrapper functions do. Use `xllify.rtd_subscribe`:
327+
Lua functions can subscribe to an RTD server and return a live-updating cell value, just like Zig wrapper functions do. Add `@rtd` to the annotation — the function returns the prog_id and topic strings, and the framework handles the `xlfRtd` call:
327328

328329
```lua
329330
--- Live price for a symbol
330331
-- @param symbol string Ticker symbol
331-
-- @thread_safe false
332+
-- @rtd
332333
function price(symbol)
333-
return xllify.rtd_subscribe("myprog.rtd", symbol)
334+
return "myprog.rtd", symbol
334335
end
335336
```
336337

337-
`xllify.rtd_subscribe(prog_id, topic1, ...)` accepts a prog ID and up to 28 topic strings. It calls Excel's `xlfRtd` and returns the live cell value. Excel handles the subscription — the cell updates automatically whenever the RTD server pushes a new value.
338-
339-
The return value is forwarded directly to Excel as an RTD subscription; the cell will update live exactly as if the user had typed `=RTD("myprog.rtd", , symbol)`.
340-
341-
**Thread safety is mandatory.** Functions that call `xllify.rtd_subscribe` must be declared `@thread_safe false` (or `.thread_safe = false` in hand-written Zig). The underlying `xlfRtd` call must run on Excel's main thread. Without this, the call will fail or crash.
338+
The function's return values are interpreted as: first = prog_id, rest = topic strings. The framework calls `xlfRtd` with these values and returns the live cell value to Excel. The cell updates automatically whenever the RTD server pushes a new value.
342339

343-
If `xllify.rtd_subscribe` fails (e.g. the RTD server is not registered), it returns `nil`, which Excel renders as an empty cell.
340+
`@rtd` automatically sets `thread_safe = false` (the underlying `xlfRtd` call must run on Excel's main thread). You don't need to add `@thread_safe false` separately.
344341

345-
Multiple topic strings are supported:
342+
Multiple topic strings are just more return values:
346343

347344
```lua
348345
--- Price on a specific exchange
349346
-- @param exchange string Exchange code
350347
-- @param symbol string Ticker symbol
351-
-- @thread_safe false
348+
-- @rtd
352349
function price_on_exchange(exchange, symbol)
353-
return xllify.rtd_subscribe("myprog.rtd", exchange, symbol)
350+
return "myprog.rtd", exchange, symbol
354351
end
355352
```
356353

354+
If the Lua function errors or returns no values, the cell shows `#VALUE!`.
355+
357356
## Limitations
358357

359358
- Maximum 8 parameters per function (same as `ExcelFunction`)
360359
- No matrix/table parameter or return type support yet
361360
- Pool states are independent — global variable mutations don't propagate between states (use `xll.get`/`xll.set` for shared state)
362361
- `is_async = true` automatically forces `thread_safe = false` (async functions use `xlfRtd` which must run on Excel's main thread)
363-
- Functions using `xllify.rtd_subscribe` must be `@thread_safe false``xlfRtd` must run on Excel's main thread
362+
- `@rtd` functions are always non-thread-safe (`xlfRtd` must run on Excel's main thread)

docs/rtd-servers.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,20 +208,18 @@ return rtd_call.subscribe("myprog.rtd", &.{ exchange, symbol });
208208

209209
## Using RTD from Lua
210210

211-
Lua functions can subscribe to an RTD server using `xllify.rtd_subscribe`. This is the Lua equivalent of calling `rtd_call.subscribe()` from Zig:
211+
Add `@rtd` to a Lua function annotation. The function returns the prog_id and topic strings; the framework handles the `xlfRtd` call:
212212

213213
```lua
214214
--- Subscribe to a live NATS subject
215215
-- @param subject string NATS subject
216-
-- @thread_safe false
216+
-- @rtd
217217
function nats_sub(subject)
218-
return xllify.rtd_subscribe("zigxll.connectors.nats", subject)
218+
return "zigxll.connectors.nats", subject
219219
end
220220
```
221221

222-
`xllify.rtd_subscribe(prog_id, topic1, ...)` accepts a prog ID string and up to 28 topic strings. The cell updates live whenever the RTD server pushes a new value.
223-
224-
**Thread safety is mandatory.** Functions calling `xllify.rtd_subscribe` must be `@thread_safe false``xlfRtd` must run on Excel's main thread. See [Lua Functions — RTD subscriptions](lua-functions.md#rtd-subscriptions-from-lua) for full details.
222+
The function's return values are: first = prog_id, rest = topic strings. `@rtd` automatically sets `thread_safe = false`. The cell updates live whenever the RTD server pushes a new value. See [Lua Functions — RTD subscriptions](lua-functions.md#rtd-subscriptions-from-lua) for full details.
225223

226224
## Limitations
227225

example/src/lua/functions.lua

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,17 +151,16 @@ end
151151

152152
--- Subscribe to a timer tick via the demo RTD server.
153153
-- Returns a live-updating counter that increments every 2 seconds.
154-
-- Demonstrates xllify.rtd_subscribe from Lua.
155154
-- @name Lua.TimerTick
156-
-- @thread_safe false
155+
-- @rtd
157156
function timer_tick()
158-
return xllify.rtd_subscribe("zigxll.example.timer")
157+
return "zigxll.rtd", "tick"
159158
end
160159

161160
--- Subscribe to a timer tick with a named topic.
162161
-- @name Lua.TimerTickLabeled
163162
-- @param label string Topic label (ignored by the server, illustrates multi-topic)
164-
-- @thread_safe false
163+
-- @rtd
165164
function timer_tick_labeled(label)
166-
return xllify.rtd_subscribe("zigxll.example.timer", label)
165+
return "zigxll.rtd", label
167166
end

src/async_cache_tests.zig

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
const std = @import("std");
2+
const async_cache = @import("async_cache.zig");
3+
const xl_imports = @import("xl_imports.zig");
4+
const xl = xl_imports.xl;
5+
6+
const allocator = std.heap.c_allocator;
7+
8+
fn makeNumericXloper(val: f64) *xl.XLOPER12 {
9+
const ptr = allocator.create(xl.XLOPER12) catch unreachable;
10+
ptr.* = .{
11+
.xltype = xl.xltypeNum | xl.xlbitDLLFree,
12+
.val = .{ .num = val },
13+
};
14+
return ptr;
15+
}
16+
17+
fn freeXloper(ptr: *xl.XLOPER12) void {
18+
allocator.destroy(ptr);
19+
}
20+
21+
test "cache miss returns null" {
22+
var cache = async_cache.AsyncCache.init();
23+
defer cache.clear();
24+
25+
try std.testing.expect(cache.get("nonexistent") == null);
26+
}
27+
28+
test "put and get" {
29+
var cache = async_cache.AsyncCache.init();
30+
defer cache.clear();
31+
32+
const xloper = makeNumericXloper(42.0);
33+
defer freeXloper(xloper);
34+
35+
cache.put("key1", .{ .xloper = xloper, .completed = true });
36+
37+
const result = cache.get("key1");
38+
try std.testing.expect(result != null);
39+
try std.testing.expect(result.?.completed);
40+
try std.testing.expectEqual(42.0, result.?.xloper.val.num);
41+
}
42+
43+
test "put overwrites existing key" {
44+
var cache = async_cache.AsyncCache.init();
45+
defer cache.clear();
46+
47+
const xloper1 = makeNumericXloper(1.0);
48+
defer freeXloper(xloper1);
49+
const xloper2 = makeNumericXloper(2.0);
50+
defer freeXloper(xloper2);
51+
52+
cache.put("key", .{ .xloper = xloper1, .completed = false });
53+
cache.put("key", .{ .xloper = xloper2, .completed = true });
54+
55+
const result = cache.get("key");
56+
try std.testing.expect(result != null);
57+
try std.testing.expect(result.?.completed);
58+
try std.testing.expectEqual(2.0, result.?.xloper.val.num);
59+
}
60+
61+
test "contains" {
62+
var cache = async_cache.AsyncCache.init();
63+
defer cache.clear();
64+
65+
const xloper = makeNumericXloper(0.0);
66+
defer freeXloper(xloper);
67+
68+
try std.testing.expect(!cache.contains("key"));
69+
cache.put("key", .{ .xloper = xloper, .completed = false });
70+
try std.testing.expect(cache.contains("key"));
71+
}
72+
73+
test "clear removes all entries" {
74+
var cache = async_cache.AsyncCache.init();
75+
76+
const xloper1 = makeNumericXloper(1.0);
77+
defer freeXloper(xloper1);
78+
const xloper2 = makeNumericXloper(2.0);
79+
defer freeXloper(xloper2);
80+
81+
cache.put("a", .{ .xloper = xloper1, .completed = true });
82+
cache.put("b", .{ .xloper = xloper2, .completed = true });
83+
84+
cache.clear();
85+
86+
try std.testing.expect(cache.get("a") == null);
87+
try std.testing.expect(cache.get("b") == null);
88+
}
89+
90+
test "in-progress then completed" {
91+
var cache = async_cache.AsyncCache.init();
92+
defer cache.clear();
93+
94+
const pending = makeNumericXloper(0.0);
95+
defer freeXloper(pending);
96+
const done = makeNumericXloper(99.0);
97+
defer freeXloper(done);
98+
99+
// Mark in-progress
100+
cache.put("calc|5", .{ .xloper = pending, .completed = false });
101+
const r1 = cache.get("calc|5");
102+
try std.testing.expect(!r1.?.completed);
103+
104+
// Complete
105+
cache.put("calc|5", .{ .xloper = done, .completed = true });
106+
const r2 = cache.get("calc|5");
107+
try std.testing.expect(r2.?.completed);
108+
try std.testing.expectEqual(99.0, r2.?.xloper.val.num);
109+
}
110+
111+
test "concurrent reads and writes" {
112+
var cache = async_cache.AsyncCache.init();
113+
defer cache.clear();
114+
115+
const xloper = makeNumericXloper(42.0);
116+
defer freeXloper(xloper);
117+
cache.put("shared", .{ .xloper = xloper, .completed = true });
118+
119+
const Reader = struct {
120+
fn run(c: *async_cache.AsyncCache) void {
121+
for (0..1000) |_| {
122+
if (c.get("shared")) |r| {
123+
std.debug.assert(r.xloper.val.num == 42.0);
124+
}
125+
}
126+
}
127+
};
128+
129+
var threads: [4]std.Thread = undefined;
130+
for (&threads) |*t| {
131+
t.* = std.Thread.spawn(.{}, Reader.run, .{&cache}) catch unreachable;
132+
}
133+
for (&threads) |t| t.join();
134+
}
135+
136+
test "global cache singleton" {
137+
const c1 = async_cache.getGlobalCache();
138+
const c2 = async_cache.getGlobalCache();
139+
try std.testing.expectEqual(c1, c2);
140+
}

src/lua_builtins.zig

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@
66
// xllify.json_stringify(value) -> string
77
// xllify.regex_match(text, pattern, occurrence?) -> string
88
// xllify.regex_replace(text, pattern, replacement) -> string
9-
// xllify.rtd_subscribe(prog_id, topic1, ...) -> lightuserdata (*XLOPER12) | nil
109

1110
const std = @import("std");
1211
const c = @import("lua_c.zig").c;
13-
const rtd_call = @import("rtd_call.zig");
1412

1513
const allocator = std.heap.c_allocator;
1614

@@ -191,57 +189,6 @@ fn writeJsonString(writer: anytype, s: []const u8) !void {
191189
try writer.writeByte('"');
192190
}
193191

194-
// ============================================================================
195-
// RTD
196-
// ============================================================================
197-
198-
/// xllify.rtd_subscribe(prog_id, topic1, topic2, ...) -> lightuserdata | nil
199-
///
200-
/// Calls xlfRtd and returns the resulting *XLOPER12 as a Lua light userdata.
201-
/// The Lua function wrapper (pullResult) recognises light userdata and forwards
202-
/// it directly to Excel as an RTD subscription cell value.
203-
///
204-
/// IMPORTANT: Functions that call this MUST be declared @thread_safe false (or
205-
/// .thread_safe = false in hand-written Zig), because xlfRtd must run on
206-
/// Excel's main thread. Calling from a worker thread will crash or silently
207-
/// fail.
208-
fn luaRtdSubscribe(L: ?*c.lua_State) callconv(.c) c_int {
209-
const state = L orelse return 0;
210-
const n = c.lua_gettop(state);
211-
if (n < 1) {
212-
c.lua_pushnil(state);
213-
return 1;
214-
}
215-
216-
// First arg: prog_id (string)
217-
var prog_id_len: usize = 0;
218-
const prog_id_ptr = c.lua_tolstring(state, 1, &prog_id_len) orelse {
219-
c.lua_pushnil(state);
220-
return 1;
221-
};
222-
const prog_id = prog_id_ptr[0..prog_id_len];
223-
224-
// Remaining args: topic strings (up to 28)
225-
const max_topics = 28;
226-
var topics_buf: [max_topics][]const u8 = undefined;
227-
var topic_count: usize = 0;
228-
var i: c_int = 2;
229-
while (i <= n and topic_count < max_topics) : (i += 1) {
230-
var tlen: usize = 0;
231-
const tptr = c.lua_tolstring(state, i, &tlen) orelse continue;
232-
topics_buf[topic_count] = tptr[0..tlen];
233-
topic_count += 1;
234-
}
235-
236-
const result = rtd_call.subscribeDynamic(prog_id, topics_buf[0..topic_count]) catch {
237-
c.lua_pushnil(state);
238-
return 1;
239-
};
240-
241-
c.lua_pushlightuserdata(state, result);
242-
return 1;
243-
}
244-
245192
// ============================================================================
246193
// Registration
247194
// ============================================================================
@@ -266,8 +213,5 @@ pub fn register(L: *c.lua_State) void {
266213
c.lua_pushcclosure(L, luaJsonStringify, 0);
267214
c.lua_setfield(L, -2, "json_stringify");
268215

269-
c.lua_pushcclosure(L, luaRtdSubscribe, 0);
270-
c.lua_setfield(L, -2, "rtd_subscribe");
271-
272216
c.lua_settop(L, c.lua_gettop(L) - 1); // pop xllify table
273217
}

src/lua_function.zig

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,6 @@ pub fn LuaFunction(comptime meta: anytype) type {
176176
.val = undefined,
177177
});
178178
},
179-
lua.LUA_TLIGHTUSERDATA => {
180-
// xllify.rtd_subscribe() pushes the *XLOPER12 from rtd_call.subscribeDynamic
181-
// as a light userdata. Forward it directly — Excel frees it via xlAutoFree12.
182-
const ptr = lua.lua_touserdata(L, -1);
183-
lua.lua_pop(L, 1);
184-
if (ptr == null) return makeErrorValue();
185-
return @ptrCast(@alignCast(ptr.?));
186-
},
187179
else => {
188180
lua.lua_pop(L, 1);
189181
return makeErrorValue();

0 commit comments

Comments
 (0)