Line data Source code
1 0 : /*
2 :
3 : OOJSTimer.m
4 :
5 :
6 : Oolite
7 : Copyright (C) 2004-2013 Giles C Williams and contributors
8 :
9 : This program is free software; you can redistribute it and/or
10 : modify it under the terms of the GNU General Public License
11 : as published by the Free Software Foundation; either version 2
12 : of the License, or (at your option) any later version.
13 :
14 : This program is distributed in the hope that it will be useful,
15 : but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : GNU General Public License for more details.
18 :
19 : You should have received a copy of the GNU General Public License
20 : along with this program; if not, write to the Free Software
21 : Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
22 : MA 02110-1301, USA.
23 :
24 : */
25 :
26 : #import "OOJSTimer.h"
27 : #import "OOJavaScriptEngine.h"
28 : #import "Universe.h"
29 :
30 :
31 : // Minimum allowable interval for repeating timers.
32 0 : #define kMinInterval 0.25
33 :
34 :
35 0 : static JSObject *sTimerPrototype;
36 0 : static JSClass sTimerClass;
37 :
38 :
39 : @interface OOJSTimer (Private)
40 :
41 0 : - (id) initWithDelay:(OOTimeAbsolute)delay
42 : interval:(OOTimeDelta)interval
43 : context:(JSContext *)context
44 : function:(jsval)function
45 : this:(JSObject *)jsThis;
46 :
47 : @end
48 :
49 :
50 : @implementation OOJSTimer
51 :
52 0 : - (id) initWithDelay:(OOTimeAbsolute)delay
53 : interval:(OOTimeDelta)interval
54 : context:(JSContext *)context
55 : function:(jsval)function
56 : this:(JSObject *)jsThis
57 : {
58 : self = [super initWithNextTime:[UNIVERSE getTime] + delay interval:interval];
59 : if (self != nil)
60 : {
61 : NSAssert(OOJSValueIsFunction(context, function), @"Attempt to init OOJSTimer with a function that isn't.");
62 :
63 : _jsThis = jsThis;
64 : OOJSAddGCObjectRoot(context, &_jsThis, "OOJSTimer this");
65 :
66 : _function = function;
67 : OOJSAddGCValueRoot(context, &_function, "OOJSTimer function");
68 :
69 : _jsSelf = JS_NewObject(context, &sTimerClass, sTimerPrototype, NULL);
70 : if (_jsSelf != NULL)
71 : {
72 : if (!JS_SetPrivate(context, _jsSelf, [self retain])) _jsSelf = NULL;
73 : }
74 : if (_jsSelf == NULL)
75 : {
76 : [self release];
77 : return nil;
78 : }
79 :
80 : _owningScript = [[OOJSScript currentlyRunningScript] weakRetain];
81 :
82 : [[NSNotificationCenter defaultCenter] addObserver:self
83 : selector:@selector(deleteJSPointers)
84 : name:kOOJavaScriptEngineWillResetNotification
85 : object:[OOJavaScriptEngine sharedEngine]];
86 : }
87 :
88 : return self;
89 : }
90 :
91 :
92 0 : - (void) deleteJSPointers
93 : {
94 : [self unscheduleTimer];
95 :
96 : if (_jsThis != NULL)
97 : {
98 : _jsThis = NULL;
99 : _function = JSVAL_VOID;
100 :
101 : JSContext *context = OOJSAcquireContext();
102 : JS_RemoveObjectRoot(context, &_jsThis);
103 : JS_RemoveValueRoot(context, &_function);
104 : OOJSRelinquishContext(context);
105 :
106 : [[NSNotificationCenter defaultCenter] removeObserver:self
107 : name:kOOJavaScriptEngineWillResetNotification
108 : object:[OOJavaScriptEngine sharedEngine]];
109 : }
110 : }
111 :
112 :
113 0 : - (void) dealloc
114 : {
115 : [_owningScript release];
116 :
117 : [self deleteJSPointers];
118 :
119 : [super dealloc];
120 : }
121 :
122 :
123 0 : - (NSString *) descriptionComponents
124 : {
125 : NSString *funcName = nil;
126 : JSContext *context = NULL;
127 :
128 : if (JSVAL_IS_VOID(_function) || JSVAL_IS_NULL(_function))
129 : {
130 : return @"invalid";
131 : }
132 :
133 : context = OOJSAcquireContext();
134 : funcName = OOStringFromJSString(context, JS_GetFunctionId(JS_ValueToFunction(context, _function)));
135 : OOJSRelinquishContext(context);
136 :
137 : if (funcName == nil)
138 : {
139 : funcName = @"anonymous";
140 : }
141 :
142 : return [NSString stringWithFormat:@"%@, function: %@", [super descriptionComponents], funcName];
143 : }
144 :
145 :
146 0 : - (NSString *) oo_jsClassName
147 : {
148 : return @"Timer";
149 : }
150 :
151 :
152 0 : - (void) timerFired
153 : {
154 : jsval rval = JSVAL_VOID;
155 : NSString *description = nil;
156 :
157 : OOJavaScriptEngine *engine = [OOJavaScriptEngine sharedEngine];
158 : JSContext *context = OOJSAcquireContext();
159 :
160 : // stop and remove the timer if _jsThis (the first parameter in the constructor) dies.
161 : id object = OOJSNativeObjectFromJSObject(context, _jsThis);
162 : if (object != nil)
163 : {
164 : description = [object oo_jsDescription];
165 : if (description == nil) description = [object description];
166 : }
167 :
168 : if (description == nil)
169 : {
170 : [self unscheduleTimer];
171 : OOJSRelinquishContext(context);
172 : return;
173 : }
174 :
175 : [OOJSScript pushScript:_owningScript];
176 : [engine callJSFunction:_function
177 : forObject:_jsThis
178 : argc:0
179 : argv:NULL
180 : result:&rval];
181 : [OOJSScript popScript:_owningScript];
182 :
183 : OOJSRelinquishContext(context);
184 : }
185 :
186 :
187 0 : - (jsval) oo_jsValueInContext:(JSContext *)context
188 : {
189 : return OBJECT_TO_JSVAL(_jsSelf);
190 : }
191 :
192 : @end
193 :
194 :
195 : static JSBool TimerGetProperty(JSContext *context, JSObject *this, jsid propID, jsval *value);
196 : static JSBool TimerSetProperty(JSContext *context, JSObject *this, jsid propID, JSBool strict, jsval *value);
197 : static void TimerFinalize(JSContext *context, JSObject *this);
198 : static JSBool TimerConstruct(JSContext *context, uintN argc, jsval *vp);
199 :
200 : // Methods
201 : static JSBool TimerStart(JSContext *context, uintN argc, jsval *vp);
202 : static JSBool TimerStop(JSContext *context, uintN argc, jsval *vp);
203 :
204 :
205 : static JSClass sTimerClass =
206 : {
207 : "Timer",
208 : JSCLASS_HAS_PRIVATE,
209 :
210 : JS_PropertyStub, // addProperty
211 : JS_PropertyStub, // delProperty
212 : TimerGetProperty, // getProperty
213 : TimerSetProperty, // setProperty
214 : JS_EnumerateStub, // enumerate
215 : JS_ResolveStub, // resolve
216 : JS_ConvertStub, // convert
217 : TimerFinalize, // finalize
218 : JSCLASS_NO_OPTIONAL_MEMBERS
219 : };
220 :
221 :
222 0 : enum
223 : {
224 : // Property IDs
225 : kTimer_nextTime, // next fire time, double, read/write
226 : kTimer_interval, // interval, double, read/write
227 : kTimer_isRunning // is scheduled, boolean, read-only
228 : };
229 :
230 :
231 0 : static JSPropertySpec sTimerProperties[] =
232 : {
233 : // JS name ID flags
234 : { "interval", kTimer_interval, OOJS_PROP_READWRITE_CB },
235 : { "isRunning", kTimer_isRunning, OOJS_PROP_READONLY_CB },
236 : { "nextTime", kTimer_nextTime, OOJS_PROP_READWRITE_CB },
237 : { 0 }
238 : };
239 :
240 :
241 0 : static JSFunctionSpec sTimerMethods[] =
242 : {
243 : // JS name Function min args
244 : { "toString", OOJSObjectWrapperToString, 0 },
245 : { "start", TimerStart, 0 },
246 : { "stop", TimerStop, 0 },
247 : { 0 }
248 : };
249 :
250 :
251 0 : DEFINE_JS_OBJECT_GETTER(JSTimerGetTimer, &sTimerClass, sTimerPrototype, OOJSTimer);
252 :
253 :
254 0 : void InitOOJSTimer(JSContext *context, JSObject *global)
255 : {
256 : sTimerPrototype = JS_InitClass(context, global, NULL, &sTimerClass, TimerConstruct, 0, sTimerProperties, sTimerMethods, NULL, NULL);
257 : OOJSRegisterObjectConverter(&sTimerClass, OOJSBasicPrivateObjectConverter);
258 : }
259 :
260 :
261 0 : static JSBool TimerGetProperty(JSContext *context, JSObject *this, jsid propID, jsval *value)
262 : {
263 : if (!JSID_IS_INT(propID)) return YES;
264 :
265 : OOJS_NATIVE_ENTER(context)
266 :
267 : OOJSTimer *timer = nil;
268 :
269 : if (EXPECT_NOT(!JSTimerGetTimer(context, this, &timer))) return NO;
270 :
271 : switch (JSID_TO_INT(propID))
272 : {
273 : case kTimer_nextTime:
274 : return JS_NewNumberValue(context, [timer nextTime], value);
275 :
276 : case kTimer_interval:
277 : return JS_NewNumberValue(context, [timer interval], value);
278 :
279 : case kTimer_isRunning:
280 : *value = OOJSValueFromBOOL([timer isScheduled]);
281 : return YES;
282 :
283 : default:
284 : OOJSReportBadPropertySelector(context, this, propID, sTimerProperties);
285 : return NO;
286 : }
287 :
288 : OOJS_NATIVE_EXIT
289 : }
290 :
291 :
292 0 : static JSBool TimerSetProperty(JSContext *context, JSObject *this, jsid propID, JSBool strict, jsval *value)
293 : {
294 : if (!JSID_IS_INT(propID)) return YES;
295 :
296 : OOJS_NATIVE_ENTER(context)
297 :
298 : OOJSTimer *timer = nil;
299 : double fValue;
300 :
301 : if (EXPECT_NOT(!JSTimerGetTimer(context, this, &timer))) return NO;
302 :
303 : switch (JSID_TO_INT(propID))
304 : {
305 : case kTimer_nextTime:
306 : if (JS_ValueToNumber(context, *value, &fValue))
307 : {
308 : if (![timer setNextTime:fValue])
309 : {
310 : OOJSReportWarning(context, @"Ignoring attempt to change next fire time for running timer %@.", timer);
311 : }
312 : return YES;
313 : }
314 : break;
315 :
316 : case kTimer_interval:
317 : if (JS_ValueToNumber(context, *value, &fValue))
318 : {
319 : [timer setInterval:fValue];
320 : return YES;
321 : }
322 : break;
323 :
324 : default:
325 : OOJSReportBadPropertySelector(context, this, propID, sTimerProperties);
326 : return NO;
327 : }
328 :
329 : OOJSReportBadPropertyValue(context, this, propID, sTimerProperties, *value);
330 : return NO;
331 :
332 : OOJS_NATIVE_EXIT
333 : }
334 :
335 :
336 0 : static void TimerFinalize(JSContext *context, JSObject *this)
337 : {
338 : OOJS_PROFILE_ENTER
339 :
340 : // Can't use JSTimerGetTimer() here - potential chicken-and-egg problem manifesting as a crash.
341 : OOJSTimer *timer = (OOJSTimer *)JS_GetPrivate(context, this);
342 :
343 : if (timer != nil)
344 : {
345 : if ([timer isScheduled])
346 : {
347 : OOLogWARN(@"script.javaScript.unrootedTimer", @"Timer %@ is being garbage-collected while still running. You must keep a reference to all running timers, or they will stop unpredictably!", timer);
348 : }
349 : [timer release];
350 : JS_SetPrivate(context, this, NULL);
351 : }
352 :
353 : OOJS_PROFILE_EXIT_VOID
354 : }
355 :
356 :
357 : // new Timer(this : Object, function : Function, delay : Number [, interval : Number]) : Timer
358 0 : static JSBool TimerConstruct(JSContext *context, uintN argc, jsval *vp)
359 : {
360 : OOJS_NATIVE_ENTER(context)
361 :
362 : jsval function = JSVAL_VOID;
363 : double delay;
364 : double interval = -1.0;
365 : OOJSTimer *timer = nil;
366 : JSObject *callbackThis = NULL;
367 :
368 : if (EXPECT_NOT(!JS_IsConstructing(context, vp)))
369 : {
370 : OOJSReportError(context, @"Timer() cannot be called as a function, it must be used as a constructor (as in new Timer(...)).");
371 : return NO;
372 : }
373 :
374 : if (argc < 3)
375 : {
376 : OOJSReportBadArguments(context, nil, @"Timer", argc, OOJS_ARGV, @"Invalid arguments in constructor", @"(object, function, number [, number])");
377 : return NO;
378 : }
379 :
380 : if (!JSVAL_IS_NULL(OOJS_ARGV[0]) && !JSVAL_IS_VOID(OOJS_ARGV[0]))
381 : {
382 : if (!JS_ValueToObject(context, OOJS_ARGV[0], &callbackThis))
383 : {
384 : OOJSReportBadArguments(context, nil, @"Timer", 1, OOJS_ARGV, @"Invalid argument in constructor", @"object");
385 : return NO;
386 : }
387 : }
388 :
389 : function = OOJS_ARGV[1];
390 : if (JS_ValueToFunction(context, function) == NULL)
391 : {
392 : OOJSReportBadArguments(context, nil, @"Timer", 1, OOJS_ARGV + 1, @"Invalid argument in constructor", @"function");
393 : return NO;
394 : }
395 :
396 : if (!JS_ValueToNumber(context, OOJS_ARGV[2], &delay) || isnan(delay))
397 : {
398 : OOJSReportBadArguments(context, nil, @"Timer", 1, OOJS_ARGV + 2, @"Invalid argument in constructor", @"number");
399 : return NO;
400 : }
401 :
402 : // Fourth argument is optional.
403 : if (3 < argc && !JS_ValueToNumber(context, OOJS_ARGV[3], &interval)) interval = -1;
404 :
405 : // Ensure interval is not too small.
406 : if (0.0 < interval && interval < kMinInterval) interval = kMinInterval;
407 :
408 : timer = [[OOJSTimer alloc] initWithDelay:delay
409 : interval:interval
410 : context:context
411 : function:function
412 : this:callbackThis];
413 : if (EXPECT_NOT(!timer)) return NO;
414 :
415 : if (delay >= 0) // Leave in stopped state if delay is negative
416 : {
417 : [timer scheduleTimer];
418 : }
419 : [timer autorelease];
420 : OOJS_RETURN_OBJECT(timer);
421 :
422 : OOJS_NATIVE_EXIT
423 : }
424 :
425 :
426 : // *** Methods ***
427 :
428 : // start() : Boolean
429 0 : static JSBool TimerStart(JSContext *context, uintN argc, jsval *vp)
430 : {
431 : OOJS_NATIVE_ENTER(context)
432 :
433 : OOJSTimer *thisTimer = nil;
434 :
435 : if (EXPECT_NOT(!JSTimerGetTimer(context, OOJS_THIS, &thisTimer))) return NO;
436 :
437 : OOJS_RETURN_BOOL([thisTimer scheduleTimer]);
438 :
439 : OOJS_NATIVE_EXIT
440 : }
441 :
442 :
443 : // stop()
444 0 : static JSBool TimerStop(JSContext *context, uintN argc, jsval *vp)
445 : {
446 : OOJS_NATIVE_ENTER(context)
447 :
448 : OOJSTimer *thisTimer = nil;
449 :
450 : if (EXPECT_NOT(!JSTimerGetTimer(context, OOJS_THIS, &thisTimer))) return NO;
451 :
452 : [thisTimer unscheduleTimer];
453 : OOJS_RETURN_VOID;
454 :
455 : OOJS_NATIVE_EXIT
456 : }
|