Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement addFunction under wasm backend purely in terms of the wasm table #8255

Merged
merged 5 commits into from
Mar 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 6 additions & 34 deletions emscripten.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,15 +665,6 @@ def update_settings_glue(metadata):
shared.Settings.MAX_GLOBAL_ALIGN = metadata['maxGlobalAlign']
shared.Settings.IMPLEMENTED_FUNCTIONS = metadata['implementedFunctions']

# addFunction support for Wasm backend
if shared.Settings.WASM_BACKEND and shared.Settings.RESERVED_FUNCTION_POINTERS > 0:
start_index = metadata['jsCallStartIndex']
# e.g. jsCallFunctionType ['v', 'ii'] -> sig2order{'v': 0, 'ii': 1}
sig2order = {sig: i for i, sig in enumerate(metadata['jsCallFuncType'])}
# Index in the Wasm function table in which jsCall thunk function starts
shared.Settings.JSCALL_START_INDEX = start_index
shared.Settings.JSCALL_SIG_ORDER = sig2order

# Extract the list of function signatures that MAIN_THREAD_EM_ASM blocks in
# the compiled code have, each signature will need a proxy function invoker
# generated for it.
Expand Down Expand Up @@ -2156,19 +2147,16 @@ def emscript_wasm_backend(infile, outfile, memfile, libraries, compiler_engine,
pre = None

invoke_funcs = metadata.get('invokeFuncs', [])
# List of function signatures used in jsCall functions, e.g.['v', 'vi']
jscall_sigs = metadata.get('jsCallFuncType', [])

try:
del forwarded_json['Variables']['globals']['_llvm_global_ctors'] # not a true variable
except:
pass

sending = create_sending_wasm(invoke_funcs, jscall_sigs, forwarded_json,
metadata)
sending = create_sending_wasm(invoke_funcs, forwarded_json, metadata)
receiving = create_receiving_wasm(all_implemented)

module = create_module_wasm(sending, receiving, invoke_funcs, jscall_sigs, metadata)
module = create_module_wasm(sending, receiving, invoke_funcs, metadata)

write_output_file(outfile, post, module)
module = None
Expand Down Expand Up @@ -2205,9 +2193,7 @@ def debug_copy(src, dst):
debug_copy(base_source_map, 'base_wasm.map')

cmd = [wasm_emscripten_finalize, base_wasm, '-o', wasm,
'--global-base=%s' % shared.Settings.GLOBAL_BASE,
('--emscripten-reserved-function-pointers=%d' %
shared.Settings.RESERVED_FUNCTION_POINTERS)]
'--global-base=%s' % shared.Settings.GLOBAL_BASE]
if shared.Settings.DEBUG_LEVEL >= 2 or shared.Settings.PROFILING_FUNCS:
cmd.append('-g')
if shared.Settings.LEGALIZE_JS_FFI != 1:
Expand Down Expand Up @@ -2294,7 +2280,7 @@ def create_em_js(forwarded_json, metadata):
return em_js_funcs


def create_sending_wasm(invoke_funcs, jscall_sigs, forwarded_json, metadata):
def create_sending_wasm(invoke_funcs, forwarded_json, metadata):
basic_funcs = []
if shared.Settings.SAFE_HEAP:
basic_funcs += ['segfault', 'alignfault']
Expand All @@ -2310,10 +2296,7 @@ def create_sending_wasm(invoke_funcs, jscall_sigs, forwarded_json, metadata):
library_funcs = set(k for k, v in forwarded_json['Functions']['libraryFunctions'].items() if v != 2)
global_funcs = list(library_funcs.difference(set(global_vars)).difference(implemented_functions))

jscall_funcs = ['jsCall_' + sig for sig in jscall_sigs]

send_items = (basic_funcs + invoke_funcs + jscall_funcs + global_funcs +
basic_vars + global_vars)
send_items = (basic_funcs + invoke_funcs + global_funcs + basic_vars + global_vars)

def fix_import_name(g):
if g.startswith('Math_'):
Expand Down Expand Up @@ -2361,9 +2344,8 @@ def create_receiving_wasm(exports):
return '\n'.join(receiving) + '\n'


def create_module_wasm(sending, receiving, invoke_funcs, jscall_sigs, metadata):
def create_module_wasm(sending, receiving, invoke_funcs, metadata):
invoke_wrappers = create_invoke_wrappers(invoke_funcs)
jscall_funcs = create_jscall_funcs(jscall_sigs)

module = []
module.append('var asmGlobalArg = {};\n')
Expand All @@ -2375,7 +2357,6 @@ def create_module_wasm(sending, receiving, invoke_funcs, jscall_sigs, metadata):

module.append(receiving)
module.append(invoke_wrappers)
module.append(jscall_funcs)
return module


Expand All @@ -2392,8 +2373,6 @@ def load_metadata_wasm(metadata_raw, DEBUG):
'externs': [],
'simd': False,
'maxGlobalAlign': 0,
'jsCallStartIndex': 0,
'jsCallFuncType': [],
'staticBump': 0,
'tableSize': 0,
'initializers': [],
Expand Down Expand Up @@ -2443,13 +2422,6 @@ def create_invoke_wrappers(invoke_funcs):
return invoke_wrappers


def create_jscall_funcs(sigs):
jscall_funcs = ''
for i, sig in enumerate(sigs):
jscall_funcs += '\n' + shared.JS.make_jscall(sig, i) + '\n'
return jscall_funcs


def treat_as_user_function(name):
library_functions_in_module = ('setTempRet0', 'getTempRet0', 'stackAlloc',
'stackSave', 'stackRestore',
Expand Down
2 changes: 1 addition & 1 deletion src/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -2215,7 +2215,7 @@ LibraryManager.library = {
// Insert the function into the wasm table. Since we know the function
// comes directly from the loaded wasm module we can insert it directly
// into the table, avoiding any JS interaction.
return addWasmFunction(result);
return addFunctionWasm(result);
#else
// convert the exported function into a function pointer using our generic
// JS mechanism.
Expand Down
4 changes: 4 additions & 0 deletions src/preamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -1019,7 +1019,11 @@ Module['asm'] = function(global, env, providedBuffer) {
env['table'] = wasmTable = new WebAssembly.Table({
'initial': {{{ getQuoted('WASM_TABLE_SIZE') }}},
#if !ALLOW_TABLE_GROWTH
#if WASM_BACKEND
'maximum': {{{ getQuoted('WASM_TABLE_SIZE') }}} + {{{ RESERVED_FUNCTION_POINTERS }}},
#else
'maximum': {{{ getQuoted('WASM_TABLE_SIZE') }}},
#endif
#endif // WASM_BACKEND
'element': 'anyfunc'
});
Expand Down
6 changes: 1 addition & 5 deletions src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,7 @@ var ALIASING_FUNCTION_POINTERS = 0;
// By default we use a wasm Table for function pointers, which is fast and
// efficient. When enabling emulation, we also use the Table *outside* the wasm
// module, exactly as when emulating in asm.js, just replacing the plain JS
// array with a Table. However, Tables have some limitations currently, like not
// being able to assign an arbitrary JS method to them, which we have yet to
// work around.
// array with a Table.
var EMULATED_FUNCTION_POINTERS = 0;

// Allows function pointers to be cast, wraps each call of an incorrect type
Expand Down Expand Up @@ -1165,8 +1163,6 @@ var PTHREADS_DEBUG = 0;

var MAX_GLOBAL_ALIGN = -1; // received from the backend
var IMPLEMENTED_FUNCTIONS = []; // received from the backend
var JSCALL_START_INDEX = 0; // received from the backend
var JSCALL_SIG_ORDER = {}; // received from the backend

// Duplicate function elimination. This coalesces function bodies that are
// identical, which can happen e.g. if two methods have different C/C++ or LLVM
Expand Down
147 changes: 117 additions & 30 deletions src/support.js
Original file line number Diff line number Diff line change
Expand Up @@ -599,48 +599,128 @@ Module['registerFunctions'] = registerFunctions;
#endif // RELOCATABLE
#endif // EMULATED_FUNCTION_POINTERS

#if EMULATED_FUNCTION_POINTERS == 0
#if WASM_BACKEND && RESERVED_FUNCTION_POINTERS
var jsCallStartIndex = {{{ JSCALL_START_INDEX }}};
var jsCallSigOrder = {{{ JSON.stringify(JSCALL_SIG_ORDER) }}};
var jsCallNumSigs = Object.keys(jsCallSigOrder).length;
var functionPointers = new Array(jsCallNumSigs * {{{ RESERVED_FUNCTION_POINTERS }}});
#else // WASM_BACKEND && RESERVED_FUNCTION_POINTERS == 0
#if !WASM_BACKEND && EMULATED_FUNCTION_POINTERS == 0
var jsCallStartIndex = 1;
var functionPointers = new Array({{{ RESERVED_FUNCTION_POINTERS }}});
#endif // WASM_BACKEND && RESERVED_FUNCTION_POINTERS
#endif // EMULATED_FUNCTION_POINTERS == 0
#endif // !WASM_BACKEND && EMULATED_FUNCTION_POINTERS == 0

#if WASM
// Wraps a JS function as a wasm function with a given signature.
// In the future, we may get a WebAssembly.Function constructor. Until then,
// we create a wasm module that takes the JS function as an import with a given
// signature, and re-exports that as a wasm function.
function convertJsFunctionToWasm(func, sig) {
Copy link
Contributor

@binji binji Mar 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a bit of fun golfing this down... :-) [edit: now 238 chars, with help from teammates]

w=(f,[R,...P],W=WebAssembly,L=x=>[x.length,...x],C=x=>'dfji'.indexOf(x)+124)=>
new W.Instance(new W.Module(new Int8Array([,97,115,109,1,,,,1,...L([1,96,...L(P.
map(C)),...L(R=='v'?[]:[C(R)])]),2,5,1,,,,,7,4,1,,,0])),{'':{'':f}}).exports['']

let convertJsFunctionToWasm = w;

const add = (x, y) => x + y;
const addi = convertJsFunctionToWasm(add, 'iii');
const addd = convertJsFunctionToWasm(add, 'ddd');
const printi = convertJsFunctionToWasm(print, 'vi');

print(addi(1.2, 2.3)); // 3
print(addd(1.2, 2.3)); // 3.5
printi(42.1);          // 42

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the size of the generated module also reduced? Are you suggesting we use this version?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, sorry, we definitely shouldn't use this code. 😄 just had some insomnia last night so I started trying to see how small I could make it, thought you might appreciate. Feel free to ignore, haha

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the most useful wasm module ever on per-byte basis?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devil's advocate, should we not use this code? JS optimizers are extremely unlikely to shrink this code as well, and code size wins are code size wins.

Though it should be #ifed to only be shipped in release builds, and if we ever change the non-golfed version we should delete it. That is some write-only code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably best not to use it :) Does it even work with emscripten since it uses ES6 features? Was the old uglify pass ever removed?

Copy link
Member

@kripken kripken Mar 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do support ES6 now in all the major JS passes (but we didn't remove the old uglify versions because they let us support source maps in asm.js), and can probably assume JS is modern in places where wasm is used (but I'm not sure about that).

// The module is static, with the exception of the type section, which is
// generated based on the signature passed in.
var typeSection = [
0x01, // id: section,
0x00, // length: 0 (placeholder)
0x01, // count: 1
0x60, // form: func
];
var sigRet = sig.slice(0, 1);
var sigParam = sig.slice(1);
var typeCodes = {
'i': 0x7f, // i32
'j': 0x7e, // i64
'f': 0x7d, // f32
'd': 0x7c, // f64
};

// Parameters, length + signatures
typeSection.push(sigParam.length);
for (var i = 0; i < sigParam.length; ++i) {
typeSection.push(typeCodes[sigParam[i]]);
}

// Return values, length + signatures
// With no multi-return in MVP, either 0 (void) or 1 (anything else)
if (sigRet == 'v') {
typeSection.push(0x00);
} else {
typeSection = typeSection.concat([0x01, typeCodes[sigRet]]);
}

// Write the overall length of the type section back into the section header
// (excepting the 2 bytes for the section id and length)
typeSection[1] = typeSection.length - 2;

// Rest of the module is static
var bytes = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, // magic ("\0asm")
0x01, 0x00, 0x00, 0x00, // version: 1
].concat(typeSection, [
0x02, 0x07, // import section
// (import "e" "f" (func 0 (type 0)))
0x01, 0x01, 0x65, 0x01, 0x66, 0x00, 0x00,
0x07, 0x05, // export section
// (export "f" (func 0 (type 0)))
0x01, 0x01, 0x66, 0x00, 0x00,
]));

// We can compile this wasm module synchronously because it is very small.
// This accepts an import (at "e.f"), that it reroutes to an export (at "f")
var module = new WebAssembly.Module(bytes);
var instance = new WebAssembly.Instance(module, {
e: {
f: func
}
});
var wrappedFunc = instance.exports.f;
return wrappedFunc;
}

// Add a wasm function to the table.
// Attempting to call this with JS function will cause of table.set() to fail
function addWasmFunction(func) {
function addFunctionWasm(func, sig) {
var table = wasmTable;
var ret = table.length;
table.grow(1);
table.set(ret, func);

// Grow the table
try {
table.grow(1);
} catch (err) {
if (!err instanceof RangeError) {
throw err;
}
throw 'Unable to grow wasm table. Use a higher value for RESERVED_FUNCTION_POINTERS or set ALLOW_TABLE_GROWTH.';
}

// Insert new element
try {
// Attempting to call this with JS function will cause of table.set() to fail
table.set(ret, func);
} catch (err) {
if (!err instanceof TypeError) {
throw err;
}
assert(typeof sig !== 'undefined', 'Missing signature argument to addFunction');
var wrapped = convertJsFunctionToWasm(func, sig);
table.set(ret, wrapped);
}

return ret;
}

function removeFunctionWasm(index) {
// TODO(sbc): Look into implementing this to allow re-using of table slots
}
#endif

// 'sig' parameter is currently only used for LLVM backend under certain
// circumstance: RESERVED_FUNCTION_POINTERS=1, EMULATED_FUNCTION_POINTERS=0.
// 'sig' parameter is required for the llvm backend but only when func is not
// already a WebAssembly function.
function addFunction(func, sig) {
#if ASSERTIONS == 2
if (typeof sig === 'undefined') {
err('warning: addFunction(): You should provide a wasm function signature string as a second argument. This is not necessary for asm.js and asm2wasm, but can be required for the LLVM wasm backend, so it is recommended for full portability.');
}
#endif // ASSERTIONS

#if WASM_BACKEND
return addFunctionWasm(func, sig);
#else

#if EMULATED_FUNCTION_POINTERS == 0
#if WASM_BACKEND && RESERVED_FUNCTION_POINTERS
assert(typeof sig !== 'undefined',
'Second argument of addFunction should be a wasm function signature ' +
'string');
var base = jsCallSigOrder[sig] * {{{ RESERVED_FUNCTION_POINTERS }}};
#else // WASM_BACKEND && RESERVED_FUNCTION_POINTERS == 0
var base = 0;
#endif // WASM_BACKEND && RESERVED_FUNCTION_POINTERS
for (var i = base; i < base + {{{ RESERVED_FUNCTION_POINTERS }}}; i++) {
if (!functionPointers[i]) {
functionPointers[i] = func;
Expand All @@ -652,13 +732,9 @@ function addFunction(func, sig) {
#else // EMULATED_FUNCTION_POINTERS == 0

#if WASM
// assume we have been passed a wasm function and can add it to the table
// directly.
// TODO(sbc): This assumtion is most likely not valid. Look into ways of
// creating wasm functions based on JS functions as input.
return addWasmFunction(func);
return addFunctionWasm(func, sig);
#else
alignFunctionTables(); // XXX we should rely on this being an invariant
alignFunctionTables(); // TODO: we should rely on this being an invariant
var tables = getFunctionTables();
var ret = -1;
for (var sig in tables) {
Expand All @@ -668,21 +744,32 @@ function addFunction(func, sig) {
table.push(func);
}
return ret;
#endif
#endif // WASM

#endif // EMULATED_FUNCTION_POINTERS == 0
#endif // WASM_BACKEND
}

function removeFunction(index) {
#if WASM_BACKEND
removeFunctionWasm(index);
#else

#if EMULATED_FUNCTION_POINTERS == 0
functionPointers[index-jsCallStartIndex] = null;
#else
#if WASM
removeFunctionWasm(index);
#else
alignFunctionTables(); // XXX we should rely on this being an invariant
var tables = getFunctionTables();
for (var sig in tables) {
tables[sig][index] = null;
}
#endif
#endif // WASM

#endif // EMULATE_FUNCTION_POINTER_CASTS == 0
#endif // WASM_BACKEND
}

var funcWrappers = {};
Expand Down
Loading