Subj : Re: JS_AddNamedRoot() and finalizer To : netscape.public.mozilla.jseng From : Matthew Mondor Date : Thu Aug 26 2004 07:40 am --tThc/1wpZn/ma/RB Content-Type: text/plain; charset=us-ascii Content-Disposition: inline > So you made a stuck root. You should be able to fix that, but first: > Why do you need a global root? Perhaps that my assumption was wrong in needing it. I guess that as long as there exists a context using the API object tied to the global object, there exists a reference and so the class can never be garbage collected... > Assuming you do need a global root: Any code that adds such a root must > have an explicit, mandatory or reference-counting protocol to tell it > when to remove that root. So in this way we would be simulating the GC reference counter or such, and this is for use in special code only then perhaps. I remember reading some document about avoiding some GC pitfalls in which it was used, but I probably did not understand the principle properly yet, and did not have enough time to throughly grasp how the GC works in the SpiderMonkey code. I (finally) had some time to touch again my old test code I had posted earlier, and cleaned up things a bit. Here are my main concerns: A slab-like allocator will be used to manage a pool of processes and associated JS contexts, and this means that the pool of processes must be reusable to execute multiple scripts each without them necessarily destroying the context. I would like js_context_reset() to be enough to ensure that it is possible to reuse the process and thus it's associated context often. The code to destroy the context and runtime would only be called when it is considered that the process is extraneous after statistics show that the server should be allowed to scale down a bit. So important aspect of this was that I had be able to lock API in a way that a script cannot override methods or add new ones to it... However I first should ask this perhaps: Is SpiderMonkey appropriate for such use, or is it overly complicated to properly achieve this kind of security without destroying and creating new context and/or runtime for each script execution. I guess that as necessary I could have a few scealed classes for the "system" methods, a global shared one for some persistent properties among multiple execution (like current API's properties)... Thanks again, Matt --tThc/1wpZn/ma/RB Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename="test.c" /* $Id: test.c,v 1.18 2004/08/26 10:21:37 mmondor Exp $ */ /* Copyright (c) 2004, Matthew Mondor */ /* * TODO: * * - Verify with Brendan Eich: * - Do I really need to use JS_AddNamedRoot() for the API class? * - If reusing the context to execute several other scripts, it is * important that they not be able to add global properties or methods. * This seems to currently work using a custom api_class_property_add(). * This however also required standard properties to be shared * (JS_PROP_SHARED), otherwise api_class_property_add() would be called * and even setting values to existing API system properties would fail in * user scripts. Scealing was also too strict. * I assumed that JS_AddNamedRoot() was required for the API class to * never be freed, so that it can be reused after a call to * js_context_reset(). Perhaps this is not necessary. * - Also, is it normal for the finalizer function to be called twice? * This is what seems to happen here... * * - Finish implementation of test File class. Ideally, this class should be * instantiated by user scripts using new, with a constructor used, and if * possible this class will be used as: * file = new API.File(... */ #include #include #include #include #include #include #include #include #include #include /* Size runtime objects must take to run the GC */ #define GCBYTES 1048576 /* 1MB */ /* Size of stack to allocate for every context */ #define STACKBYTES 8192 /* 8KB */ /* Structure used to load files */ typedef struct { void *data; size_t size; } file_t; /* Structure used to link a context with custom objects we need to perform * some cleanup from before destroying the context. Ideally managed via * mmpool(3) in a real world application for slap management and recycling. * We'll have one of these per process in our pool of processes. */ typedef struct { JSRuntime *rt; JSContext *ctx; JSObject *global, *class_api, *class_file; } js_context_t; /* Only here for a test */ int main(int, char **); /* Internal use */ static JSObject *class_api_init(js_context_t *); static void class_api_destroy(js_context_t *); static JSObject *class_file_init(js_context_t *); static void class_file_destroy(js_context_t *); static file_t *file_load(const char *); static void file_free(file_t *); static JSBool branch_callback(JSContext *, JSScript *); static int api_class_property_add(JSContext *, JSObject *, jsval, jsval *); static void api_class_finalize(JSContext *, JSObject *); static JSBool api_print(JSContext *, JSObject *, uintN, jsval *, jsval *); static JSBool api_file_size(JSContext *, JSObject *, uintN, jsval *, jsval *); static JSBool api_property_get(JSContext *, JSObject *, jsval, jsval *); static JSBool api_property_set(JSContext *, JSObject *, jsval, jsval *); /* Exported API */ js_context_t *js_context_init(size_t, size_t); void js_context_destroy(js_context_t *); void js_context_reset(js_context_t *); /* Defaults for the global class */ static JSClass global_class = { "global", 0, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub }; /* * API class */ /* List of allowed property IDs */ enum api_properties { APIP_PROPERTY1 = 0, APIP_PROPERTY2, APIP_PROPERTY3, APIP_MAX }; /* Defaults for the API class */ static JSClass api_class = { "API", JSCLASS_HAS_PRIVATE, api_class_property_add, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, api_class_finalize }; /* The array of function/method definitions for API class */ static JSFunctionSpec api_functions[] = { { "print", api_print, 1, 0, 0 }, { "fileSize", api_file_size, 1, 0, 0 }, { NULL, NULL, 0, 0, 0 } }; /* The array of property definitions for API class */ static JSPropertySpec api_properties[] = { { "property1", APIP_PROPERTY1, JSPROP_PERMANENT | JSPROP_SHARED, api_property_get, api_property_set }, { "property2", APIP_PROPERTY2, JSPROP_PERMANENT | JSPROP_SHARED, api_property_get, api_property_set }, { "property3", APIP_PROPERTY3, JSPROP_PERMANENT | JSPROP_SHARED | JSPROP_READONLY, api_property_get, api_property_set }, { NULL, 0, 0, NULL, NULL } }; /* Properties array */ static jsval api_property_jsval[APIP_MAX]; /* static JSObject *global; */ static JSBool api_class_protect = JS_FALSE; int main(int argc, char **argv) { file_t *file; js_context_t *cctx; if (argc != 2) { (void) fprintf(stderr, "Usage: test \n"); exit(EXIT_FAILURE); } if ((file = file_load(argv[1])) == NULL) { (void) fprintf(stderr, "Error loading '%s'\n", argv[1]); exit(EXIT_FAILURE); } /* * We always need at least one runtime per process, at least one * context per thread and at least a global object per context * (standard classes, like Date). */ if ((cctx = js_context_init(GCBYTES, STACKBYTES)) == NULL) { file_free(file); (void) fprintf(stderr, "js_context_init()\n"); } /* * This is a very useful and important feature, enable our callback * function which will get called whenever the script branches * backwards, returns from a function or exits. It allows us to * even maintain control in cases where the script loops endlessly. */ (void) JS_SetBranchCallback(cctx->ctx, branch_callback); /* * Now enable addProperty() protection for all classes using our * custom api_class_property_add() function. This will prevent user * code from adding properties or methods to the API class for * instance. * This however requires that properties use the JSPROP_SHARED flag * since addProperty() method would internally get called to create * shadow copies for the runtime otherwise. */ api_class_protect = JS_TRUE; /* * Now execute script loaded into our file_t. * We simplify this process by calling JS_EvaluateScript() which * will first tokenize/compile the result, and then interpret/run it. * Moreover, it allows the script to optionally return a value * directly like if it was a function. * Alternatively, we could use JS_CompileFile() or JS_CompileScript() * to pre-tokenize the script, and JS_ExecuteScript() to interpret it. */ { jsval rval, pval; JSString *str; int i; if (JS_EvaluateScript(cctx->ctx, cctx->global, file->data, file->size, argv[1], 1, &rval)) { str = JS_ValueToString(cctx->ctx, rval); (void) printf("Script result: %s\n", JS_GetStringBytes(str)); /* * Attempt to call JS function "callMe" if the script * created it. */ for (i = 0; i < 10; i++) { pval = INT_TO_JSVAL(i); if (!JS_CallFunctionName(cctx->ctx, cctx->global, "callMe", 1, &pval, &rval)) break; } } } /* Cleanup */ file_free(file); js_context_destroy(cctx); exit(EXIT_SUCCESS); } static JSObject * class_api_init(js_context_t *cctx) { JSObject *class; int i; /* * Create a custom class named API and attach our custom methods * (functions) and properties (variables) to it. This will allow * user scripts to call in this case: API.print(), API.fileSize(). */ /* Create prototype object, child of global object */ if ((cctx->class_api = JS_NewObject(cctx->ctx, &api_class, NULL, cctx->global)) == NULL) { (void) fprintf(stderr, "Error initializing API object\n"); return NULL; } /* Define our methods in the prototype object */ if (!JS_DefineFunctions(cctx->ctx, cctx->class_api, api_functions)) { (void) fprintf(stderr, "Error defining API functions\n"); return NULL; } /* Define our properties in the prototype object */ if (!JS_DefineProperties(cctx->ctx, cctx->class_api, api_properties)) { (void) fprintf(stderr, "Error defining API properties\n"); return NULL; } /* Make sure that GC doesn't free/destroy this object */ /* if (!JS_AddNamedRoot(cctx->ctx, cctx->class_api, "API_Prototype")) { (void) fprintf(stderr, "Error adding API to root\n"); return NULL; } */ /* Finally initialize our new class */ if ((class = JS_InitClass(cctx->ctx, cctx->global, cctx->class_api, &api_class, NULL, 0, NULL, NULL, NULL, NULL)) == NULL) { (void) fprintf(stderr, "Error initializing API class\n"); /* JS_RemoveRoot(cctx->ctx, cctx->class_api); */ return NULL; } /* * Make sure that noone can delete our class, setting the permanent * and read only flags. */ { uintN prop_flags; JSBool prop_found; if (JS_GetPropertyAttributes(cctx->ctx, cctx->global, "API", &prop_flags, &prop_found) && prop_found) (void) JS_SetPropertyAttributes(cctx->ctx, cctx->global, "API", prop_flags | JSPROP_PERMANENT | JSPROP_READONLY, &prop_found); else { (void) fprintf(stderr, "Error modifying API attributes\n"); /* JS_RemoveRoot(cctx->ctx, cctx->class_api); */ return NULL; } } /* * Set object private data for our test properties. * different types to be assigned. Once refined, we shall allocate * the private data so that each context has it's own instance. */ for (i = 0; i < APIP_MAX; i++) api_property_jsval[i] = INT_TO_JSVAL(-1); (void) JS_SetPrivate(cctx->ctx, class, api_property_jsval); return cctx->class_api; } static void class_api_destroy(js_context_t *cctx) { /* JS_RemoveRoot(cctx->ctx, cctx->class_api); */ } /* ARGSUSED */ static JSObject * class_file_init(js_context_t *cctx) { /* XXX */ return NULL; } /* ARGSUSED */ static void class_file_destroy(js_context_t *cctx) { /* NOOP */ } /* Loads specified file and returns a file_t pointer. */ static file_t * file_load(const char *filename) { int fd; struct stat st; file_t *file; assert(filename != NULL); if ((fd = open(filename, O_RDONLY)) != -1) { if (fstat(fd, &st) == 0) { if ((file = malloc(sizeof(file_t))) != NULL) { file->size = (size_t)st.st_size; if ((file->data = mmap(NULL, file->size, PROT_READ, MAP_FILE, fd, 0)) != MAP_FAILED) { (void) close(fd); return file; } free(file); } } (void) close(fd); } return NULL; } /* Frees a file_t which was returned by a previous file_load() call. */ static void file_free(file_t *file) { assert (file != NULL); (void) munmap(file->data, file->size); free(file); } /* * This function is called during the execution of the script so that we can * remain in control of the application. If we only allow the scripts to * define functions for callbacks, we can use the first instance if this event * to abort the script if wanted, as well. We can then set an alternative * callback function and execute the script provided functions at specific * events. Of course, it also would be possible to use setitimer(2) to have * a SIGALRM signal trigger a function at regular set intervals. */ /* ARGSUSED */ static JSBool branch_callback(JSContext *ctx, JSScript *script) { jsval pval/*, rval*/; static int count = 0; (void) printf("branch_callback()\n"); /* Call callMe() script-provided handler if any, as a test */ pval = INT_TO_JSVAL(count++); /* XXX global object cannot be obtained :( so check how to solve this * later, we can surely obtain it via the context. (void) JS_CallFunctionName(ctx, global, "callMe", 1, &pval, &rval); */ /* Returning JS_FALSE here aborts the script */ return JS_TRUE; } /* * This function is used to disable adding properties to our API object, after * our own code added the necessary properties. We set protect to TRUE when * ready. */ static int api_class_property_add(JSContext *ctx, JSObject *obj, jsval v1, jsval *v2) { return !api_class_protect; } /* ARGSUSED */ static void api_class_finalize(JSContext *ctx, JSObject *obj) { printf("test!\n"); } /* A test function which we want to provide our JS API with. */ static JSBool api_print(JSContext *ctx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) { JSString *str; /* * Arguments are in argc, argv and we must return our return value * into rval. */ /* * If we do not perform this check, the caller is allowed to supply * more arguments than we expect, or to supply none at all, in which * case the argument we expect is undefined. Returning JS_FALSE here * causes script execution to be aborted. Note that JS_ValueToString() * always returns a JSString pointer, and that we do not need to free * the JSString buffer, since it becomes subject to the GC. This also * means that if we needed to store the value for some time we would * either need to tag it to not be subject to the GC, or to copy the * result in an application-specific buffer. */ if (argc != 1) return JS_FALSE; str = JS_ValueToString(ctx, *argv); (void) printf("%s", JS_GetStringBytes(str)); return JS_TRUE; } /* * Another test function, which allows the script to obtain a result from us. */ static JSBool api_file_size(JSContext *ctx, JSObject *obj, uintN argc, jsval *argv, jsval *ret) { JSString *str; struct stat st; ssize_t size; if (argc != 1) return JS_FALSE; str = JS_ValueToString(ctx, *argv); if (stat(JS_GetStringBytes(str), &st) == 0) size = (ssize_t)st.st_size; else size = -1; *ret = INT_TO_JSVAL((int)size); return JS_TRUE; } /* * Functions to get and set API.property, as an example. It is possible to use * the same functions for multiple peoperties, and to use the 8-bit unsigned * unique ID of the property to distinguish them. It is possible to use object- * specific private data as well, using JS_SetPrivate() and JS_GetPrivate(). * In the case of read-only properties, the api_property_set() function is * simply not called. */ static JSBool api_property_get(JSContext *ctx, JSObject *obj, jsval id, jsval *vp) { jsval *val; int p; p = JSVAL_TO_INT(id); assert(p > -1 && p < APIP_MAX); val = JS_GetPrivate(ctx, obj); *vp = val[p]; return JS_TRUE; } static JSBool api_property_set(JSContext *ctx, JSObject *obj, jsval id, jsval *vp) { jsval *val; int p; p = JSVAL_TO_INT(id); assert(p > -1 && p < APIP_MAX); val = JS_GetPrivate(ctx, obj); val[p] = *vp; return JS_TRUE; } /* Exported API */ js_context_t * js_context_init(size_t gc_size, size_t stack_size) { js_context_t *cctx; if ((cctx = malloc(sizeof(js_context_t))) == NULL || (cctx->rt = JS_NewRuntime(gc_size)) == NULL || (cctx->ctx = JS_NewContext(cctx->rt, stack_size)) == NULL || (cctx->global = JS_NewObject(cctx->ctx, &global_class, NULL, NULL)) == NULL || !JS_InitStandardClasses(cctx->ctx, cctx->global) || (cctx->class_api = class_api_init(cctx)) == NULL /*|| (cctx->file = class_file_init(cctx)) == NULL*/) { /* An error, free any partially allocated resources */ if (cctx != NULL) js_context_destroy(cctx); return NULL; } return cctx; } void js_context_destroy(js_context_t *cctx) { /* if (cctx->class_file != NULL) class_file_destroy(cctx); */ if (cctx->class_api != NULL) class_api_destroy(cctx); if (cctx->ctx != NULL) JS_DestroyContext(cctx->ctx); if (cctx->rt != NULL) JS_DestroyRuntime(cctx->rt); free(cctx); } /* ARGSUSED */ void js_context_reset(js_context_t *cctx) { /* NOOP */ } --tThc/1wpZn/ma/RB Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename="test.js" /* $Id: test.js,v 1.8 2004/07/26 07:22:12 mmondor Exp $ */ /* * The following test should not succeed on API class */ /* {{ */ /* function test(s) { API.print(s); } API.test = test; API.test("Successfully added method to API class!\n"); API.test2 = "Successfully added property to API class!\n"; API.test(API.test2 + "\n"); */ /* }} */ /* * These tests should succeed, however. */ API.print("API.property1 = " + API.property1 + "\n"); API.print("API.property2 = " + API.property2 + "\n"); API.print("API.property3 = " + API.property3 + "\n\n"); /* * XXX * These fail, even though these are permanent and read/write properties. * JS_SealObject() seems to be converting all read/write properties to * report an error if setProperty() method is called. However, interestingly * enough, read/only properties report no error. However, they obviously * are not modified despite attempts to assign them new values. */ API.property1 = 'Hello'; API.property2 = 911; /* * Following statement should fail, but is simply internally ignored at least * (the setProperty() method is not called). */ API.property3 = 'READONLY!'; API.print("API.property1 = " + API.property1 + "\n"); API.print("API.property2 = " + API.property2 + "\n"); API.print("API.property3 = " + API.property3 + "\n\n"); /* * Perform a test loop, during which callMe() should be called by the * application code, via the break callback handler function. */ for (i = 0; i < 3; i++) { API.print("NetBSD Kernel size is " + (API.fileSize("/netbsd") / 1024 / 1024) + "MB\n"); API.print("END\n"); } API.print("\n"); /* * Will be called by our application if we create it */ function callMe(n) { API.print("CallMe(" + n + ")!\n"); } /* * Interesting feature where we can return a value explicitely, optionally */ 10 --tThc/1wpZn/ma/RB-- .