Skip to content

Commit 5c7a48a

Browse files
committed
[emval] Support dynamically-typed conversion to/from val
...for cases where a dynamically given user-defined type is known to be ABI-compatible with a statically given wire type (e.g., an enum type being compatible with its underlying type, or a struct type being compatible with `const void*`; for the latter, a returned `const void*` needs to be transformed somehow into the desired dynamic type before destructors are run and the pointed-to object is destroyed).
1 parent ec93a04 commit 5c7a48a

File tree

4 files changed

+277
-20
lines changed

4 files changed

+277
-20
lines changed

src/lib/libemval.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,23 +248,33 @@ var LibraryEmVal = {
248248
249249
// Leave id 0 undefined. It's not a big deal, but might be confusing
250250
// to have null be a valid method caller.
251-
$emval_methodCallers: [undefined],
251+
$emval_methodCallers: {list: [undefined], lookup: {}},
252252
253253
$emval_addMethodCaller__deps: ['$emval_methodCallers'],
254-
$emval_addMethodCaller: (caller) => {
255-
var id = emval_methodCallers.length;
256-
emval_methodCallers.push(caller);
254+
$emval_addMethodCaller: (sig, caller) => {
255+
var id = emval_methodCallers.list.length;
256+
emval_methodCallers.list.push(caller);
257+
emval_methodCallers.lookup[sig] = id;
257258
return id;
258259
},
259260
260261
_emval_create_invoker__deps: [
261-
'$emval_addMethodCaller', '$emval_lookupTypes',
262+
'$emval_methodCallers', '$emval_addMethodCaller', '$emval_lookupTypes',
262263
'$createNamedFunction', '$emval_returnValue',
263264
'$Emval', '$getStringOrSymbol',
264265
],
265266
_emval_create_invoker: (argCount, argTypesPtr, kind) => {
266267
var GenericWireTypeSize = {{{ 2 * POINTER_SIZE }}};
267268
269+
var sig = kind.toString();
270+
for (var i = 0; i < argCount; ++i) {
271+
sig += ',' + {{{ makeGetValue('argTypesPtr', `i*${POINTER_SIZE}`, '*') }}};
272+
}
273+
var id = emval_methodCallers.lookup[sig];
274+
if (id) {
275+
return id;
276+
}
277+
268278
var [retType, ...argTypes] = emval_lookupTypes(argCount, argTypesPtr);
269279
var toReturnWire = retType.toWireType.bind(retType);
270280
var argFromPtr = argTypes.map(type => type.readValueFromPointer.bind(type));
@@ -332,12 +342,12 @@ ${functionBody}
332342
var invokerFunction = new Function(Object.keys(captures), functionBody)(...Object.values(captures));
333343
#endif
334344
var functionName = `methodCaller<(${argTypes.map(t => t.name)}) => ${retType.name}>`;
335-
return emval_addMethodCaller(createNamedFunction(functionName, invokerFunction));
345+
return emval_addMethodCaller(sig, createNamedFunction(functionName, invokerFunction));
336346
},
337347
338348
_emval_invoke__deps: ['$getStringOrSymbol', '$emval_methodCallers', '$Emval'],
339349
_emval_invoke: (caller, handle, methodName, destructorsRef, args) => {
340-
return emval_methodCallers[caller](handle, methodName, destructorsRef, args);
350+
return emval_methodCallers.list[caller](handle, methodName, destructorsRef, args);
341351
},
342352
343353
// Same as `_emval_invoke`, just imported into Wasm under a different return type.

system/include/emscripten/val.h

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
#include <cstdint> // uintptr_t
1919
#include <vector>
2020
#include <type_traits>
21+
#include <typeinfo>
2122
#include <pthread.h>
2223
#if __cplusplus >= 202002L
2324
#include <coroutine>
2425
#include <exception>
26+
#include <functional>
2527
#include <variant>
2628
#endif
2729

@@ -256,6 +258,17 @@ struct WireTypePack {
256258
std::array<GenericWireType, PackSize<Args...>::value> elements;
257259
};
258260

261+
#if __cplusplus >= 202002L
262+
using identity = std::identity;
263+
#else
264+
struct identity {
265+
template<typename T>
266+
constexpr T&& operator()(T&& t) const noexcept {
267+
return std::forward<T>(t);
268+
}
269+
};
270+
#endif
271+
259272
} // end namespace internal
260273

261274
#define EMSCRIPTEN_SYMBOL(name) \
@@ -341,6 +354,14 @@ class EMBIND_VISIBILITY_DEFAULT val {
341354
return val(internal::_emval_get_module_property(name));
342355
}
343356

357+
#if __has_feature(cxx_rtti)
358+
// Create a `val` from a user-defined dynamic `type` that is ABI-compatible with static type `T`:
359+
template<typename T>
360+
static val dyn(const std::type_info& type, T&& value) {
361+
return internalDynCast<val>(internal::TypeID<val>::get(), &type, nullptr, std::forward<T>(value), internal::identity());
362+
}
363+
#endif
364+
344365
template<typename T, typename... Policies>
345366
explicit val(T&& value, Policies...) {
346367
using namespace internal;
@@ -516,6 +537,14 @@ class EMBIND_VISIBILITY_DEFAULT val {
516537
return internalCall<EM_INVOKER_KIND::CAST, WithPolicies<Policies...>, T>(as_handle(), nullptr, *this);
517538
}
518539

540+
#if __has_feature(cxx_rtti)
541+
// Retrieve a user-defined dynamic `type` that is ABI-compatible with static type `T`:
542+
template<typename T, typename TransformFn>
543+
auto dyn_as(const std::type_info& type, TransformFn transform) const {
544+
return internalDynCast<T>(&type, internal::TypeID<val>::get(), as_handle(), *this, transform);
545+
}
546+
#endif
547+
519548
// 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.
520549
val typeOf() const {
521550
return val(internal::_emval_typeof(as_handle()));
@@ -573,42 +602,68 @@ class EMBIND_VISIBILITY_DEFAULT val {
573602
template<typename WrapperType>
574603
friend val internal::wrapped_extend(const std::string& , const val& );
575604

576-
template<internal::EM_INVOKER_KIND Kind, typename Policy, typename Ret, typename... Args>
577-
static Ret internalCall(EM_VAL handle, const char *methodName, Args&&... args) {
578-
static_assert(!std::is_lvalue_reference<Ret>::value,
579-
"Cannot create a lvalue reference out of a JS value.");
580-
605+
template<typename Ret, typename... Args>
606+
static auto internalDoCall(internal::EM_INVOKER caller, EM_VAL handle, const char* methodName, internal::EM_DESTRUCTORS* destructors, Args&&... args) {
581607
using namespace internal;
582608

583609
using RetWire = BindingType<Ret>::WireType;
584610

585-
static constexpr typename Policy::template ArgTypeList<Ret, Args...> argTypes;
586-
thread_local EM_INVOKER mc = _emval_create_invoker(argTypes.getCount(), argTypes.getTypes(), Kind);
587-
588611
WireTypePack<Args...> argv(std::forward<Args>(args)...);
589-
EM_DESTRUCTORS destructors = nullptr;
590612

591613
RetWire result;
592614
if constexpr (std::is_integral<RetWire>::value && sizeof(RetWire) == 8) {
593615
// 64-bit integers can't go through "generic wire type" because double and int64 have different ABI.
594616
result = static_cast<RetWire>(_emval_invoke_i64(
595-
mc,
617+
caller,
596618
handle,
597619
methodName,
598-
&destructors,
620+
destructors,
599621
argv));
600622
} else {
601623
result = GenericWireTypeConverter<RetWire>::from(_emval_invoke(
602-
mc,
624+
caller,
603625
handle,
604626
methodName,
605-
&destructors,
627+
destructors,
606628
argv));
607629
}
630+
return result;
631+
}
632+
633+
template<internal::EM_INVOKER_KIND Kind, typename Policy, typename Ret, typename... Args>
634+
static Ret internalCall(EM_VAL handle, const char *methodName, Args&&... args) {
635+
static_assert(!std::is_lvalue_reference<Ret>::value,
636+
"Cannot create a lvalue reference out of a JS value.");
637+
638+
using namespace internal;
639+
640+
static constexpr typename Policy::template ArgTypeList<Ret, Args...> argTypes;
641+
thread_local EM_INVOKER mc = _emval_create_invoker(argTypes.getCount(), argTypes.getTypes(), Kind);
642+
643+
EM_DESTRUCTORS destructors = nullptr;
644+
645+
auto result = internalDoCall<Ret>(mc, handle, methodName, &destructors, std::forward<Args>(args)...);
608646
DestructorsRunner rd(destructors);
609647
return BindingType<Ret>::fromWireType(result);
610648
}
611649

650+
template<typename Ret, typename Arg, typename TransformFn>
651+
static auto internalDynCast(internal::TYPEID retType, internal::TYPEID argType, EM_VAL handle, Arg&& arg, TransformFn transform) {
652+
static_assert(!std::is_lvalue_reference<Ret>::value,
653+
"Cannot create a lvalue reference out of a JS value.");
654+
655+
using namespace internal;
656+
657+
TYPEID const argTypes[2] { retType, argType };
658+
EM_INVOKER mc = _emval_create_invoker(2, argTypes, EM_INVOKER_KIND::CAST);
659+
660+
EM_DESTRUCTORS destructors = nullptr;
661+
662+
auto result = internalDoCall<Ret>(mc, handle, nullptr, &destructors, std::forward<Arg>(arg));
663+
DestructorsRunner rd(destructors);
664+
return transform(BindingType<Ret>::fromWireType(result));
665+
}
666+
612667
template<typename T>
613668
val val_ref(const T& v) const {
614669
return val(v);

test/embind/test_val_dyn.cpp

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#include <cassert>
2+
#include <cstddef>
3+
#include <cstring>
4+
#include <functional>
5+
#include <stdexcept>
6+
#include <string>
7+
#include <typeinfo>
8+
#include <utility>
9+
10+
#include <emscripten.h>
11+
#include <emscripten/bind.h>
12+
13+
namespace {
14+
15+
#if __cplusplus >= 202002L
16+
using identity = std::identity;
17+
#else
18+
struct identity {
19+
template<typename T>
20+
constexpr T&& operator()(T&& t) const noexcept { return std::forward<T>(t); }
21+
};
22+
#endif
23+
24+
enum class Enum1: int { E1 = 1 };
25+
26+
enum class Enum2: int { E2 = 2 };
27+
28+
struct Struct1 { int n; };
29+
30+
struct Struct2 { double d; };
31+
32+
class Any {
33+
public:
34+
enum Kind { KIND_ENUM, KIND_STRUCT };
35+
36+
Any(std::type_info const& type, int enum_value):
37+
kind_(KIND_ENUM), type_(&type), enum_value_(enum_value) {}
38+
39+
Any(std::type_info const& type, void const* struct_pointer):
40+
kind_(KIND_STRUCT), type_(&type), struct_pointer_(copyStruct(type, struct_pointer)) {}
41+
42+
Any(Any const& other): kind_(other.kind_), type_(other.type_) {
43+
switch (kind_) {
44+
case KIND_ENUM:
45+
enum_value_ = other.enum_value_;
46+
break;
47+
case KIND_STRUCT:
48+
struct_pointer_ = copyStruct(*other.type_, other.struct_pointer_);
49+
break;
50+
}
51+
}
52+
53+
~Any() { release(); }
54+
55+
Any& operator=(Any const& other) {
56+
if (&other != this) {
57+
release();
58+
kind_ = other.kind_;
59+
type_ = other.type_;
60+
switch (kind_) {
61+
case KIND_ENUM:
62+
enum_value_ = other.enum_value_;
63+
break;
64+
case KIND_STRUCT:
65+
struct_pointer_ = copyStruct(*other.type_, other.struct_pointer_);
66+
break;
67+
}
68+
}
69+
return *this;
70+
}
71+
72+
template<typename T> T get() const;
73+
74+
template<> Enum1 get() const {
75+
verify(KIND_ENUM, typeid(Enum1));
76+
return static_cast<Enum1>(enum_value_);
77+
}
78+
79+
template<> Enum2 get() const {
80+
verify(KIND_ENUM, typeid(Enum2));
81+
return static_cast<Enum2>(enum_value_);
82+
}
83+
84+
template<> Struct1 get() const {
85+
verify(KIND_STRUCT, typeid(Struct1));
86+
return *static_cast<Struct1 const*>(struct_pointer_);
87+
}
88+
89+
template<> Struct2 get() const {
90+
verify(KIND_STRUCT, typeid(Struct2));
91+
return *static_cast<Struct2 const*>(struct_pointer_);
92+
}
93+
94+
static Any jsNew(std::string id, emscripten::val const& value) {
95+
Kind kind;
96+
std::type_info const* type;
97+
if (id == "Enum1") {
98+
kind = KIND_ENUM;
99+
type = &typeid(Enum1);
100+
} else if (id == "Enum2") {
101+
kind = KIND_ENUM;
102+
type = &typeid(Enum2);
103+
} else if (id == "Struct1") {
104+
kind = KIND_STRUCT;
105+
type = &typeid(Struct1);
106+
} else if (id == "Struct2") {
107+
kind = KIND_STRUCT;
108+
type = &typeid(Struct2);
109+
} else {
110+
throw std::runtime_error("unknown type ID " + id);
111+
}
112+
switch (kind) {
113+
case KIND_ENUM:
114+
return Any(*type, value.dyn_as<int>(*type, identity()));
115+
case KIND_STRUCT:
116+
return value.dyn_as<void const*>(*type, [&type](void const* p) { return Any(*type, p); });
117+
}
118+
assert(false);
119+
}
120+
121+
emscripten::val jsGet() {
122+
switch (kind_) {
123+
case Any::KIND_ENUM:
124+
return emscripten::val::dyn(*type_, enum_value_);
125+
case Any::KIND_STRUCT:
126+
return emscripten::val::dyn(*type_, copyStruct(*type_, struct_pointer_));
127+
}
128+
assert(false);
129+
}
130+
131+
private:
132+
static void const* copyStruct(std::type_info const& type, void const* struct_pointer) {
133+
std::size_t n;
134+
if (type == typeid(Struct1)) {
135+
n = sizeof(Struct1);
136+
} else if (type == typeid(Struct2)) {
137+
n = sizeof(Struct2);
138+
} else {
139+
assert(false);
140+
}
141+
return std::memcpy(new unsigned char[n], struct_pointer, n);
142+
}
143+
144+
void verify(Kind kind, std::type_info const& type) const {
145+
assert(kind_ == kind);
146+
assert(*type_ == type);
147+
}
148+
149+
void release() {
150+
if (kind_ == KIND_STRUCT) {
151+
delete[] static_cast<unsigned char const*>(struct_pointer_);
152+
}
153+
}
154+
155+
Kind kind_;
156+
std::type_info const* type_;
157+
union {
158+
int enum_value_;
159+
void const* struct_pointer_;
160+
};
161+
};
162+
163+
}
164+
165+
int main() {
166+
emscripten::enum_<Enum1>("Enum1").value("E1", Enum1::E1);
167+
emscripten::enum_<Enum2>("Enum2").value("E2", Enum2::E2);
168+
emscripten::value_object<Struct1>("Struct1").field("n", &Struct1::n);
169+
emscripten::value_object<Struct2>("Struct2").field("d", &Struct2::d);
170+
emscripten::class_<Any>("Any").constructor(Any::jsNew).function("get", &Any::jsGet);
171+
EM_ASM(
172+
globalThis.AnyEnum1 = new Module.Any("Enum1", Module.Enum1.E1);
173+
globalThis.AnyEnum2 = new Module.Any("Enum2", Module.Enum2.E2);
174+
globalThis.AnyStruct1 = new Module.Any("Struct1", {n: 1});
175+
globalThis.AnyStruct2 = new Module.Any("Struct2", {d: 1.5});
176+
globalThis.testGetEnum1 = () => globalThis.AnyEnum1.get() === Module.Enum1.E1;
177+
globalThis.testGetEnum2 = () => globalThis.AnyEnum2.get() === Module.Enum2.E2;
178+
globalThis.testGetStruct1 = () => globalThis.AnyStruct1.get().n === 1;
179+
globalThis.testGetStruct2 = () => globalThis.AnyStruct2.get().d === 1.5;
180+
);
181+
assert(emscripten::val::global("AnyEnum1").as<Any>().get<Enum1>() == Enum1::E1);
182+
assert(emscripten::val::global("AnyEnum2").as<Any>().get<Enum2>() == Enum2::E2);
183+
assert(emscripten::val::global("AnyStruct1").as<Any>().get<Struct1>().n == 1);
184+
assert(emscripten::val::global("AnyStruct2").as<Any>().get<Struct2>().d == 1.5);
185+
assert(emscripten::val::global().call<bool>("testGetEnum1"));
186+
assert(emscripten::val::global().call<bool>("testGetEnum2"));
187+
assert(emscripten::val::global().call<bool>("testGetStruct1"));
188+
assert(emscripten::val::global().call<bool>("testGetStruct2"));
189+
}

test/test_core.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7636,6 +7636,9 @@ def test_embind_val_coro_propogate_js_error(self):
76367636
self.cflags += ['-std=c++20', '--bind', '--pre-js=pre.js', '-fexceptions']
76377637
self.do_runf('embind/test_val_coro.cpp', 'rejected with: bang from JS promise!\n')
76387638

7639+
def test_embind_val_dyn(self):
7640+
self.do_runf('embind/test_val_dyn.cpp', cflags=['-lembind'])
7641+
76397642
def test_embind_dynamic_initialization(self):
76407643
self.cflags += ['-lembind']
76417644
self.do_run_in_out_file_test('embind/test_dynamic_initialization.cpp')

0 commit comments

Comments
 (0)