Tracktion / choc

A collection of header only classes, permissively licensed, to provide basic useful tasks with the bare-minimum of dependencies.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

creating javascript class from c++ (quickjs)

mightgoyardstill opened this issue · comments

loving the choc library! just came across your recent adc talk.

i was wondering if its possible to register classes/objects via choc into our javascript engines?

i've seen you've got a registerFunction but nothing along the lines of a registerClass?

currently using the barebones quickjs library you can register a class like this: https://github.com/bellard/quickjs/blob/master/examples/point.c

wasn't too sure if i missed something?

Thanks! No, you didn't miss anything, I didn't add that to the API.
In my own use-cases I ended up finding that declaring a javascript class which forwards any method calls to global functions worked just fine, and was a place to put extra javascript boilerplate code. So, given that I don't need it myself, I don't have any plans to spend the time adding object support to the wrapper.

ah! so in your case you're just creating your classes via javascript instead of internally on the c++ side? (just reiterating incase i misunderstood anything).

makes sense if so, i guess it clearly defines your javascript API while maintaining a cleaner and simpler project code base.

is there anything thats stopping you from calling these methods as global functions outside of their respective classes?

Yeah, it just seemed simpler to keep the library code small, and not have to implement objects for both QuickJS and Duktape (if it's even possible in duktape)

No, nothing to stop them being called, other than their names being pretty obscure.

i can see why you might've avoided it now.. i spent a horrendous amount of time last night attempting it and only got as far as this. couldn't really figure out a nice way to handle methods while still keeping it quite clean on the user's c++ class declaration

template <typename T>
struct NativeClassBinding
{
    static JSClassID js_class_id;
    static JSClassDef js_class_def;
    // static const JSCFunctionListEntry js_proto_funcs[];

    static JSValue js_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv)
    {
        JSValue obj = JS_NewObjectClass(ctx, js_class_id);
        
        T* native_obj = new T();

        JS_SetOpaque(obj, native_obj);
        return obj;
    }
    
    static void js_finalizer(JSRuntime *rt, JSValue val) 
    {
        T* native_obj = (T*)JS_GetOpaque(val, js_class_id);
        
        if (native_obj) 
        {
            delete native_obj;
        }
    }

    // static JSValue MethodWrapper(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
    // {
    //     T *native_obj = (T*)JS_GetOpaque2(ctx, this_val, js_class_id);
    //     if (!native_obj) {
    //         return JS_EXCEPTION;
    //     }
    //     native_obj->anotherMethod(arg); ??????
    //     return JS_EXCEPTION;
    // }

    // static const JSCFunctionListEntry js_proto_funcs[] =
    // {
    //     JS_CFUNC_DEF("method name", argc, js_method),
    // };

    static void Register(JSContext* ctx, const char* name, int ctorArgs = 0, int magic = 0) 
    {
        JS_NewClassID(&js_class_id);

        js_class_def.class_name = name;
        js_class_def.finalizer = js_finalizer;

        JS_NewClass(JS_GetRuntime(ctx), js_class_id, &js_class_def);

        JSValue global_obj, js_class, proto;

        global_obj = JS_GetGlobalObject(ctx);
        proto = JS_NewObject(ctx);

        // does it have ctorArgs? does it have magic?
        js_class = JS_NewCFunction2(ctx, js_constructor, name, ctorArgs, JS_CFUNC_constructor, magic);

        // does it have methods? how many?
        // JS_SetPropertyFunctionList(ctx, proto, JSCFunctionListEntries, sizeOfEntries);


        JS_SetConstructor(ctx, js_class, proto);
        JS_SetClassProto(ctx, js_class_id, proto);

        JS_SetPropertyStr(ctx, global_obj, name, js_class);
        JS_FreeValue(ctx, global_obj);
    }
};

template <typename T>
JSClassID NativeClassBinding<T>::js_class_id;

template <typename T>
JSClassDef NativeClassBinding<T>::js_class_def;

template <typename T>
void registerNativeClass(JSContext* ctx, const char* name, int ctorArgs = 0, int magic = 0) {
    NativeClassBinding<T>::Register(ctx, name, ctorArgs, magic);
}


class Foo {
public:
    int fooMethod(int x) { return x * 2; }
};


int main() 
{
    JSRuntime *rt;
    JSContext *ctx;

    rt = JS_NewRuntime();
    ctx = JS_NewContext(rt);

    registerNativeClass<Foo>(ctx, "Foo");


    std::string js_code =
    R"(
        'use strict';
        var fooObj = new Foo();
    )";

    JSValue val = JS_Eval(ctx, js_code.c_str(), js_code.length(), "<input>", JS_EVAL_TYPE_GLOBAL);
    
    if (JS_IsException(val))
    {    
        js_std_dump_error(ctx);
    }
    
    JS_FreeValue(ctx, val);
    JS_FreeContext(ctx);
    JS_FreeRuntime(rt);

    return 0;
}

Yes, it's very messy!

If you stick to javascript, you could probably generate a neat system that'd code-gen a javascript class as a string and automatically hook it up to some global functions, and that could outside of this wrapper code. But it's IMHO not worth the hassle, if you just need a handful of classes as part of your API, it's easy enough to code them up manually.

so i've managed something i GUESS is ok, i don't like having to include static JSValue functions in my classes but it seems to be the most cleanest way i could manage in this use case. just thought i'd share incase you were interested, open to suggestions and feel free to close this issue now!

class NativeFunctionBindings {
public:
    void registerMethod(const char* name, int nargs, JSCFunction *func)
    {
        std::cout << "registerMethod called.\n"; // Debug
        jsMethods.push_back(JS_CFUNC_DEF(name, static_cast<uint8_t>(nargs), func));
    }

    const JSCFunctionListEntry* getJSMethods() const
    {
        std::cout << "getJSMethods called.\n"; // Debug
        return jsMethods.data();
    }

    size_t getJSMethodsCount() const 
    {
        std::cout << "getJSMethodsCount called.\n"; // Debug
        return jsMethods.size();
    }
    
protected:
    std::vector<JSCFunctionListEntry> jsMethods;
};


template <typename T>
struct NativeClassBinding
{
    static JSClassID js_class_id;
    static JSClassDef js_class_def;
    static std::vector<JSCFunctionListEntry> jsMethods;

    static JSValue js_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv)
    {
        std::cout << "JS Constructor called.\n"; // Debug

        JSValue obj = JS_NewObjectClass(ctx, js_class_id);

        T* native_obj = new T();

        std::cout << "Native object created.\n"; // Debug

        JS_SetOpaque(obj, native_obj);
        return obj;
    }

    static void js_finalizer(JSRuntime *rt, JSValue val) 
    {
        T* native_obj = (T*)JS_GetOpaque(val, js_class_id);

        if (native_obj) 
        {
            delete native_obj;
        }
    }

    static void registerMethod(const char* name, int nargs, JSCFunction *func)
    {
        std::cout << "Registering Method: " << name << ".\n"; // Debug

        jsMethods.push_back( {name, JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE, JS_DEF_CFUNC, 0, .u = { .func = { static_cast<uint8_t>(nargs), JS_CFUNC_generic, { .generic = func } } }});
    }

    static void ClassRegister(JSContext* ctx, const char* name, int ctorArgs = 0, int magic = 0) 
    {
        std::cout << "Registering class: " << name << ".\n"; // Debug

        JS_NewClassID(&js_class_id);

        js_class_def.class_name = name;
        js_class_def.finalizer = js_finalizer;

        JS_NewClass(JS_GetRuntime(ctx), js_class_id, &js_class_def);

        JSValue global_obj, js_class, proto;

        global_obj = JS_GetGlobalObject(ctx);

        proto = JS_NewObject(ctx);

        js_class = JS_NewCFunction2(
            ctx, js_constructor, name, ctorArgs, JS_CFUNC_constructor, magic);

        JS_SetPropertyFunctionList(
            ctx, proto, jsMethods.data(), jsMethods.size());

        JS_SetConstructor(ctx, js_class, proto);
        JS_SetClassProto(ctx, js_class_id, proto);

        JS_SetPropertyStr(ctx, global_obj, name, js_class);
        JS_FreeValue(ctx, global_obj);
    }
};

template <typename T>
JSClassID NativeClassBinding<T>::js_class_id;

template <typename T>
JSClassDef NativeClassBinding<T>::js_class_def;

template <typename T>
std::vector<JSCFunctionListEntry> NativeClassBinding<T>::jsMethods;

template <typename T>
void registerNativeClass(JSContext* ctx, const char* name, int ctorArgs = 0, int magic = 0)
{
    std::cout << "Registering native class: " << name << ".\n"; // Debug

    T::registerJSMethods();
    NativeClassBinding<T>::ClassRegister(ctx, name, ctorArgs, magic);
}

class Foo : public NativeFunctionBindings
{
public:
    Foo() { std::cout << "Foo() constructor called.\n"; }
    ~Foo() { std::cout << "~Foo() destructor called.\n"; }

    void thisMethod()
    {
        std::cout << "Foo:: this method called\n";
    }
    void thatMethod()
    {
        std::cout << "Foo:: that method called\n";
    }

    static JSValue js_this_method(
        JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) 
    {
        Foo *native_obj = (Foo*)JS_GetOpaque2(
            ctx, this_val, NativeClassBinding<Foo>::js_class_id);

        if (!native_obj)
            return JS_EXCEPTION;

        native_obj->thisMethod();
        return JS_UNDEFINED;
    }

    static JSValue js_that_method(
        JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) 
    {
        Foo *native_obj = (Foo*)JS_GetOpaque2(
            ctx, this_val, NativeClassBinding<Foo>::js_class_id);

        if (!native_obj)
            return JS_EXCEPTION;

        native_obj->thatMethod();
        return JS_UNDEFINED;
    }


    static void registerJSMethods()
    {
        std::cout << "Registering JS methods.\n"; // Debug

        NativeClassBinding<Foo>::registerMethod("thisMethod", 0, js_this_method);
        NativeClassBinding<Foo>::registerMethod("thatMethod", 0, js_that_method);
    }
};


int main() 
{
    JSRuntime *rt;
    JSContext *ctx;

    rt = JS_NewRuntime();
    ctx = JS_NewContext(rt);

    registerNativeClass<Foo>(ctx, "Foo");

    std::string js_code =
    R"(
        'use strict';
        var fooObj = new Foo();
        fooObj.thisMethod();
        fooObj.thatMethod();
    )";

    JSValue val = JS_Eval(
        ctx, js_code.c_str(), js_code.length(), "<input>", JS_EVAL_TYPE_GLOBAL);

    if (JS_IsException(val))
    {    
        js_std_dump_error(ctx);
    }

    JS_FreeValue(ctx, val);

    JS_FreeContext(ctx);
    JS_FreeRuntime(rt);

    return 0;
}

Yes, that's probably the best you can do, but still ends up being at least as much code as the way I've been doing things with JS helper classes!

I'm going to park this issue, but thanks, has been interesting!

i know you parked this issue but thought i'd share some code with you incase you find it interesting..
so i've managed to (in my opinion) improve the binding code and have implemented a base c++ javascript which interfaces with the binding code.

namespace QuickJS
{
    template <typename T>
    struct Binding
    {
        // Each class of JavaScript objects gets its own unique identifier.
        static JSClassID js_class_id;
        // This contains information about the JavaScript class itself, such as its name and finalizer.
        static JSClassDef js_class_def;
        // The list of JavaScript methods associated with the class.
        static std::vector<JSCFunctionListEntry> js_class_methods;
        // The expected number of arguments in the constructor for the class.
        static int constructor_args;

        // This function acts as a JavaScript constructor for the wrapped C++ class.
        // It creates a new C++ object and links it with a new JavaScript object.
        static JSValue constructor(
            JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) 
        {
            // Check if the number of arguments is correct.
            if (argc != Binding<T>::constructor_args)
                return JS_ThrowTypeError(
                    ctx, "Incorrect number of constructor arguments");

            JSValue obj = JS_UNDEFINED;
            JSValue proto;

            // Allocate a new C++ object.
            std::unique_ptr<T> native_obj(new T());

            // If allocation failed, return an exception.
            if (!native_obj)
                return JS_EXCEPTION;

            native_obj->setContext(ctx);
            native_obj->setThisInstance(new_target);
            native_obj->JSConstructor(argc, argv);

            // Retrieve the prototype of the new JavaScript object.
            proto = JS_GetPropertyStr(ctx, new_target, "prototype");
            if (JS_IsException(proto))
            {
                return proto;
            }

            // Create a new JavaScript object with the correct prototype and class.
            obj = JS_NewObjectProtoClass(ctx, proto, js_class_id);
            JS_FreeValue(ctx, proto);
            if (JS_IsException(obj)) 
            {
                return obj;
            }

            // Attach the C++ object to the JavaScript object.
            JS_SetOpaque(obj, native_obj.release());
            return obj;
        }

        // This function acts as a finalizer for the JavaScript object.
        // It deletes the C++ object when the JavaScript object is garbage collected.
        static void finalizer(JSRuntime *rt, JSValue val) 
        {
            // Retrieve the C++ object attached to the JavaScript object.
            T* native_obj = (T*)JS_GetOpaque(val, js_class_id);
            if (native_obj)
                delete native_obj;
        }

        // This function is used to register a new method for the JavaScript class.
        static void registerMethod(const char* name, int magic)
        {
            // Create a new JSCFunctionListEntry for the method and add it to the list of methods.
            js_class_methods.push_back(
                JS_CFUNC_MAGIC_DEF(name, 0, T::JSMethodBinding, static_cast<int16_t>(magic)));
        }

        static void registerNativeMethods()
        {
            // Clear the vector before registering native methods.
            js_class_methods.clear();
            // Retrieve the method map from the inheriting class.
            auto methods = T::setMethods();
            // Register each method with the class wrapper.
            for (auto& method : methods) {
                T::registerMethod(method.first, method.second);
            }
        }

        // This function is used to register the JavaScript class in the global object.
        static void registerPrototype(JSContext* ctx, const char* name) 
        {
            // Create a new class id.
            JS_NewClassID(&js_class_id);

            // Set up the class def.
            js_class_def.class_name = name;
            js_class_def.finalizer = finalizer;

            // Register the class with the JavaScript runtime.
            JS_NewClass(JS_GetRuntime(ctx), js_class_id, &js_class_def);

            JSValue global_obj, js_class, proto;

            // Get the global object.
            global_obj = JS_GetGlobalObject(ctx);

            // Create a new object for the prototype and a new function for the constructor.
            proto = JS_NewObject(ctx);

            // Set the function list for the prototype and set the prototype for the constructor.
            JS_SetPropertyFunctionList(ctx, proto, js_class_methods.data(), static_cast<int>(js_class_methods.size()));
            js_class = JS_NewCFunction2(ctx, constructor, name, 0, JS_CFUNC_constructor, 0);

            JS_SetConstructor(ctx, js_class, proto);
            JS_SetClassProto(ctx, js_class_id, proto);

            // Add the constructor to the global object.
            JS_SetPropertyStr(ctx, global_obj, name, js_class);

            JS_FreeValue(ctx, global_obj);
        }

        static JSValue JSMethodBinding(
            JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) 
        {
            // Get the instance of the C++ object.
            auto *native_obj = (T*)JS_GetOpaque2(ctx, this_val, js_class_id);
            // If we can't get the C++ object, return an exception.
            if (!native_obj) 
                return JS_EXCEPTION;
            // Call the methodBindings function on the C++ object.
            return native_obj->JSMethodCallback(argc, argv, magic);
        }
    };

    template <typename T>
    JSClassID Binding<T>::js_class_id;

    template <typename T>
    JSClassDef Binding<T>::js_class_def;

    template <typename T>
    std::vector<JSCFunctionListEntry> Binding<T>::js_class_methods;

    template <typename T>
    int Binding<T>::constructor_args;
} // namespace QuickJS

namespace QuickJS
{
    template <typename T>
    class Base : public QuickJS::Binding<T> {
    public:
        // ==================================================================
        virtual void JSConstructor(int argc, JSValueConst *argv) = 0; // should probably make this a JSValue type
        virtual ~Base() = 0; // Declare virtual destructor.
        virtual JSValue JSMethodCallback(int argc, JSValueConst *argv, int magic) = 0;
        // ==================================================================
    protected:
        friend struct QuickJS::Binding<T>;
        // ==================================================================
        void setContext(JSContext *ctx) { context = ctx; }
        JSContext* getContext() { return context; }
        // ==================================================================
        void setThisInstance(JSValueConst inst) { instance = inst; }
        JSValueConst getThisInstance() { return instance; }
        // ==================================================================
    private:
        // ==================================================================
        JSContext *context;
        JSValueConst instance;
        // ==================================================================
    };
    template <typename T>
    Base<T>::~Base() = default; // Implement virtual destructor.
} // namespace QuickJS

this is probably the cleanest i'm going to get it! hope you find it interesting (feel free to be as brutal as possible haha)

sorry to keep going on about this, i'm finally getting round to trying out your method of creating a javascript api using choc and was hoping i could ask a couple questions..

you mentioned that forwarding method calls to global functions worked fine for your use-case, and it helped to keep the library code small?

i guess i'm just curious on what type of stuff you would choose to register as global functions, and then what it might look like for you making your javascript class with them? (especially if you're using obscure names!)

i've also been trying to dig into the cmajor source code to see if it has any hints to this but i can't seem to find too much!

my naive approach would be something like this, where anything prefixed with "_" is a global registered function but my obsessive compulsiveness still hates this lol:

class Window
{
    constructor(name, x, y, width, height)
    {
        _setPosition (x, y, width, height);
        _setTitle (name);
        _setResizeable (false);
        _setMinimumSize (width, height);
        _setMaximumSize (1500, 1200);
    }
    toFront()
    {
        _toFront();
    }
};

once again sorry for bringing this up again, just want to know what the best practice would be for something like this

the ONE thing i've noticed in cmajor is that a lot of the javascript classes extend HTMLElement! i'm gonna take a shot in the dark and assume thats where a lot of those global functions are?

I think you're getting a bit muddled there - HTMLElement is part of the browser javascript runtime, none of which is available in an embedded JS engine. The code you're referring to must be our web UI code, not something that we run with QuickJS.

yeah i definitely am, i just assumed there were some parts of cmajor that had quickjs embedded to communicate between the web code and the c++ source. so when you're embedding quickjs and making a bunch of classes are you doing something along the lines of what i'm doing there?

to be a bit more specific on my use cases i'm trying to do plugin hosting and basic "canvas" type stuff via scripting. i originally managed it through the first method i shown you but it made the code base so messy so wondered if your way would make life easier

No.. that's not how it works. If you're running code in a browser, you need to add bindings to the webview, there's no QuickJS involved because the engine is the one in the browser.
In Cmajor we use QuickJS for a totally different purpose - running test scripts on the command line.

ah right! that was another suspicion of where you guys were using quickjs (unfortunately i can't see that stuff though!).
so essentially the stuff here is what is exposed as global functions and the wrapped in these classes? https://cmajor.dev/docs/ScriptFileFormat

thanks for bearing with!

Yep, that's right.