Search code examples
quickjs

Use QuickJS to modify preferences and global variables


I am interested in using QuickJS to modify preferences in a C++ app. So, there is a preference variable called int myPref = 0

I want to expose this as a JS variable called jsPref

Then the user can modify this directly via JS using jsPref = 1

This can be extended to a struct - where one can define struct myPref { int a, int b}; which is exposed as a global in JS as jsPref

Inside the JS script, I wish to call

jsPref.a = 1;
jsPref.b = 2;

and the assignments should be reflected in the C struct myPref Sadly, I do not even have sample code on exposing a struct. After searching, I only came up with examples of C/C++ functions exposed as JS methods.


Solution

  • Take a look at e.g. JS_AddIntrinsicAtomics in the QuickJS source code. It uses JS_SetPropertyFunctionList to create an object whose properties are defined by a simple array of structures, with the help of some convenience macros.

    Instead of JS_CFUNC_MAGIC_DEF, you can use JS_CGETSET_DEF or JS_CGETSET_MAGIC_DEF to define custom properties with getters and setters. Since the contents of the JSContext structure are not exposed in the public API, you will have to use JS_GetGlobalObject to access the global object. Otherwise though, what you need to do is substantially the same.

    This should look something like this:

    // those will be called when jsPref.a is accessed
    static JSValue pref_a_get(JSContext *ctx, JSValueConst this_val);
    static JSValue pref_a_set(JSContext *ctx, JSValueConst this_val, JSValueConst value);
    
    // those will be called when jsPref.b or jsPref.c is accessed and receive 123 or 456 respectively in the `magic` parameter
    static JSValue pref_bc_get(JSContext *ctx, JSValueConst this_val, int magic);
    static JSValue pref_bc_set(JSContext *ctx, JSValueConst this_val, JSValueConst value, int magic);
    
    static const JSCFunctionListEntry prefs_obj_props[] = {
        JS_CGETSET_DEF("a", pref_a_get, pref_a_set),
        JS_CGETSET_MAGIC_DEF("b", pref_bc_get, pref_bc_set, 123),
        JS_CGETSET_MAGIC_DEF("c", pref_bc_get, pref_bc_set, 456),
    };
    
    static const JSCFunctionListEntry prefs_obj[] = {
        JS_OBJECT_DEF("jsPref", prefs_obj_props, countof(prefs_obj_props), JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE ),
    };
    
    void create_prefs_obj(JSContext *ctx)
    {
        JSValue globalThis = JS_GetGlobalObject(ctx);
        JS_SetPropertyFunctionList(ctx, globalThis, prefs_obj, countof(prefs_obj));
        JS_FreeValue(ctx, globalThis);
    }
    

    The above defines the struct variation. For a single preference, you can put a JS_CGETSET_DEF definition directly in the prefs_obj, to define an accessor-based property of the global object.

    Note that properties defined using JS_CGETSET_DEF are configurable, which means user code can perform e.g. delete jsPrefs.a; and lose access to the exposed property. If you want to prevent that, you will have to write your own helper macro so that you can set property flags yourself. You may also want to have a look at JS_PreventExtensions, to catch user code attempting to write to a preference that doesn’t exist. If you want to use that, you may need to create the preferences object with JS_NewObject, define its properties with JS_SetPropertyFunctionList, seal it and add it to the global object manually, with JS_DefinePropertyValue, instead of doing everything at once with just one JS_SetPropertyFunctionList call:

    void create_prefs_obj(JSContext *ctx)
    {
        JSValue globalThis = JS_GetGlobalObject(ctx);
    
        // null prototype to avoid namespace clashes
        JSValue prefsObj = JS_NewObjectProto(ctx, JS_NULL);
    
        // define preference properties
        JS_SetPropertyFunctionList(ctx, prefsObj, prefs_obj, countof(prefs_obj));
    
        // catch user code writing to nonexistent properties
        JS_PreventExtensions(ctx, prefsObj);
    
        // expose the preferences object
        JS_DefinePropertyValueStr(ctx, globalThis, "jsPref", prefsObj, PROP_CONFIGURABLE | PROP_WRITABLE);
        JS_FreeValue(ctx, globalThis);
    }