Oolite 1.91.0.7646-241128-10e222e
Loading...
Searching...
No Matches
OOJSFrameCallbacks.m
Go to the documentation of this file.
1/*
2
3OOJSFrameCallbacks.m
4
5
6Copyright (C) 2011-2013 Jens Ayton
7
8Permission is hereby granted, free of charge, to any person obtaining a copy
9of this software and associated documentation files (the "Software"), to deal
10in the Software without restriction, including without limitation the rights
11to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12copies of the Software, and to permit persons to whom the Software is
13furnished to do so, subject to the following conditions:
14
15The above copyright notice and this permission notice shall be included in all
16copies or substantial portions of the Software.
17
18THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24SOFTWARE.
25
26*/
27
31
32
33/*
34 By default, tracking IDs are scrambled to discourage people from trying to
35 be clever or making assumptions about them. If DEBUG_FCB_SIMPLE_TRACKING_IDS
36 is non-zero, tracking IDs starting from 1 and rising monotonously are used
37 instead. Additionally, the next ID is reset to 1 when all frame callbacks
38 are removed.
39*/
40#ifndef DEBUG_FCB_SIMPLE_TRACKING_IDS
41#define DEBUG_FCB_SIMPLE_TRACKING_IDS 0
42#endif
43
44#ifndef DEBUG_FCB_VERBOSE_LOGGING
45#define DEBUG_FCB_VERBOSE_LOGGING 0
46#endif
47
48
49
50#if defined (NDEBUG) && DEBUG_FCB_SIMPLE_TRACKING_IDS
51#error Deployment builds may not be built with DEBUG_FCB_SIMPLE_TRACKING_IDS.
52#endif
53
54#if DEBUG_FCB_VERBOSE_LOGGING
55#define FCBLog OOLog
56#define FCBLogIndentIf OOLogIndentIf
57#define FCBLogOutdentIf OOLogOutdentIf
58#else
59#define FCBLog(...) do {} while (0)
60#define FCBLogIndentIf(key) do {} while (0)
61#define FCBLogOutdentIf(key) do {} while (0)
62#endif
63
64
65enum
66{
68
69#if DEBUG_FCB_SIMPLE_TRACKING_IDS
71 kIDIncrement = 1
72#else
73 kIDScrambleMask = 0x2315EB16, // Just a random number.
74 kIDIncrement = 992699 // A large prime number, to produce a non-obvious sequence which still uses all 2^32 values.
75#endif
76};
77
78
79typedef struct
80{
81 jsval callback;
82 uint32 trackingID;
83 uint32 _padding;
85
86
88static NSUInteger sCount; // Number of slots in use.
89static NSUInteger sSpace; // Number of slots allocated.
90static NSUInteger sHighWaterMark; // Number of slots which are GC roots.
91static NSMutableArray *sDeferredOps; // Deferred adds/removes while running.
92static uint32 sNextID;
93static BOOL sRunning;
94
95
96// Methods
97static JSBool GlobalAddFrameCallback(JSContext *context, uintN argc, jsval *vp);
98static JSBool GlobalRemoveFrameCallback(JSContext *context, uintN argc, jsval *vp);
99static JSBool GlobalIsValidFrameCallback(JSContext *context, uintN argc, jsval *vp);
100
101
102// Internals
103static BOOL AddCallback(JSContext *context, jsval callback, uint32 trackingID, NSString **errorString);
104static BOOL GrowCallbackList(JSContext *context, NSString **errorString);
105
106static BOOL GetIndexForTrackingID(uint32 trackingID, NSUInteger *outIndex);
107
108static BOOL RemoveCallbackWithTrackingID(JSContext *context, uint32 trackingID);
109static void RemoveCallbackAtIndex(JSContext *context, NSUInteger index);
110
111static void QueueDeferredOperation(NSString *opType, uint32 trackingID, OOJSValue *value);
112static void RunDeferredOperations(JSContext *context);
113
114
115// MARK: Public
116
117void InitOOJSFrameCallbacks(JSContext *context, JSObject *global)
118{
119 JS_DefineFunction(context, global, "addFrameCallback", GlobalAddFrameCallback, 1, OOJS_METHOD_READONLY);
120 JS_DefineFunction(context, global, "removeFrameCallback", GlobalRemoveFrameCallback, 1, OOJS_METHOD_READONLY);
121 JS_DefineFunction(context, global, "isValidFrameCallback", GlobalIsValidFrameCallback, 1, OOJS_METHOD_READONLY);
122
123#if DEBUG_FCB_SIMPLE_TRACKING_IDS
124 sNextID = 1;
125#else
126 // Set randomish initial ID to catch bad habits.
127 sNextID = [[NSDate date] timeIntervalSinceReferenceDate];
128#endif
129}
130
131
133{
134 NSCAssert1(!sRunning, @"%s cannot be called while frame callbacks are running.", __PRETTY_FUNCTION__);
135
136 if (sCount != 0)
137 {
138 const OOTimeDelta delta = inDeltaT * [UNIVERSE timeAccelerationFactor];
139 JSContext *context = OOJSAcquireContext();
140 jsval deltaVal, result;
141 NSUInteger i;
142
143 if (EXPECT(JS_NewNumberValue(context, delta, &deltaVal)))
144 {
145 // Block mutations.
146 sRunning = YES;
147
148 /*
149 The watchdog timer only fires once per second in deployment builds,
150 but in testrelease builds at least we can keep them on a short leash.
151 */
153
154 for (i = 0; i < sCount; i++)
155 {
156 // TODO: remove out of scope callbacks - post MNSR!
157 JS_CallFunctionValue(context, NULL, sCallbacks[i].callback, 1, &deltaVal, &result);
158 JS_ReportPendingException(context);
159 }
160
162 sRunning = NO;
163
164 if (EXPECT_NOT(sDeferredOps != NULL))
165 {
166 RunDeferredOperations(context);
168 }
169 }
170 OOJSRelinquishContext(context);
171 }
172}
173
174
176{
177 NSCAssert1(!sRunning, @"%s cannot be called while frame callbacks are running.", __PRETTY_FUNCTION__);
178
179 if (sCount != 0)
180 {
181 JSContext *context = OOJSAcquireContext();
182 while (sCount != 0) RemoveCallbackAtIndex(context, sCount - 1);
183 OOJSRelinquishContext(context);
184 }
185}
186
187
188// MARK: Methods
189
190// addFrameCallback(callback : Function) : Number
191static JSBool GlobalAddFrameCallback(JSContext *context, uintN argc, jsval *vp)
192{
193 OOJS_NATIVE_ENTER(context)
194
195 // Get callback argument and verify that it's a function.
196 jsval callback = OOJS_ARGV[0];
197 if (EXPECT_NOT(argc < 1 || !OOJSValueIsFunction(context, callback)))
198 {
199 OOJSReportBadArguments(context, nil, @"addFrameCallback", MIN(argc, 1U), OOJS_ARGV, nil, @"function");
200 return NO;
201 }
202
203 // Assign a tracking ID.
204 uint32 trackingID = sNextID ^ kIDScrambleMask;
206
207 if (EXPECT(!sRunning))
208 {
209 // Add to list immediately.
210 NSString *errorString = nil;
211 if (EXPECT_NOT(!AddCallback(context, callback, trackingID, &errorString)))
212 {
213 OOJSReportError(context, @"%@", errorString);
214 return NO;
215 }
216 }
217 else
218 {
219 // Defer mutations during callback invocation.
220 FCBLog(@"script.frameCallback.debug.add.deferred", @"Deferring addition of frame callback with tracking ID %u.", trackingID);
221 QueueDeferredOperation(@"add", trackingID, [OOJSValue valueWithJSValue:callback inContext:context]);
222 }
223
224 OOJS_RETURN_INT(trackingID);
225
227}
228
229
230// removeFrameCallback(trackingID : Number)
231static JSBool GlobalRemoveFrameCallback(JSContext *context, uintN argc, jsval *vp)
232{
233 OOJS_NATIVE_ENTER(context)
234
235 // Get tracking ID argument.
236 uint32 trackingID;
237 if (EXPECT_NOT(argc < 1 || !JS_ValueToECMAUint32(context, OOJS_ARGV[0], &trackingID)))
238 {
239 OOJSReportBadArguments(context, nil, @"removeFrameCallback", MIN(argc, 1U), OOJS_ARGV, nil, @"frame callback tracking ID");
240 return NO;
241 }
242
243 if (EXPECT(!sRunning))
244 {
245 // Remove it.
246 if (EXPECT_NOT(!RemoveCallbackWithTrackingID(context, trackingID)))
247 {
248 OOJSReportWarning(context, @"removeFrameCallback(): invalid tracking ID.");
249 }
250 }
251 else
252 {
253 // Defer mutations during callback invocation.
254 FCBLog(@"script.frameCallback.debug.remove.deferred", @"Deferring removal of frame callback with tracking ID %u.", trackingID);
255 QueueDeferredOperation(@"remove", trackingID, nil);
256 }
257
259
261}
262
263
264// isValidFrameCallback(trackingID : Number)
265static JSBool GlobalIsValidFrameCallback(JSContext *context, uintN argc, jsval *vp)
266{
267 OOJS_NATIVE_ENTER(context)
268
269 if (EXPECT_NOT(argc < 1))
270 {
271 OOJSReportBadArguments(context, nil, @"isValidFrameCallback", 0, OOJS_ARGV, nil, @"frame callback tracking ID");
272 return NO;
273 }
274
275 // Get tracking ID argument.
276 uint32 trackingID;
277 if (EXPECT_NOT(!JS_ValueToECMAUint32(context, OOJS_ARGV[0], &trackingID)))
278 {
280 }
281
282 NSUInteger index;
283 OOJS_RETURN_BOOL(GetIndexForTrackingID(trackingID, &index));
284
286}
287
288
289// MARK: Internals
290
291static BOOL AddCallback(JSContext *context, jsval callback, uint32 trackingID, NSString **errorString)
292{
293 NSCParameterAssert(context != NULL && JS_IsInRequest(context));
294 NSCParameterAssert(errorString != NULL);
295 NSCAssert1(!sRunning, @"%s cannot be called while frame callbacks are running.", __PRETTY_FUNCTION__);
296
297 if (EXPECT_NOT(sCount == sSpace))
298 {
299 if (!GrowCallbackList(context, errorString)) return NO;
300 }
301
302 FCBLog(@"script.frameCallback.debug.add", @"Adding frame callback with tracking ID %u.", trackingID);
303
304 sCallbacks[sCount].callback = callback;
305 if (sCount >= sHighWaterMark)
306 {
307 // If we haven't used this slot before, root it.
308
309 if (EXPECT_NOT(!OOJSAddGCValueRoot(context, &sCallbacks[sCount].callback, "frame callback")))
310 {
311 *errorString = @"Failed to add GC root for frame callback.";
312 return NO;
313 }
314
316 }
317
318 sCallbacks[sCount].trackingID = trackingID;
319 sCount++;
320
321 return YES;
322}
323
324
325static BOOL GrowCallbackList(JSContext *context, NSString **errorString)
326{
327 NSCParameterAssert(context != NULL && JS_IsInRequest(context));
328 NSCParameterAssert(errorString != NULL);
329
330 NSUInteger newSpace = MAX(sSpace * 2, (NSUInteger)kMinCount);
331
332 CallbackEntry *newCallbacks = calloc(sizeof (CallbackEntry), newSpace);
333 if (newCallbacks == NULL) return NO;
334
335 CallbackEntry *oldCallbacks = sCallbacks;
336
337 // Root and copy occupied slots.
338 NSUInteger newHighWaterMark = sCount;
339 NSUInteger i;
340 for (i = 0; i < newHighWaterMark; i++)
341 {
342 if (EXPECT_NOT(!OOJSAddGCValueRoot(context, &newCallbacks[i].callback, "frame callback")))
343 {
344 // If we can't root them all, we fail; unroot all entries to date, free the buffer and return NO.
345 NSUInteger j;
346 for (j = 0; j < i; j++)
347 {
348 JS_RemoveValueRoot(context, &newCallbacks[j].callback);
349 }
350 free(newCallbacks);
351
352 *errorString = @"Failed to add GC root for frame callback.";
353 return NO;
354 }
355 newCallbacks[i] = oldCallbacks[i];
356 }
357
358 // Unroot old array's slots.
359 for (i = 0; i < sHighWaterMark; i++)
360 {
361 JS_RemoveValueRoot(context, &oldCallbacks[i].callback);
362 }
363
364 // We only rooted the occupied slots, so reset high water mark.
365 sHighWaterMark = newHighWaterMark;
366
367 // Replace array.
368 sCallbacks = newCallbacks;
369 free(oldCallbacks);
370 sSpace = newSpace;
371
372 return YES;
373}
374
375
376static BOOL GetIndexForTrackingID(uint32 trackingID, NSUInteger *outIndex)
377{
378 NSCParameterAssert(outIndex != NULL);
379
380 /* It is assumed that few frame callbacks will be active at once, so a
381 linear search is reasonable. If they become unexpectedly popular, we
382 can switch to a sorted list or a separate lookup table without changing
383 the API.
384 */
385 NSUInteger i;
386 for (i = 0; i < sCount; i++)
387 {
388 if (sCallbacks[i].trackingID == trackingID)
389 {
390 *outIndex = i;
391 return YES;
392 }
393 }
394
395 return NO;
396}
397
398
399static BOOL RemoveCallbackWithTrackingID(JSContext *context, uint32 trackingID)
400{
401 NSCParameterAssert(context != NULL && JS_IsInRequest(context));
402 NSCAssert1(!sRunning, @"%s cannot be called while frame callbacks are running.", __PRETTY_FUNCTION__);
403
404 NSUInteger index = 0;
405 if (GetIndexForTrackingID(trackingID, &index))
406 {
407 RemoveCallbackAtIndex(context, index);
408 return YES;
409 }
410
411 return NO;
412}
413
414
415static void RemoveCallbackAtIndex(JSContext *context, NSUInteger index)
416{
417 NSCParameterAssert(context != NULL && JS_IsInRequest(context));
418 NSCParameterAssert(index < sCount && sCallbacks != NULL);
419 NSCAssert1(!sRunning, @"%s cannot be called while frame callbacks are running.", __PRETTY_FUNCTION__);
420
421 FCBLog(@"script.frameCallback.debug.remove", @"Removing frame callback with tracking ID %u.", sCallbacks[index].trackingID);
422
423 // Overwrite entry to be removed with last entry, and decrement count.
424 sCount--;
425 sCallbacks[index] = sCallbacks[sCount];
426 sCallbacks[sCount].callback = JSVAL_NULL;
427
428#if DEBUG_FCB_SIMPLE_TRACKING_IDS
429 if (sCount == 0)
430 {
431 OOLog(@"script.frameCallback.debug.reset", @"All frame callbacks removed, resetting next ID to 1.");
432 sNextID = 1;
433 }
434#endif
435}
436
437
438static void QueueDeferredOperation(NSString *opType, uint32 trackingID, OOJSValue *value)
439{
440 NSCAssert1(sRunning, @"%s can only be called while frame callbacks are running.", __PRETTY_FUNCTION__);
441
442 if (sDeferredOps == nil) sDeferredOps = [[NSMutableArray alloc] init];
443 [sDeferredOps addObject:[NSDictionary dictionaryWithObjectsAndKeys:
444 opType, @"operation",
445 [NSNumber numberWithInt:trackingID], @"trackingID",
446 value, @"value",
447 nil]];
448}
449
450
451static void RunDeferredOperations(JSContext *context)
452{
453 NSDictionary *operation = nil;
454 NSEnumerator *operationEnum = nil;
455
456 FCBLog(@"script.frameCallback.debug.run-deferred", @"Running %lu deferred frame callback operations.", (long)[sDeferredOps count]);
457 FCBLogIndentIf(@"script.frameCallback.debug.run-deferred");
458
459 for (operationEnum = [sDeferredOps objectEnumerator]; (operation = [operationEnum nextObject]); )
460 {
461 NSString *opType = [operation objectForKey:@"operation"];
462 uint32 trackingID = [operation oo_intForKey:@"trackingID"];
463
464 if ([opType isEqualToString:@"add"])
465 {
466 OOJSValue *callbackObj = [operation objectForKey:@"value"];
467 NSString *errorString = nil;
468
469 if (!AddCallback(context, OOJSValueFromNativeObject(context, callbackObj), trackingID, &errorString))
470 {
471 OOLogWARN(@"script.frameCallback.deferredAdd.failed", @"Deferred frame callback insertion failed: %@", errorString);
472 }
473 }
474 else if ([opType isEqualToString:@"remove"])
475 {
476 RemoveCallbackWithTrackingID(context, trackingID);
477 }
478 }
479
480 FCBLogOutdentIf(@"script.frameCallback.debug.run-deferred");
481}
#define DESTROY(x)
Definition OOCocoa.h:77
#define EXPECT_NOT(x)
#define EXPECT(x)
#define OOJS_NATIVE_ENTER(cx)
#define OOJS_NATIVE_EXIT
#define OOJSStopTimeLimiter()
#define OOJSStartTimeLimiterWithTimeLimit(limit)
void InitOOJSFrameCallbacks(JSContext *context, JSObject *global)
@ kIDIncrement
@ kMinCount
@ kIDScrambleMask
static JSBool GlobalAddFrameCallback(JSContext *context, uintN argc, jsval *vp)
static void RemoveCallbackAtIndex(JSContext *context, NSUInteger index)
#define FCBLog(...)
#define FCBLogOutdentIf(key)
static uint32 sNextID
static BOOL RemoveCallbackWithTrackingID(JSContext *context, uint32 trackingID)
static BOOL GetIndexForTrackingID(uint32 trackingID, NSUInteger *outIndex)
static NSUInteger sCount
void OOJSFrameCallbacksRemoveAll(void)
static NSUInteger sHighWaterMark
static void QueueDeferredOperation(NSString *opType, uint32 trackingID, OOJSValue *value)
#define FCBLogIndentIf(key)
static BOOL sRunning
void OOJSFrameCallbacksInvoke(OOTimeDelta inDeltaT)
static CallbackEntry * sCallbacks
static BOOL AddCallback(JSContext *context, jsval callback, uint32 trackingID, NSString **errorString)
static NSUInteger sSpace
static JSBool GlobalRemoveFrameCallback(JSContext *context, uintN argc, jsval *vp)
static BOOL GrowCallbackList(JSContext *context, NSString **errorString)
static void RunDeferredOperations(JSContext *context)
static NSMutableArray * sDeferredOps
static JSBool GlobalIsValidFrameCallback(JSContext *context, uintN argc, jsval *vp)
void OOJSReportWarning(JSContext *context, NSString *format,...)
#define JS_IsInRequest(context)
OOINLINE jsval OOJSValueFromNativeObject(JSContext *context, id object)
#define OOJS_RETURN_BOOL(v)
OOINLINE BOOL OOJSValueIsFunction(JSContext *context, jsval value)
OOINLINE JSContext * OOJSAcquireContext(void)
void OOJSReportError(JSContext *context, NSString *format,...)
#define OOJS_ARGV
OOINLINE void OOJSRelinquishContext(JSContext *context)
void OOJSReportBadArguments(JSContext *context, NSString *scriptClass, NSString *function, uintN argc, jsval *argv, NSString *message, NSString *expectedArgsDescription)
#define OOJSAddGCValueRoot(context, root, name)
#define OOJS_RETURN_VOID
#define OOJS_RETURN_INT(v)
#define OOJS_METHOD_READONLY
#define OOLogWARN(class, format,...)
Definition OOLogging.h:113
#define OOLog(class, format,...)
Definition OOLogging.h:88
#define MAX(A, B)
Definition OOMaths.h:114
#define MIN(A, B)
Definition OOMaths.h:111
unsigned count
return nil
double OOTimeDelta
Definition OOTypes.h:224