Skip to content
This repository
Browse code

Fix crash in TDJSViewCompiler

The compiled function objects were getting GC'd, after which calling them would crash.
  • Loading branch information...
commit d1aadf8003cbeb0ac859f94ecaa500e6ad5a5c9c 1 parent dd70487
Jens Alfke authored January 23, 2013

Showing 1 changed file with 130 additions and 69 deletions. Show diff stats Hide diff stats

  1. 199  Source/TDJSViewCompiler.m
199  Source/TDJSViewCompiler.m
@@ -24,6 +24,20 @@
24 24
 
25 25
 static JSValueRef IDToValue(JSContextRef ctx, id object);
26 26
 static id ValueToID(JSContextRef ctx, JSValueRef value);
  27
+void WarnJSException(JSContextRef context, NSString* warning, JSValueRef exception);
  28
+
  29
+
  30
+@interface TDJSViewCompiler ()
  31
+@property (readonly) JSGlobalContextRef context;
  32
+@end
  33
+
  34
+
  35
+@interface TDJSFunction : NSObject
  36
+- (id) initWithOwner: (TDJSViewCompiler*)owner
  37
+          sourceCode: (NSString*)source
  38
+          paramNames: (NSArray*)paramNames;
  39
+- (JSValueRef) call: (id)param1, ...;
  40
+@end
27 41
 
28 42
 
29 43
 @implementation TDJSViewCompiler
@@ -32,6 +46,9 @@ @implementation TDJSViewCompiler
32 46
 }
33 47
 
34 48
 
  49
+@synthesize context=_context;
  50
+
  51
+
35 52
 // This is a kludge that remembers the emit block passed to the currently active map block.
36 53
 // It's valid only while a map block is running its JavaScript function.
37 54
 static
@@ -46,7 +63,6 @@ static JSValueRef EmitCallback(JSContextRef ctx, JSObjectRef function, JSObjectR
46 63
                                size_t argumentCount, const JSValueRef arguments[],
47 64
                                JSValueRef* exception)
48 65
 {
49  
-    NSLog(@"EMIT!");
50 66
     id key = nil, value = nil;
51 67
     if (argumentCount > 0) {
52 68
         key = ValueToID(ctx, arguments[0]);
@@ -87,34 +103,18 @@ - (TDMapBlock) compileMapFunction: (NSString*)mapSource language: (NSString*)lan
87 103
     if (![language isEqualToString: @"javascript"])
88 104
         return nil;
89 105
 
90  
-    // The source code given is a complete function, like "function(doc){....}".
91  
-    // But JSObjectMakeFunction wants the source code of the _body_ of a function.
92  
-    // Therefore we wrap the given source in an expression that will call it:
93  
-    mapSource = [NSString stringWithFormat: @"(%@)(doc);", mapSource];
94  
-
95 106
     // Compile the function:
96  
-    JSStringRef paramName = JSStringCreateWithCFString(CFSTR("doc"));
97  
-    JSStringRef body = JSStringCreateWithCFString((__bridge CFStringRef)mapSource);
98  
-    JSValueRef exception = NULL;
99  
-    JSObjectRef fn = JSObjectMakeFunction(_context, NULL, 1, &paramName, body, NULL, 1, &exception);
100  
-    JSStringRelease(body);
101  
-    JSStringRelease(paramName);
102  
-
103  
-    if (!fn) {
104  
-        [self warn: @"JS map compile failed" withJSException: exception];
  107
+    TDJSFunction* fn = [[TDJSFunction alloc] initWithOwner: self
  108
+                                                sourceCode: mapSource
  109
+                                                paramNames: @[@"doc"]];
  110
+    if (!fn)
105 111
         return nil;
106  
-    }
107 112
 
108 113
     // Return the TDMapBlock; the code inside will be called when TouchDB wants to run the map fn:
109 114
     TDMapBlock mapBlock = ^(NSDictionary* doc, TDMapEmitBlock emit) {
110  
-        JSValueRef jsDoc = IDToValue(_context, doc);
111 115
         sCurrentEmitBlock = emit;
112  
-        JSValueRef exception = NULL;
113  
-        JSValueRef result = JSObjectCallAsFunction(_context, fn, NULL, 1, &jsDoc, &exception);
  116
+        [fn call: doc];
114 117
         sCurrentEmitBlock = nil;
115  
-        if (!result) {
116  
-            [self warn: @"JS map function failed" withJSException: exception];
117  
-        }
118 118
     };
119 119
     return [mapBlock copy];
120 120
 }
@@ -124,73 +124,125 @@ - (TDReduceBlock) compileReduceFunction: (NSString*)reduceSource language: (NSSt
124 124
     if (![language isEqualToString: @"javascript"])
125 125
         return nil;
126 126
 
127  
-    // The source code given is a complete function, like "function(k,v,re){....}".
128  
-    // But JSObjectMakeFunction wants the source code of the _body_ of a function.
129  
-    // Therefore we wrap the given source in an expression that will call it:
130  
-    reduceSource = [NSString stringWithFormat: @"return (%@)(keys,values,rereduce);", reduceSource];
131  
-
132 127
     // Compile the function:
133  
-    JSStringRef paramNames[3] = {
134  
-        JSStringCreateWithCFString(CFSTR("keys")),
135  
-        JSStringCreateWithCFString(CFSTR("values")),
136  
-        JSStringCreateWithCFString(CFSTR("rereduce"))
137  
-    };
138  
-    JSStringRef body = JSStringCreateWithCFString((__bridge CFStringRef)reduceSource);
139  
-    JSValueRef exception = NULL;
140  
-    JSObjectRef fn = JSObjectMakeFunction(_context, NULL, 3, paramNames, body, NULL, 1, &exception);
141  
-    JSStringRelease(body);
142  
-    JSStringRelease(paramNames[0]);
143  
-    JSStringRelease(paramNames[1]);
144  
-    JSStringRelease(paramNames[2]);
145  
-
146  
-    if (!fn) {
147  
-        [self warn: @"JS reduce compile failed" withJSException: exception];
  128
+    TDJSFunction* fn = [[TDJSFunction alloc] initWithOwner: self
  129
+                                                sourceCode: reduceSource
  130
+                                                paramNames: @[@"keys", @"values", @"rereduce"]];
  131
+    if (!fn)
148 132
         return nil;
149  
-    }
150 133
 
151 134
     // Return the TDReduceBlock; the code inside will be called when TouchDB wants to reduce:
152 135
     TDReduceBlock reduceBlock = ^id(NSArray* keys, NSArray* values, BOOL rereduce) {
153  
-        JSValueRef jsParams[3] = {
154  
-            IDToValue(_context, keys),
155  
-            IDToValue(_context, values),
156  
-            JSValueMakeBoolean(_context, rereduce)
157  
-        };
158  
-        JSValueRef exception = NULL;
159  
-        JSValueRef result = JSObjectCallAsFunction(_context, fn, NULL, 3, jsParams, &exception);
160  
-        if (!result) {
161  
-            [self warn: @"JS reduce function failed" withJSException: exception];
162  
-        }
  136
+        JSValueRef result = [fn call: keys, values, @(rereduce)];
163 137
         return ValueToID(_context, result);
164 138
     };
165 139
     return [reduceBlock copy];
166 140
 }
167 141
 
168 142
 
169  
-- (void) warn: (NSString*)warning withJSException: (JSValueRef)exception {
170  
-    JSStringRef error = JSValueToStringCopy(_context, exception, NULL);
171  
-    CFStringRef cfError = error ? JSStringCopyCFString(NULL, error) : NULL;
172  
-    NSLog(@"*** WARNING: %@: %@", warning, cfError);
173  
-    if (cfError)
174  
-        CFRelease(cfError);
  143
+@end
  144
+
  145
+
  146
+
  147
+
  148
+@implementation TDJSFunction
  149
+{
  150
+    TDJSViewCompiler* _owner;
  151
+    unsigned _nParams;
  152
+    JSObjectRef _fn;
175 153
 }
176 154
 
  155
+- (id) initWithOwner: (TDJSViewCompiler*)owner
  156
+          sourceCode: (NSString*)source
  157
+          paramNames: (NSArray*)paramNames
  158
+{
  159
+    self = [super init];
  160
+    if (self) {
  161
+        _owner = owner;
  162
+        _nParams = (unsigned)paramNames.count;
  163
+
  164
+        // The source code given is a complete function, like "function(doc){....}".
  165
+        // But JSObjectMakeFunction wants the source code of the _body_ of a function.
  166
+        // Therefore we wrap the given source in an expression that will call it:
  167
+        NSString* body = [NSString stringWithFormat: @"return (%@)(%@);",
  168
+                                               source, [paramNames componentsJoinedByString: @","]];
  169
+
  170
+        // Compile the function:
  171
+        JSStringRef jsParamNames[_nParams];
  172
+        for (NSUInteger i = 0; i < _nParams; ++i)
  173
+            jsParamNames[i] = JSStringCreateWithCFString((__bridge CFStringRef)paramNames[i]);
  174
+        JSStringRef jsBody = JSStringCreateWithCFString((__bridge CFStringRef)body);
  175
+        JSValueRef exception;
  176
+        _fn = JSObjectMakeFunction(_owner.context, NULL, _nParams, jsParamNames, jsBody,
  177
+                                   NULL, 1, &exception);
  178
+        JSStringRelease(jsBody);
  179
+        for (NSUInteger i = 0; i < _nParams; ++i)
  180
+            JSStringRelease(jsParamNames[i]);
  181
+        
  182
+        if (!_fn) {
  183
+            WarnJSException(_owner.context, @"JS function compile failed", exception);
  184
+            return nil;
  185
+        }
  186
+        JSValueProtect(_owner.context, _fn);
  187
+    }
  188
+    return self;
  189
+}
  190
+
  191
+- (JSValueRef) call: (id)param1, ... {
  192
+    JSContextRef context = _owner.context;
  193
+    JSValueRef jsParams[_nParams];
  194
+    jsParams[0] = IDToValue(context, param1);
  195
+    if (_nParams > 1) {
  196
+        va_list args;
  197
+        va_start(args, param1);
  198
+        for (NSUInteger i = 1; i < _nParams; ++i)
  199
+            jsParams[i] = IDToValue(context, va_arg(args, id));
  200
+        va_end(args);
  201
+    }
  202
+    JSValueRef exception = NULL;
  203
+    JSValueRef result = JSObjectCallAsFunction(context, _fn, NULL, _nParams, jsParams, &exception);
  204
+    if (!result)
  205
+        WarnJSException(context, @"JS function threw exception", exception);
  206
+    return result;
  207
+}
  208
+
  209
+- (void)dealloc
  210
+{
  211
+    if (_fn)
  212
+        JSValueUnprotect(_owner.context, _fn);
  213
+}
177 214
 
178 215
 @end
179 216
 
180 217
 
  218
+
  219
+
181 220
 // Converts a JSON-compatible NSObject to a JSValue.
182 221
 static JSValueRef IDToValue(JSContextRef ctx, id object) {
183  
-    if (!object)
  222
+    if (object == nil) {
184 223
         return NULL;
185  
-    //FIX: Going through JSON is inefficient.
186  
-    NSData* json = [NSJSONSerialization dataWithJSONObject: object options: 0 error: NULL];
187  
-    if (!json)
188  
-        return NULL;
189  
-    NSString* jsonStr = [[NSString alloc] initWithData: json encoding: NSUTF8StringEncoding];
190  
-    JSStringRef jsStr = JSStringCreateWithCFString((__bridge CFStringRef)jsonStr);
191  
-    JSValueRef value = JSValueMakeFromJSONString(ctx, jsStr);
192  
-    JSStringRelease(jsStr);
193  
-    return value;
  224
+    } else if (object == (id)kCFBooleanFalse || object == (id)kCFBooleanTrue) {
  225
+        return JSValueMakeBoolean(ctx, object == (id)kCFBooleanTrue);
  226
+    } else if (object == [NSNull null]) {
  227
+        return JSValueMakeNull(ctx);
  228
+    } else if ([object isKindOfClass: [NSNumber class]]) {
  229
+        return JSValueMakeNumber(ctx, [object doubleValue]);
  230
+    } else if ([object isKindOfClass: [NSString class]]) {
  231
+        JSStringRef jsStr = JSStringCreateWithCFString((__bridge CFStringRef)object);
  232
+        JSValueRef value = JSValueMakeString(ctx, jsStr);
  233
+        JSStringRelease(jsStr);
  234
+        return value;
  235
+    } else {
  236
+        //FIX: Going through JSON is inefficient.
  237
+        NSData* json = [NSJSONSerialization dataWithJSONObject: object options: 0 error: NULL];
  238
+        if (!json)
  239
+            return NULL;
  240
+        NSString* jsonStr = [[NSString alloc] initWithData: json encoding: NSUTF8StringEncoding];
  241
+        JSStringRef jsStr = JSStringCreateWithCFString((__bridge CFStringRef)jsonStr);
  242
+        JSValueRef value = JSValueMakeFromJSONString(ctx, jsStr);
  243
+        JSStringRelease(jsStr);
  244
+        return value;
  245
+    }
194 246
 }
195 247
 
196 248
 
@@ -209,3 +261,12 @@ static id ValueToID(JSContextRef ctx, JSValueRef value) {
209 261
     NSArray* result = [NSJSONSerialization JSONObjectWithData: data options: 0 error: NULL];
210 262
     return [result objectAtIndex: 0];
211 263
 }
  264
+
  265
+
  266
+void WarnJSException(JSContextRef context, NSString* warning, JSValueRef exception) {
  267
+    JSStringRef error = JSValueToStringCopy(context, exception, NULL);
  268
+    CFStringRef cfError = error ? JSStringCopyCFString(NULL, error) : NULL;
  269
+    NSLog(@"*** WARNING: %@: %@", warning, cfError);
  270
+    if (cfError)
  271
+        CFRelease(cfError);
  272
+}

0 notes on commit d1aadf8

Please sign in to comment.
Something went wrong with that request. Please try again.