Skip to content

Commit 6ae900e

Browse files
authored
embind: Fix method pointers for AOT JS generation. (#21168)
Previously, AOT JS glue code for method pointers used the address of the method pointer as a unique ID to match JS glue with the binding. However, sometimes static initializers malloc different sizes when running the AOT generation code versus when the code is run normally. This caused the method pointer address to be different and the binding didn't match up. Instead of using the function pointer or the method pointer address we now use a generated signature for the invoker to match up the glue code. This works for both types of pointers and also has the advantage that many of the glue code functions are nearly identical and can be reusued (closure arguments make them behave differently). Fixes #20994
1 parent 8635504 commit 6ae900e

File tree

5 files changed

+85
-13
lines changed

5 files changed

+85
-13
lines changed

src/embind/embind.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,7 @@ var LibraryEmbind = {
781781
#endif
782782
#if EMBIND_AOT
783783
'$InvokerFunctions',
784+
'$createJsInvokerSignature',
784785
#endif
785786
#if ASYNCIFY
786787
'$Asyncify',
@@ -884,7 +885,7 @@ var LibraryEmbind = {
884885
#else
885886
// Builld the arguments that will be passed into the closure around the invoker
886887
// function.
887-
var closureArgs = [throwBindingError, cppInvokerFunc, cppTargetFunc, runDestructors, argTypes[0], argTypes[1]];
888+
var closureArgs = [humanName, throwBindingError, cppInvokerFunc, cppTargetFunc, runDestructors, argTypes[0], argTypes[1]];
888889
#if EMSCRIPTEN_TRACING
889890
closureArgs.push(Module);
890891
#endif
@@ -903,9 +904,10 @@ var LibraryEmbind = {
903904
}
904905

905906
#if EMBIND_AOT
906-
var invokerFn = InvokerFunctions[cppTargetFunc].apply(null, closureArgs);
907+
var signature = createJsInvokerSignature(argTypes, isClassMethodFunc, returns, isAsync);
908+
var invokerFn = InvokerFunctions[signature].apply(null, closureArgs);
907909
#else
908-
let [args, invokerFnBody] = createJsInvoker(humanName, argTypes, isClassMethodFunc, returns, isAsync);
910+
let [args, invokerFnBody] = createJsInvoker(argTypes, isClassMethodFunc, returns, isAsync);
909911
args.push(invokerFnBody);
910912
var invokerFn = newFunc(Function, args).apply(null, closureArgs);
911913
#endif

src/embind/embind_gen.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
var LibraryEmbind = {
88

99
$moduleDefinitions: [],
10+
// Function signatures that have already been generated for JS generation.
11+
$emittedFunctions: 'new Set()',
1012

1113
$PrimitiveType: class {
1214
constructor(typeId, name, destructorType) {
@@ -40,7 +42,7 @@ var LibraryEmbind = {
4042
this.destructorType = 'none'; // Same as emval.
4143
}
4244
},
43-
$FunctionDefinition__deps: ['$createJsInvoker'],
45+
$FunctionDefinition__deps: ['$createJsInvoker', '$createJsInvokerSignature', '$emittedFunctions'],
4446
$FunctionDefinition: class {
4547
constructor(name, returnType, argumentTypes, functionIndex, thisType = null, isAsync = false) {
4648
this.name = name;
@@ -105,10 +107,15 @@ var LibraryEmbind = {
105107
for (const argType of this.argumentTypes) {
106108
argTypes.push(this.convertToEmbindType(argType.type));
107109
}
108-
let [args, body] = createJsInvoker(this.name, argTypes, !!this.thisType, this.returnType.name !== 'void', this.isAsync);
110+
const signature = createJsInvokerSignature(argTypes, !!this.thisType, this.returnType.name !== 'void', this.isAsync)
111+
if (emittedFunctions.has(signature)) {
112+
return;
113+
}
114+
emittedFunctions.add(signature);
115+
let [args, body] = createJsInvoker(argTypes, !!this.thisType, this.returnType.name !== 'void', this.isAsync);
109116
out.push(
110117
// The ${""} is hack to workaround the preprocessor replacing "function".
111-
`'${this.functionIndex}': f${""}unction(${args.join(',')}) {\n${body}},`
118+
`'${signature}': f${""}unction(${args.join(',')}) {\n${body}},`
112119
);
113120
}
114121
},

src/embind/embind_shared.js

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,33 @@ var LibraryEmbindShared = {
179179
return false;
180180
},
181181

182+
// Many of the JS invoker functions are generic and can be reused for multiple
183+
// function bindings. This function needs to match createJsInvoker and create
184+
// a unique signature for any inputs that will create different invoker
185+
// function outputs.
186+
$createJsInvokerSignature(argTypes, isClassMethodFunc, returns, isAsync) {
187+
const signature = [
188+
isClassMethodFunc ? 't' : 'f',
189+
returns ? 't' : 'f',
190+
isAsync ? 't' : 'f'
191+
];
192+
for (let i = isClassMethodFunc ? 1 : 2; i < argTypes.length; ++i) {
193+
const arg = argTypes[i];
194+
let destructorSig = '';
195+
if (arg.destructorFunction === undefined) {
196+
destructorSig = 'u';
197+
} else if (arg.destructorFunction === null) {
198+
destructorSig = 'n';
199+
} else {
200+
destructorSig = 't';
201+
}
202+
signature.push(destructorSig);
203+
}
204+
return signature.join('');
205+
},
206+
182207
$createJsInvoker__deps: ['$usesDestructorStack'],
183-
$createJsInvoker(humanName, argTypes, isClassMethodFunc, returns, isAsync) {
208+
$createJsInvoker(argTypes, isClassMethodFunc, returns, isAsync) {
184209
var needsDestructorStack = usesDestructorStack(argTypes);
185210
var argCount = argTypes.length;
186211
var argsList = "";
@@ -193,19 +218,19 @@ var LibraryEmbindShared = {
193218
var invokerFnBody = `
194219
return function (${argsList}) {
195220
if (arguments.length !== ${argCount - 2}) {
196-
throwBindingError('function ${humanName} called with ' + arguments.length + ' arguments, expected ${argCount - 2}');
221+
throwBindingError('function ' + humanName + ' called with ' + arguments.length + ' arguments, expected ${argCount - 2}');
197222
}`;
198223

199224
#if EMSCRIPTEN_TRACING
200-
invokerFnBody += `Module.emscripten_trace_enter_context('embind::${humanName}');\n`;
225+
invokerFnBody += `Module.emscripten_trace_enter_context('embind::' + humanName );\n`;
201226
#endif
202227

203228
if (needsDestructorStack) {
204229
invokerFnBody += "var destructors = [];\n";
205230
}
206231

207232
var dtorStack = needsDestructorStack ? "destructors" : "null";
208-
var args1 = ["throwBindingError", "invoker", "fn", "runDestructors", "retType", "classParam"];
233+
var args1 = ["humanName", "throwBindingError", "invoker", "fn", "runDestructors", "retType", "classParam"];
209234

210235
#if EMSCRIPTEN_TRACING
211236
args1.push("Module");
@@ -216,7 +241,7 @@ var LibraryEmbindShared = {
216241
}
217242

218243
for (var i = 0; i < argCount - 2; ++i) {
219-
invokerFnBody += "var arg"+i+"Wired = argType"+i+"['toWireType']("+dtorStack+", arg"+i+"); // "+argTypes[i+2].name+"\n";
244+
invokerFnBody += "var arg"+i+"Wired = argType"+i+"['toWireType']("+dtorStack+", arg"+i+");\n";
220245
args1.push("argType"+i);
221246
}
222247

@@ -240,7 +265,7 @@ var LibraryEmbindShared = {
240265
for (var i = isClassMethodFunc?1:2; i < argTypes.length; ++i) { // Skip return value at index 0 - it's not deleted here. Also skip class type if not a method.
241266
var paramName = (i === 1 ? "thisWired" : ("arg"+(i - 2)+"Wired"));
242267
if (argTypes[i].destructorFunction !== null) {
243-
invokerFnBody += paramName+"_dtor("+paramName+"); // "+argTypes[i].name+"\n";
268+
invokerFnBody += paramName+"_dtor("+paramName+");\n";
244269
args1.push(paramName+"_dtor");
245270
}
246271
}
@@ -269,7 +294,7 @@ var LibraryEmbindShared = {
269294
invokerFnBody += "}\n";
270295

271296
#if ASSERTIONS
272-
invokerFnBody = `if (arguments.length !== ${args1.length}){ throw new Error("${humanName} Expected ${args1.length} closure arguments " + arguments.length + " given."); }\n${invokerFnBody}`;
297+
invokerFnBody = `if (arguments.length !== ${args1.length}){ throw new Error(humanName + "Expected ${args1.length} closure arguments " + arguments.length + " given."); }\n${invokerFnBody}`;
273298
#endif
274299
return [args1, invokerFnBody];
275300
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#include <stdio.h>
2+
#include <emscripten.h>
3+
#include <emscripten/bind.h>
4+
5+
using namespace emscripten;
6+
7+
__attribute__((constructor))
8+
void malloc_in_static_constructor(void) {
9+
// Malloc different sizes in AOT generation mode and normal mode to test
10+
// that method pointers still work correctly.
11+
EM_ASM(
12+
if (typeof InvokerFunctions == 'undefined') {
13+
_malloc(10);
14+
} else {
15+
_malloc(100);
16+
}
17+
);
18+
}
19+
20+
class Foo {
21+
public:
22+
void go() {}
23+
};
24+
25+
int main() {
26+
printf("done\n");
27+
}
28+
29+
EMSCRIPTEN_BINDINGS(xxx) {
30+
class_<Foo>("Foo")
31+
.function("go", &Foo::go);
32+
}

test/test_other.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3130,6 +3130,12 @@ def test_embind_tsgen_exceptions(self):
31303130
'-lembind', '--embind-emit-tsd', 'embind_tsgen.d.ts', '-fwasm-exceptions', '-sASSERTIONS'])
31313131
self.assertFileContents(test_file('other/embind_tsgen.d.ts'), read_file('embind_tsgen.d.ts'))
31323132

3133+
def test_embind_jsgen_method_pointer_stability(self):
3134+
self.emcc_args += ['-lembind', '-sEMBIND_AOT']
3135+
# Test that when method pointers are allocated at different addresses that
3136+
# AOT JS generation still works correctly.
3137+
self.do_runf('other/embind_jsgen_method_pointer_stability.cpp', 'done')
3138+
31333139
def test_emconfig(self):
31343140
output = self.run_process([emconfig, 'LLVM_ROOT'], stdout=PIPE).stdout.strip()
31353141
self.assertEqual(output, config.LLVM_ROOT)

0 commit comments

Comments
 (0)