Repository navigation
Expand file tree
/
Copy pathlua_async.m
More file actions
328 lines (285 loc) · 10.2 KB
/
Copy pathlua_async.m
File metadata and controls
328 lines (285 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
/* lua_async.m — shared Lua async bridge: state ownership, HTTP, timer, JSON.
*
* Included directly into the AppKit and UIKit runtime translation units.
*
* LIFETIME MODEL
* ==============
* Each lua_State gets exactly one LuaStateOwner stored unretained in
* lua_getextraspace(L). LUA_EXTRASPACE is sizeof(void*) — one slot —
* which the owner pointer occupies fully.
*
* Lua copies the main thread's extraspace into every coroutine at
* lua_newthread time, so owner_for_state(co) resolves the same owner
* from any coroutine thread with no registry lookup.
*
* For runtime-created states, the owner is the only ARC strong reference to
* the lua_State and closes it at the end of its lifetime. A framework loaded
* into an external host, such as UIKit.dylib, uses a non-closing owner: the
* host still owns the state, while callbacks use the same cancellation and
* coroutine-resolution machinery.
* Async blocks (HTTP completion, timer block) capture the owner
* strongly, keeping the state alive until outstanding work drains.
* For closing owners, the last release calls -dealloc → lua_close on the main
* thread. Non-closing owners only cancel and release their pending work.
*
* CANCELLATION MODEL
* ==================
* When a canvas is re-evaluated, the old canvas owner is cancelled:
* [owner cancel]
* This sets owner.cancelled = YES, calls -cancel on all pending
* NSURLSessionDataTasks, and invalidates all pending NSTimers.
* Subsequent async callbacks check owner.cancelled and drop silently,
* so orphaned coroutines from a stale eval never resume.
*
* Per-coroutine flags are intentionally absent: LUA_EXTRASPACE holds
* exactly one pointer (the shared owner), and cancellation is always
* per-state, not per-coroutine. Any coroutine running in the state
* reads the same owner.cancelled flag via owner_for_state(L).
*/
static void report_lua_error(lua_State *L, const char *context);
#ifndef LUA_OBJC_HTTP_USER_AGENT
#define LUA_OBJC_HTTP_USER_AGENT @"lua-objc/1.0"
#endif
#pragma mark - LuaStateOwner
@interface LuaStateOwner : NSObject
@property (nonatomic, readonly) lua_State *L;
@property (nonatomic, readonly) BOOL cancelled;
@property (nonatomic, readonly) BOOL closing;
- (instancetype)initWithState:(lua_State *)L;
- (instancetype)initWithState:(lua_State *)L closesState:(BOOL)closesState;
- (void)cancel;
- (void)detachState;
- (void)prepareToClose;
- (void)trackTask:(NSURLSessionDataTask *)task;
- (void)trackTimer:(NSTimer *)timer;
@end
void lua_objc_prepare_close(lua_State *L);
@implementation LuaStateOwner {
NSMutableArray *_pending; /* NSURLSessionDataTask | NSTimer */
BOOL _closesState;
CFRunLoopObserverRef _gcObserver;
}
- (instancetype)initWithState:(lua_State *)L {
return [self initWithState:L closesState:YES];
}
- (instancetype)initWithState:(lua_State *)L closesState:(BOOL)closesState {
self = [super init];
_L = L;
_closesState = closesState;
_pending = [NSMutableArray array];
/* Store unretained so coroutines can resolve the owner via extraspace
* without creating a retain cycle. The owner's lifetime is managed
* by whoever created it (a local ARC variable), not by the state. */
*(void **)lua_getextraspace(L) = (__bridge void *)self;
/* Lua userdata is tiny relative to the native view tree it may pin, so
* step the collector whenever the run loop is about to sleep. */
__weak LuaStateOwner *weakSelf = self;
_gcObserver = CFRunLoopObserverCreateWithHandler(
kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
LuaStateOwner *owner = weakSelf;
lua_State *state = owner.L;
if (state && !owner.closing) lua_gc(state, LUA_GCSTEP, 40);
});
if (_gcObserver) {
CFRunLoopAddObserver(CFRunLoopGetMain(), _gcObserver,
kCFRunLoopCommonModes);
}
#if TARGET_OS_IPHONE
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(didReceiveMemoryWarning)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
#endif
return self;
}
- (void)cancel {
if (_cancelled) return;
_cancelled = YES;
for (id item in _pending) {
if ([item isKindOfClass:[NSURLSessionDataTask class]]) {
[(NSURLSessionDataTask *)item cancel];
} else if ([item isKindOfClass:[NSTimer class]]) {
[(NSTimer *)item invalidate];
}
}
[_pending removeAllObjects];
}
- (void)prepareToClose {
_closing = YES;
[self cancel];
}
- (void)detachState {
[self prepareToClose];
_L = NULL;
}
#if TARGET_OS_IPHONE
- (void)didReceiveMemoryWarning {
if (_L && !_closing) lua_gc(_L, LUA_GCCOLLECT, 0);
}
#endif
- (void)trackTask:(NSURLSessionDataTask *)task {
if (_cancelled) { [task cancel]; return; }
[_pending addObject:task];
}
- (void)trackTimer:(NSTimer *)timer {
if (_cancelled) { [timer invalidate]; return; }
[_pending addObject:timer];
}
- (void)_untrack:(id)item {
[_pending removeObjectIdenticalTo:item];
}
- (void)dealloc {
#if TARGET_OS_IPHONE
[[NSNotificationCenter defaultCenter] removeObserver:self];
#endif
if (_gcObserver) {
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _gcObserver,
kCFRunLoopCommonModes);
CFRelease(_gcObserver);
_gcObserver = NULL;
}
_closing = YES;
lua_State *L = _L;
_L = NULL;
if (!L || !_closesState) return;
if ([NSThread isMainThread]) {
lua_close(L);
} else {
/* lua_close runs __gc handlers that release native views; both AppKit
* and UIKit require view deallocation on the main thread. */
dispatch_async(dispatch_get_main_queue(), ^{
lua_close(L);
});
}
}
@end
/* Resolve the owner from any thread of a global state (main or coroutine).
* Async bridge functions reject states whose runtime did not install one. */
static inline LuaStateOwner *owner_for_state(lua_State *L) {
return (__bridge LuaStateOwner *)(*(void **)lua_getextraspace(L));
}
void lua_objc_prepare_close(lua_State *L) {
if (!L) return;
LuaStateOwner *owner = owner_for_state(L);
[owner prepareToClose];
}
#include "lua_reg.m"
#ifdef LUA_OBJC_EXTERNAL_STATE_OWNER
typedef struct {
void *ptr;
} LuaStateOwnerRef;
static int gc_external_lua_state_owner(lua_State *L) {
LuaStateOwnerRef *ref = lua_touserdata(L, 1);
if (!ref || !ref->ptr) return 0;
LuaStateOwner *owner = (__bridge LuaStateOwner *)ref->ptr;
if (owner_for_state(L) == owner) {
*(void **)lua_getextraspace(L) = NULL;
}
[owner detachState];
CFRelease(ref->ptr);
ref->ptr = NULL;
return 0;
}
/* External framework hosts own lua_close(). A registry userdata retains the
* non-closing owner for exactly the lifetime of that state and detaches it
* before lua_close releases native userdata. */
static void install_external_lua_state_owner(lua_State *L) {
LuaStateOwner *owner = [[LuaStateOwner alloc] initWithState:L closesState:NO];
LuaStateOwnerRef *ref = lua_newuserdata(L, sizeof(LuaStateOwnerRef));
ref->ptr = (void *)CFBridgingRetain(owner);
if (luaL_newmetatable(L, "lua_objc.async_owner")) {
lua_pushcfunction(L, gc_external_lua_state_owner);
lua_setfield(L, -2, "__gc");
}
lua_setmetatable(L, -2);
lua_setfield(L, LUA_REGISTRYINDEX, "lua_objc.async_owner");
}
#endif
#pragma mark - Timer
static int bridge_timer_after(lua_State *L) {
double delay = luaL_checknumber(L, 1);
luaL_checktype(L, 2, LUA_TFUNCTION);
/* A runtime must install an owner before exposing async bridge calls. */
LuaStateOwner *owner = owner_for_state(L);
if (!owner) return luaL_error(L, "async runtime is not initialized");
LuaReg *reg = lua_reg_create(L, 2, YES);
/* __block so the block can untrack itself after firing. Common modes keep
* Lua work (sleep loops, streamed audio) running while a slider or menu
* tracks the mouse, as SwiftUI's `Timer.publish(on: .main, in: .common)`. */
__block NSTimer *timer = [NSTimer
timerWithTimeInterval:delay
repeats:NO
block:^(NSTimer *t) {
[owner _untrack:timer];
lua_State *callL = lua_reg_live_state(reg);
if (callL && !owner.cancelled) {
lua_reg_push(reg);
lua_objc_pcall(callL, 0, 0, "timer");
}
[reg dispose];
}];
[NSRunLoop.currentRunLoop addTimer:timer forMode:NSRunLoopCommonModes];
[owner trackTimer:timer];
return 0;
}
/* _uptime() -> seconds of monotonic system time, the clock CADisplayLink and
* CACurrentMediaTime count in. Frame loops measure their real interval with
* it: a timer fires late whenever the run loop is busy. */
static int bridge_uptime(lua_State *L) {
lua_pushnumber(L, NSProcessInfo.processInfo.systemUptime);
return 1;
}
#pragma mark - HTTP & JSON
static int bridge_http_get(lua_State *L) {
const char *url = luaL_checkstring(L, 1);
luaL_checktype(L, 2, LUA_TFUNCTION);
/* L may be a coroutine; extraspace inherits from the main thread. */
LuaStateOwner *owner = owner_for_state(L);
if (!owner) return luaL_error(L, "async runtime is not initialized");
LuaReg *reg = lua_reg_create(L, 2, YES);
NSMutableURLRequest *req = [NSMutableURLRequest
requestWithURL:[NSURL URLWithString:[NSString stringWithUTF8String:url]]];
[req setValue:LUA_OBJC_HTTP_USER_AGENT forHTTPHeaderField:@"User-Agent"];
/* __block so the completion block can untrack itself by name. */
__block NSURLSessionDataTask *task = [[NSURLSession sharedSession]
dataTaskWithRequest:req
completionHandler:^(NSData *data, NSURLResponse *resp, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
[owner _untrack:task];
lua_State *callL = lua_reg_live_state(reg);
if (callL && !owner.cancelled) {
lua_reg_push(reg);
if (error) {
lua_pushnil(callL);
lua_pushstring(callL, error.localizedDescription.UTF8String);
} else {
NSString *body = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
lua_pushstring(callL, body.UTF8String ?: "");
lua_pushnil(callL);
}
lua_objc_pcall(callL, 2, 0, "http");
}
[reg dispose];
});
}];
[owner trackTask:task];
[task resume];
return 0;
}
static int bridge_json_parse(lua_State *L) {
const char *json = luaL_checkstring(L, 1);
NSData *data = [[NSString stringWithUTF8String:json]
dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id obj = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error) {
lua_pushnil(L);
lua_pushstring(L, error.localizedDescription.UTF8String);
return 2;
}
push_objc_value(L, obj);
return 1;
}