emscripten-core / emscripten

Emscripten: An LLVM-to-WebAssembly Compiler
Other
25.83k stars 3.31k forks source link

Embind: Class function not available on subclasses #18722

Closed JeanChristopheMorinPerso closed 1 year ago

JeanChristopheMorinPerso commented 1 year ago

Please include the following in your bug report:

Version of emscripten/emsdk:

emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.30 (cfe2bdfe2692457cb5f5770672f6e5ccb3ffc2f2)
clang version 16.0.0 (https://github.com/llvm/llvm-project 800f0f1546b2352ba42a4777149afb13cb874fcd)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /home/jcmorin/jcmenv/aswf/OpenTimelineIO-JS-Bindings/emsdk/upstream/bin

Problem:

When binding classes using Embind, functions declared as class functions are not available in subclasses. For example, I have this code:

#include <string>

#include <emscripten/bind.h>

namespace ems = emscripten;

class A
{
public:
    A(){};

    static std::string from_json_string(std::string const& input)
    {
        return input;
    };
};

class B : public A
{
public:
    B(){};
};

class C : public B
{
public:
    C(){};
};

EMSCRIPTEN_BINDINGS(inheritance)
{
    ems::class_<A>("A").constructor<>().class_function(
        "from_json_string",
        &A::from_json_string);

    ems::class_<B, ems::base<A>>("B").constructor<>();

    ems::class_<C, ems::base<B>>("C").constructor<>();
}

and I compile it with emcc -lembind -o lib.js bindings.cpp -s MODULARIZE=1, and then run it in JS:

const factory = require('./lib');

factory().then((lib) => {
    console.log(lib.A.from_json_string("From A"))
    console.log(lib.B.from_json_string("From B"))
    console.log(lib.C.from_json_string("From C"))
})

It raises:

From A
/home/jcmorin/jcmenv/aswf/OpenTimelineIO-JS-Bindings/repros/inheritance/lib.js:165
      throw ex;
      ^

TypeError: lib.B.from_json_string is not a function
    at /home/jcmorin/jcmenv/aswf/OpenTimelineIO-JS-Bindings/repros/inheritance/test.js:5:23

Node.js v19.5.0

We can see that that A.from_json_string() works but B.from_json_string() and C.from_json_string() don't work.

I was kind of expecting class function to be available in sub-classes... Is this a bug or just how it should work?

Thanks a lot!

brendandahl commented 1 year ago

It looks inheritance for static class functions was never implemented. The embind code pre-dates JS classes so it was probably just hard to implement this at the time using prototype inheritance.

JeanChristopheMorinPerso commented 1 year ago

Thanks for the answer. After looking into it, it seems like it might be simple to implement. Or at least I'm hoping that it's simple enough. I'll see how far I can get.

Basically, from what I see, ES5 classes don't inherit class functions by default. You have to do it manually. With ES6 classes, class functions are automatically inherited. https://www.bennadel.com/blog/3300-static-methods-are-inherited-when-using-es6-extends-syntax-in-javascript-and-node-js.htm.

JeanChristopheMorinPerso commented 1 year ago

Example in pure JS:

// Create class A without syntax sugar
var A = function () { }

A.class_function = function () {
    console.log(`${this.name}.class_function`)
}

// Create class B without syntax sugar
var B = function () { }

var instanceProperty = Object.create(A.prototype, {
    constructor: { value: B }
})

B.prototype = instanceProperty

// Test if class_function exists on class B. It should print false.
console.log(B.hasOwnProperty('class_function'))

for (const key in A) {
    if (A.hasOwnProperty(key) && typeof (A[key] == 'function')) {
        B[key] = A[key]
    }
}

// Test if class_function exists on class B. It should print true.
console.log(B.hasOwnProperty('class_function'))

A.class_function()
B.class_function()

class C {
    static class_function() {
        console.log(`${this.name}.class_function`)
    }
}

class D extends C {
}

C.class_function()
D.class_function()

outputs

false
true
A.class_function
B.class_function
C.class_function
D.class_function