diff --git a/src/lib/libemval.js b/src/lib/libemval.js index 9537680cfb837..d6cf7a7aae575 100644 --- a/src/lib/libemval.js +++ b/src/lib/libemval.js @@ -248,23 +248,33 @@ var LibraryEmVal = { // Leave id 0 undefined. It's not a big deal, but might be confusing // to have null be a valid method caller. - $emval_methodCallers: [undefined], + $emval_methodCallers: {list: [undefined], lookup: {}}, $emval_addMethodCaller__deps: ['$emval_methodCallers'], - $emval_addMethodCaller: (caller) => { - var id = emval_methodCallers.length; - emval_methodCallers.push(caller); + $emval_addMethodCaller: (sig, caller) => { + var id = emval_methodCallers.list.length; + emval_methodCallers.list.push(caller); + emval_methodCallers.lookup[sig] = id; return id; }, _emval_create_invoker__deps: [ - '$emval_addMethodCaller', '$emval_lookupTypes', + '$emval_methodCallers', '$emval_addMethodCaller', '$emval_lookupTypes', '$createNamedFunction', '$emval_returnValue', '$Emval', '$getStringOrSymbol', ], _emval_create_invoker: (argCount, argTypesPtr, kind) => { var GenericWireTypeSize = {{{ 2 * POINTER_SIZE }}}; + var sig = kind.toString(); + for (var i = 0; i < argCount; ++i) { + sig += ',' + {{{ makeGetValue('argTypesPtr', `i*${POINTER_SIZE}`, '*') }}}; + } + var id = emval_methodCallers.lookup[sig]; + if (id) { + return id; + } + var [retType, ...argTypes] = emval_lookupTypes(argCount, argTypesPtr); var toReturnWire = retType.toWireType.bind(retType); var argFromPtr = argTypes.map(type => type.readValueFromPointer.bind(type)); @@ -332,12 +342,12 @@ ${functionBody} var invokerFunction = new Function(Object.keys(captures), functionBody)(...Object.values(captures)); #endif var functionName = `methodCaller<(${argTypes.map(t => t.name)}) => ${retType.name}>`; - return emval_addMethodCaller(createNamedFunction(functionName, invokerFunction)); + return emval_addMethodCaller(sig, createNamedFunction(functionName, invokerFunction)); }, _emval_invoke__deps: ['$getStringOrSymbol', '$emval_methodCallers', '$Emval'], _emval_invoke: (caller, handle, methodName, destructorsRef, args) => { - return emval_methodCallers[caller](handle, methodName, destructorsRef, args); + return emval_methodCallers.list[caller](handle, methodName, destructorsRef, args); }, // Same as `_emval_invoke`, just imported into Wasm under a different return type. diff --git a/system/include/emscripten/val.h b/system/include/emscripten/val.h index 370f1e18e106e..edd6007951b85 100644 --- a/system/include/emscripten/val.h +++ b/system/include/emscripten/val.h @@ -18,10 +18,12 @@ #include // uintptr_t #include #include +#include #include #if __cplusplus >= 202002L #include #include +#include #include #endif @@ -256,6 +258,17 @@ struct WireTypePack { std::array::value> elements; }; +#if __cplusplus >= 202002L +using identity = std::identity; +#else +struct identity { + template + constexpr T&& operator()(T&& t) const noexcept { + return std::forward(t); + } +}; +#endif + } // end namespace internal #define EMSCRIPTEN_SYMBOL(name) \ @@ -341,6 +354,14 @@ class EMBIND_VISIBILITY_DEFAULT val { return val(internal::_emval_get_module_property(name)); } +#if __has_feature(cxx_rtti) + // Create a `val` from a user-defined dynamic `type` that is ABI-compatible with static type `T`: + template + static val dyn(const std::type_info& type, T&& value) { + return internalDynCast(internal::TypeID::get(), &type, nullptr, std::forward(value), internal::identity()); + } +#endif + template explicit val(T&& value, Policies...) { using namespace internal; @@ -516,6 +537,14 @@ class EMBIND_VISIBILITY_DEFAULT val { return internalCall, T>(as_handle(), nullptr, *this); } +#if __has_feature(cxx_rtti) + // Retrieve a user-defined dynamic `type` that is ABI-compatible with static type `T`: + template + auto dyn_as(const std::type_info& type, TransformFn transform) const { + return internalDynCast(&type, internal::TypeID::get(), as_handle(), *this, transform); + } +#endif + // Prefer calling val::typeOf() over val::typeof(), since this form works in both C++11 and GNU++11 build modes. "typeof" is a reserved word in GNU++11 extensions. val typeOf() const { return val(internal::_emval_typeof(as_handle())); @@ -573,42 +602,68 @@ class EMBIND_VISIBILITY_DEFAULT val { template friend val internal::wrapped_extend(const std::string& , const val& ); - template - static Ret internalCall(EM_VAL handle, const char *methodName, Args&&... args) { - static_assert(!std::is_lvalue_reference::value, - "Cannot create a lvalue reference out of a JS value."); - + template + static auto internalDoCall(internal::EM_INVOKER caller, EM_VAL handle, const char* methodName, internal::EM_DESTRUCTORS* destructors, Args&&... args) { using namespace internal; using RetWire = BindingType::WireType; - static constexpr typename Policy::template ArgTypeList argTypes; - thread_local EM_INVOKER mc = _emval_create_invoker(argTypes.getCount(), argTypes.getTypes(), Kind); - WireTypePack argv(std::forward(args)...); - EM_DESTRUCTORS destructors = nullptr; RetWire result; if constexpr (std::is_integral::value && sizeof(RetWire) == 8) { // 64-bit integers can't go through "generic wire type" because double and int64 have different ABI. result = static_cast(_emval_invoke_i64( - mc, + caller, handle, methodName, - &destructors, + destructors, argv)); } else { result = GenericWireTypeConverter::from(_emval_invoke( - mc, + caller, handle, methodName, - &destructors, + destructors, argv)); } + return result; + } + + template + static Ret internalCall(EM_VAL handle, const char *methodName, Args&&... args) { + static_assert(!std::is_lvalue_reference::value, + "Cannot create a lvalue reference out of a JS value."); + + using namespace internal; + + static constexpr typename Policy::template ArgTypeList argTypes; + thread_local EM_INVOKER mc = _emval_create_invoker(argTypes.getCount(), argTypes.getTypes(), Kind); + + EM_DESTRUCTORS destructors = nullptr; + + auto result = internalDoCall(mc, handle, methodName, &destructors, std::forward(args)...); DestructorsRunner rd(destructors); return BindingType::fromWireType(result); } + template + static auto internalDynCast(internal::TYPEID retType, internal::TYPEID argType, EM_VAL handle, Arg&& arg, TransformFn transform) { + static_assert(!std::is_lvalue_reference::value, + "Cannot create a lvalue reference out of a JS value."); + + using namespace internal; + + TYPEID const argTypes[2] { retType, argType }; + EM_INVOKER mc = _emval_create_invoker(2, argTypes, EM_INVOKER_KIND::CAST); + + EM_DESTRUCTORS destructors = nullptr; + + auto result = internalDoCall(mc, handle, nullptr, &destructors, std::forward(arg)); + DestructorsRunner rd(destructors); + return transform(BindingType::fromWireType(result)); + } + template val val_ref(const T& v) const { return val(v); diff --git a/test/embind/test_val_dyn.cpp b/test/embind/test_val_dyn.cpp new file mode 100644 index 0000000000000..338cc45ac19a0 --- /dev/null +++ b/test/embind/test_val_dyn.cpp @@ -0,0 +1,189 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +#if __cplusplus >= 202002L +using identity = std::identity; +#else +struct identity { + template + constexpr T&& operator()(T&& t) const noexcept { return std::forward(t); } +}; +#endif + +enum class Enum1: int { E1 = 1 }; + +enum class Enum2: int { E2 = 2 }; + +struct Struct1 { int n; }; + +struct Struct2 { double d; }; + +class Any { +public: + enum Kind { KIND_ENUM, KIND_STRUCT }; + + Any(std::type_info const& type, int enum_value): + kind_(KIND_ENUM), type_(&type), enum_value_(enum_value) {} + + Any(std::type_info const& type, void const* struct_pointer): + kind_(KIND_STRUCT), type_(&type), struct_pointer_(copyStruct(type, struct_pointer)) {} + + Any(Any const& other): kind_(other.kind_), type_(other.type_) { + switch (kind_) { + case KIND_ENUM: + enum_value_ = other.enum_value_; + break; + case KIND_STRUCT: + struct_pointer_ = copyStruct(*other.type_, other.struct_pointer_); + break; + } + } + + ~Any() { release(); } + + Any& operator=(Any const& other) { + if (&other != this) { + release(); + kind_ = other.kind_; + type_ = other.type_; + switch (kind_) { + case KIND_ENUM: + enum_value_ = other.enum_value_; + break; + case KIND_STRUCT: + struct_pointer_ = copyStruct(*other.type_, other.struct_pointer_); + break; + } + } + return *this; + } + + template T get() const; + + template<> Enum1 get() const { + verify(KIND_ENUM, typeid(Enum1)); + return static_cast(enum_value_); + } + + template<> Enum2 get() const { + verify(KIND_ENUM, typeid(Enum2)); + return static_cast(enum_value_); + } + + template<> Struct1 get() const { + verify(KIND_STRUCT, typeid(Struct1)); + return *static_cast(struct_pointer_); + } + + template<> Struct2 get() const { + verify(KIND_STRUCT, typeid(Struct2)); + return *static_cast(struct_pointer_); + } + + static Any jsNew(std::string id, emscripten::val const& value) { + Kind kind; + std::type_info const* type; + if (id == "Enum1") { + kind = KIND_ENUM; + type = &typeid(Enum1); + } else if (id == "Enum2") { + kind = KIND_ENUM; + type = &typeid(Enum2); + } else if (id == "Struct1") { + kind = KIND_STRUCT; + type = &typeid(Struct1); + } else if (id == "Struct2") { + kind = KIND_STRUCT; + type = &typeid(Struct2); + } else { + throw std::runtime_error("unknown type ID " + id); + } + switch (kind) { + case KIND_ENUM: + return Any(*type, value.dyn_as(*type, identity())); + case KIND_STRUCT: + return value.dyn_as(*type, [&type](void const* p) { return Any(*type, p); }); + } + assert(false); + } + + emscripten::val jsGet() { + switch (kind_) { + case Any::KIND_ENUM: + return emscripten::val::dyn(*type_, enum_value_); + case Any::KIND_STRUCT: + return emscripten::val::dyn(*type_, copyStruct(*type_, struct_pointer_)); + } + assert(false); + } + +private: + static void const* copyStruct(std::type_info const& type, void const* struct_pointer) { + std::size_t n; + if (type == typeid(Struct1)) { + n = sizeof(Struct1); + } else if (type == typeid(Struct2)) { + n = sizeof(Struct2); + } else { + assert(false); + } + return std::memcpy(new unsigned char[n], struct_pointer, n); + } + + void verify(Kind kind, std::type_info const& type) const { + assert(kind_ == kind); + assert(*type_ == type); + } + + void release() { + if (kind_ == KIND_STRUCT) { + delete[] static_cast(struct_pointer_); + } + } + + Kind kind_; + std::type_info const* type_; + union { + int enum_value_; + void const* struct_pointer_; + }; +}; + +} + +int main() { + emscripten::enum_("Enum1").value("E1", Enum1::E1); + emscripten::enum_("Enum2").value("E2", Enum2::E2); + emscripten::value_object("Struct1").field("n", &Struct1::n); + emscripten::value_object("Struct2").field("d", &Struct2::d); + emscripten::class_("Any").constructor(Any::jsNew).function("get", &Any::jsGet); + EM_ASM( + globalThis.AnyEnum1 = new Module.Any("Enum1", Module.Enum1.E1); + globalThis.AnyEnum2 = new Module.Any("Enum2", Module.Enum2.E2); + globalThis.AnyStruct1 = new Module.Any("Struct1", {n: 1}); + globalThis.AnyStruct2 = new Module.Any("Struct2", {d: 1.5}); + globalThis.testGetEnum1 = () => globalThis.AnyEnum1.get() === Module.Enum1.E1; + globalThis.testGetEnum2 = () => globalThis.AnyEnum2.get() === Module.Enum2.E2; + globalThis.testGetStruct1 = () => globalThis.AnyStruct1.get().n === 1; + globalThis.testGetStruct2 = () => globalThis.AnyStruct2.get().d === 1.5; + ); + assert(emscripten::val::global("AnyEnum1").as().get() == Enum1::E1); + assert(emscripten::val::global("AnyEnum2").as().get() == Enum2::E2); + assert(emscripten::val::global("AnyStruct1").as().get().n == 1); + assert(emscripten::val::global("AnyStruct2").as().get().d == 1.5); + assert(emscripten::val::global().call("testGetEnum1")); + assert(emscripten::val::global().call("testGetEnum2")); + assert(emscripten::val::global().call("testGetStruct1")); + assert(emscripten::val::global().call("testGetStruct2")); +} diff --git a/test/test_core.py b/test/test_core.py index 956dc6595b7de..1ff25ce988dbf 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -7636,6 +7636,9 @@ def test_embind_val_coro_propogate_js_error(self): self.cflags += ['-std=c++20', '--bind', '--pre-js=pre.js', '-fexceptions'] self.do_runf('embind/test_val_coro.cpp', 'rejected with: bang from JS promise!\n') + def test_embind_val_dyn(self): + self.do_runf('embind/test_val_dyn.cpp', cflags=['-lembind']) + def test_embind_dynamic_initialization(self): self.cflags += ['-lembind'] self.do_run_in_out_file_test('embind/test_dynamic_initialization.cpp')