Subj : Re: JS_AddNamedRoot() and finalizer To : Matthew Mondor From : Brendan Eich Date : Thu Aug 26 2004 10:48 am Matthew Mondor wrote: >>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... That's right. >>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, The GC doesn't exactly count references, but yeah -- you have to manage the root set (the subset you know about, that the JS engine can't see -- unlike, say, the globalObject member of a live JSContext, which you can set and the GC can see). > 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. This is all do-able with existing JS API, and multi-threaded embeddings do it often (pool contexts and threads, mapping them 1:1 for recycling). You do mean thread, not process, right? Otherwise, adderess spaces are disjoint and there's no sharing of any data. > 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. The runtime can be per-process. The only reason to have more than one runtime per-process is to simulate multiple VMs. There are valid reasons for multiple VMs in one process, but it's not clear to me you have one yet. > 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)... Yeah, sealing (which requires a throwaway or special context if you pass JS_TRUE for the "deep" parameter) will keep anything in the sealed object graph from changing. But do you want a global that's read/write to be shared among threads? That sounds like a recipe for name collisions and race conditions, at the JS level, even if the engine's JS_THREADSAFE code keeps its data structures coherent and correct. /be > > Thanks again, > Matt > > > ------------------------------------------------------------------------ > > /* $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 */ > } > > > ------------------------------------------------------------------------ > > /* $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 .