import assert from 'node:assert';
import * as fs from 'node:fs/promises';
import {
ATMODULES,
ATEXITS,
ATINITS,
ATPOSTCTORS,
ATPRERUNS,
ATMAINS,
ATPOSTRUNS,
defineI64Param,
indentify,
makeReturn64,
modifyJSFunction,
preprocess,
processMacros,
receiveI64ParamAsI53,
} from './parseTools.mjs';
import {
addToCompileTimeContext,
error,
errorOccured,
isDecorator,
isJsOnlySymbol,
compileTimeContext,
printErr,
readFile,
runInMacroContext,
warn,
warnOnce,
warningOccured,
localFile,
} from './utility.mjs';
import {LibraryManager, librarySymbols} from './modules.mjs';
const addedLibraryItems = {};
const extraLibraryFuncs = [];
const CHECK_DEPS = process.env.EMCC_CHECK_DEPS;
const proxiedFunctionTable = [];
function mangleCSymbolName(f) {
if (f === '__main_argc_argv') {
f = 'main';
}
return f[0] == '$' ? f.slice(1) : '_' + f;
}
function splitter(array, filter) {
const splitOut = array.filter(filter);
const leftIn = array.filter((x) => !filter(x));
return {leftIn, splitOut};
}
function escapeJSONKey(x) {
if (/^[\d\w_]+$/.exec(x) || x[0] === '"' || x[0] === "'") return x;
assert(!x.includes("'"), 'cannot have internal single quotes in keys: ' + x);
return "'" + x + "'";
}
function stringifyWithFunctions(obj) {
if (typeof obj == 'function') return obj.toString();
if (obj === null || typeof obj != 'object') return JSON.stringify(obj);
if (Array.isArray(obj)) {
return '[' + obj.map(stringifyWithFunctions).join(',') + ']';
}
const builtinContainers = runInMacroContext('[Map, Set, WeakMap, WeakSet]', {
filename: '<internal>',
});
for (const container of builtinContainers) {
if (obj instanceof container) {
const className = container.name;
assert(!obj.size, `cannot stringify ${className} with data`);
return `new ${className}`;
}
}
var rtn = '{\n';
for (const [key, value] of Object.entries(obj)) {
var str = stringifyWithFunctions(value);
if (typeof value === 'function' && (str.startsWith(key) || str.startsWith('async ' + key))) {
rtn += str + ',\n';
} else {
rtn += escapeJSONKey(key) + ':' + str + ',\n';
}
}
return rtn + '}';
}
function isDefined(symName) {
if (WASM_EXPORTS.has(symName) || SIDE_MODULE_EXPORTS.has(symName)) {
return true;
}
if (symName == '__main_argc_argv' && SIDE_MODULE_EXPORTS.has('main')) {
return true;
}
if (RELOCATABLE && symName.startsWith('invoke_')) {
return true;
}
return false;
}
function resolveAlias(symbol) {
var value = LibraryManager.library[symbol];
if (typeof value == 'string' && value[0] != '=' && LibraryManager.library.hasOwnProperty(value)) {
return value;
}
return symbol;
}
function getTransitiveDeps(symbol) {
const transitiveDeps = new Set();
const seen = new Set();
const toVisit = [symbol];
while (toVisit.length) {
const sym = toVisit.pop();
if (!seen.has(sym)) {
let directDeps = LibraryManager.library[sym + '__deps'] || [];
directDeps = directDeps.filter((d) => typeof d === 'string');
for (const dep of directDeps) {
const resolved = resolveAlias(dep);
if (VERBOSE && !transitiveDeps.has(dep)) {
printErr(`adding dependency ${symbol} -> ${dep}`);
}
transitiveDeps.add(resolved);
toVisit.push(resolved);
}
seen.add(sym);
}
}
return Array.from(transitiveDeps);
}
function shouldPreprocess(fileName) {
var content = readFile(fileName).trim();
return content.startsWith('#preprocess\n') || content.startsWith('#preprocess\r\n');
}
function getIncludeFile(fileName, alwaysPreprocess, shortName) {
shortName ??= fileName;
let result = `// include: ${shortName}\n`;
const doPreprocess = alwaysPreprocess || shouldPreprocess(fileName);
if (doPreprocess) {
result += processMacros(preprocess(fileName), fileName);
} else {
result += readFile(fileName);
}
result += `// end include: ${shortName}\n`;
return result;
}
function getSystemIncludeFile(fileName) {
return getIncludeFile(localFile(fileName), true, fileName);
}
function preJS() {
let result = '';
for (const fileName of PRE_JS_FILES) {
result += getIncludeFile(fileName);
}
return result;
}
const checkDependenciesSkip = new Set([
'_mmap_js',
'_emscripten_throw_longjmp',
'_emscripten_receive_on_main_thread_js',
'emscripten_start_fetch',
'emscripten_start_wasm_audio_worklet_thread_async',
]);
const checkDependenciesIgnore = new Set([
'$PThread',
'$WebGPU',
'$SDL',
'$GLUT',
'$GLEW',
'$Browser',
'$AL',
'$GL',
'$IDBStore',
'$polyfillWaitAsync',
'$GLImmediateSetup',
'$emscriptenGetAudioObject',
'$bigintToI53Checked',
'$convertI32PairToI53Checked',
'setTempRet0',
]);
function checkDependencies(symbol, snippet, deps, postset) {
if (checkDependenciesSkip.has(symbol)) {
return;
}
for (const dep of deps) {
if (typeof dep === 'function') {
continue;
}
if (checkDependenciesIgnore.has(dep)) {
continue;
}
const mangled = mangleCSymbolName(dep);
if (!snippet.includes(mangled) && (!postset || !postset.includes(mangled))) {
error(`${symbol}: unused dependency: ${dep}`);
}
}
}
function addImplicitDeps(snippet, deps) {
const autoDeps = [
'getDynCaller',
'getWasmTableEntry',
'runtimeKeepalivePush',
'runtimeKeepalivePop',
'UTF8ToString',
];
for (const dep of autoDeps) {
if (snippet.includes(dep + '(')) {
deps.push('$' + dep);
}
}
}
function handleI64Signatures(symbol, snippet, sig, i53abi) {
return modifyJSFunction(snippet, (args, body, async_, oneliner) => {
let argLines = args.split('\n');
argLines = argLines.map((line) => line.split('//')[0]);
const argNames = argLines
.join(' ')
.split(',')
.map((name) => name.trim());
const newArgs = [];
let argConversions = '';
if (sig.length > argNames.length + 1) {
error(`handleI64Signatures: signature too long for ${symbol}`);
return snippet;
}
for (let i = 0; i < argNames.length; i++) {
const name = argNames[i];
const argType = sig[i + 1];
if (WASM_BIGINT && ((MEMORY64 && argType == 'p') || (i53abi && argType == 'j'))) {
argConversions += ` ${receiveI64ParamAsI53(name, undefined, false)}\n`;
} else {
if (argType == 'j' && i53abi) {
argConversions += ` ${receiveI64ParamAsI53(name, undefined, false)}\n`;
newArgs.push(defineI64Param(name));
} else if (argType == 'p' && CAN_ADDRESS_2GB) {
argConversions += ` ${name} >>>= 0;\n`;
newArgs.push(name);
} else {
newArgs.push(name);
}
}
}
if (!WASM_BIGINT) {
args = newArgs.join(',');
}
if ((sig[0] == 'j' && i53abi) || (sig[0] == 'p' && MEMORY64)) {
const await_ = async_ ? 'await ' : '';
if (oneliner) {
if (argConversions) {
return `${async_}(${args}) => {
${argConversions}
return ${makeReturn64(await_ + body)};
}`;
}
return `${async_}(${args}) => ${makeReturn64(await_ + body)};`;
}
return `\
${async_}function(${args}) {
${argConversions}
var ret = (() => { ${body} })();
return ${makeReturn64(await_ + 'ret')};
}`;
}
if (oneliner) {
body = `return ${body}`;
}
return `\
${async_}function(${args}) {
${argConversions}
${body};
}`;
});
}
export async function runJSify(outputFile, symbolsOnly) {
const libraryItems = [];
const symbolDeps = {};
const asyncFuncs = [];
let postSets = [];
LibraryManager.load();
let outputHandle = process.stdout;
if (outputFile) {
outputHandle = await fs.open(outputFile, 'w');
}
async function writeOutput(str) {
await outputHandle.write(str + '\n');
}
const symbolsNeeded = DEFAULT_LIBRARY_FUNCS_TO_INCLUDE;
symbolsNeeded.push(...extraLibraryFuncs);
for (const sym of EXPORTED_RUNTIME_METHODS) {
if ('$' + sym in LibraryManager.library) {
symbolsNeeded.push('$' + sym);
}
}
for (const key of Object.keys(LibraryManager.library)) {
if (!isDecorator(key)) {
if (INCLUDE_FULL_LIBRARY || EXPORTED_FUNCTIONS.has(mangleCSymbolName(key))) {
symbolsNeeded.push(key);
}
}
}
function processLibraryFunction(snippet, symbol, mangled, deps, isStub) {
snippet = snippet.toString().replace(/\r\n/gm, '\n');
if (snippet.startsWith(symbol)) {
snippet = 'function ' + snippet;
}
if (isStub) {
return snippet;
}
if (LIBRARY_DEBUG && !isJsOnlySymbol(symbol)) {
snippet = modifyJSFunction(snippet, (args, body, _async, oneliner) => {
var run_func;
if (oneliner) {
run_func = `var ret = ${body}`;
} else {
run_func = `var ret = (() => { ${body} })();`;
}
return `\
function(${args}) {
dbg("[library call:${mangled}: " + Array.prototype.slice.call(arguments).map(prettyPrint) + "]");
${run_func}
dbg(" [ return:" + prettyPrint(ret));
return ret;
}`;
});
}
const sig = LibraryManager.library[symbol + '__sig'];
const i53abi = LibraryManager.library[symbol + '__i53abi'];
if (i53abi) {
if (!sig) {
error(`JS library error: '__i53abi' decorator requires '__sig' decorator: '${symbol}'`);
}
if (!sig.includes('j')) {
error(
`JS library error: '__i53abi' only makes sense when '__sig' includes 'j' (int64): '${symbol}'`,
);
}
}
if (
sig &&
((i53abi && sig.includes('j')) || ((MEMORY64 || CAN_ADDRESS_2GB) && sig.includes('p')))
) {
snippet = handleI64Signatures(symbol, snippet, sig, i53abi);
compileTimeContext.i53ConversionDeps.forEach((d) => deps.push(d));
}
const proxyingMode = LibraryManager.library[symbol + '__proxy'];
if (proxyingMode) {
if (proxyingMode !== 'sync' && proxyingMode !== 'async' && proxyingMode !== 'none') {
throw new Error(`Invalid proxyingMode ${symbol}__proxy: '${proxyingMode}' specified!`);
}
if (SHARED_MEMORY && proxyingMode != 'none') {
const sync = proxyingMode === 'sync';
if (PTHREADS) {
snippet = modifyJSFunction(snippet, (args, body, async_, oneliner) => {
if (oneliner) {
body = `return ${body}`;
}
const rtnType = sig && sig.length ? sig[0] : null;
const proxyFunc =
MEMORY64 && rtnType == 'p' ? 'proxyToMainThreadPtr' : 'proxyToMainThread';
deps.push('$' + proxyFunc);
return `
${async_}function(${args}) {
if (ENVIRONMENT_IS_PTHREAD)
return ${proxyFunc}(${proxiedFunctionTable.length}, 0, ${+sync}${args ? ', ' : ''}${args});
${body}
}\n`;
});
} else if (WASM_WORKERS && ASSERTIONS) {
snippet = modifyJSFunction(
snippet,
(args, body) => `
function(${args}) {
assert(!ENVIRONMENT_IS_WASM_WORKER, "Attempted to call proxied function '${mangled}' in a Wasm Worker, but in Wasm Worker enabled builds, proxied function architecture is not available!");
${body}
}\n`,
);
}
proxiedFunctionTable.push(mangled);
}
}
return snippet;
}
function symbolHandler(symbol) {
if (LINK_AS_CXX && !WASM_EXCEPTIONS && symbol.startsWith('__cxa_find_matching_catch_')) {
if (DISABLE_EXCEPTION_THROWING) {
error(
'DISABLE_EXCEPTION_THROWING was set (likely due to -fno-exceptions), which means no C++ exception throwing support code is linked in, but exception catching code appears. Either do not set DISABLE_EXCEPTION_THROWING (if you do want exception throwing) or compile all source files with -fno-exceptions (so that no exceptions support code is required); also make sure DISABLE_EXCEPTION_CATCHING is set to the right value - if you want exceptions, it should be off, and vice versa.',
);
return;
}
if (!(symbol in LibraryManager.library)) {
const num = +symbol.split('_').slice(-1)[0];
compileTimeContext.addCxaCatch(num);
}
}
function addFromLibrary(symbol, dependent, force = false) {
if (isDecorator(symbol)) {
return;
}
if (WASM_EXPORTS.has(symbol) && !force) {
return;
}
if (symbol in addedLibraryItems) {
return;
}
addedLibraryItems[symbol] = true;
if (!(symbol + '__deps' in LibraryManager.library)) {
LibraryManager.library[symbol + '__deps'] = [];
}
const deps = LibraryManager.library[symbol + '__deps'];
let sig = LibraryManager.library[symbol + '__sig'];
if (!WASM_BIGINT && sig && sig[0] == 'j') {
deps.push('setTempRet0');
}
let isAsyncFunction = false;
if (ASYNCIFY) {
const original = LibraryManager.library[symbol];
if (typeof original == 'function') {
isAsyncFunction = LibraryManager.library[symbol + '__async'];
}
if (isAsyncFunction) {
asyncFuncs.push(symbol);
}
}
if (symbolsOnly) {
if (LibraryManager.library.hasOwnProperty(symbol)) {
var resolvedSymbol = resolveAlias(symbol);
var transtiveDeps = getTransitiveDeps(resolvedSymbol);
symbolDeps[symbol] = transtiveDeps.filter(
(d) => !isJsOnlySymbol(d) && !(d in LibraryManager.library),
);
}
return;
}
let isStub = false;
const mangled = mangleCSymbolName(symbol);
if (!LibraryManager.library.hasOwnProperty(symbol)) {
const isWeakImport = WEAK_IMPORTS.has(symbol);
if (!isDefined(symbol) && !isWeakImport) {
if (PROXY_TO_PTHREAD && !MAIN_MODULE && symbol == '__main_argc_argv') {
error('PROXY_TO_PTHREAD proxies main() for you, but no main exists');
return;
}
let undefinedSym = symbol;
if (symbol === '__main_argc_argv') {
undefinedSym = 'main/__main_argc_argv';
}
let msg = 'undefined symbol: ' + undefinedSym;
if (dependent) msg += ` (referenced by ${dependent})`;
if (ERROR_ON_UNDEFINED_SYMBOLS) {
error(msg);
warnOnce(
'To disable errors for undefined symbols use `-sERROR_ON_UNDEFINED_SYMBOLS=0`',
);
warnOnce(
mangled +
' may need to be added to EXPORTED_FUNCTIONS if it arrives from a system library',
);
} else if (VERBOSE || WARN_ON_UNDEFINED_SYMBOLS) {
warn(msg);
}
if (symbol === '__main_argc_argv' && STANDALONE_WASM) {
warn('To build in STANDALONE_WASM mode without a main(), use emcc --no-entry');
}
}
if (!RELOCATABLE) {
LibraryManager.library[symbol] = new Function(`abort('missing function: ${symbol}');`);
LibraryManager.library[symbol + '__docs'] = '/** @type {function(...*):?} */';
isStub = true;
} else {
let target = `wasmImports['${symbol}']`;
if (ASYNCIFY) {
target = `asyncifyStubs['${symbol}']`;
}
let assertion = '';
if (ASSERTIONS) {
assertion += `if (!${target} || ${target}.stub) abort("external symbol '${symbol}' is missing. perhaps a side module was not linked in? if this function was expected to arrive from a system library, try to build the MAIN_MODULE with EMCC_FORCE_STDLIBS=1 in the environment");\n`;
}
const functionBody = assertion + `return ${target}(...args);`;
LibraryManager.library[symbol] = new Function('...args', functionBody);
isStub = true;
}
}
librarySymbols.push(mangled);
const original = LibraryManager.library[symbol];
let snippet = original;
const isUserSymbol = LibraryManager.library[symbol + '__user'];
deps.forEach((dep) => {
if (isUserSymbol && LibraryManager.library[dep + '__internal']) {
warn(`user library symbol '${symbol}' depends on internal symbol '${dep}'`);
}
});
let isFunction = false;
let aliasTarget;
const postsetId = symbol + '__postset';
const postset = LibraryManager.library[postsetId];
if (postset) {
const postsetString = typeof postset == 'function' ? postset() : postset;
if (postsetString && !addedLibraryItems[postsetId]) {
addedLibraryItems[postsetId] = true;
postSets.push(postsetString + ';');
}
}
if (typeof snippet == 'string') {
if (snippet[0] != '=') {
if (LibraryManager.library[snippet]) {
aliasTarget = snippet;
snippet = mangleCSymbolName(aliasTarget);
deps.push(aliasTarget);
}
}
} else if (typeof snippet == 'object') {
snippet = stringifyWithFunctions(snippet);
addImplicitDeps(snippet, deps);
} else if (typeof snippet == 'function') {
isFunction = true;
snippet = processLibraryFunction(snippet, symbol, mangled, deps, isStub);
addImplicitDeps(snippet, deps);
if (CHECK_DEPS && !isUserSymbol) {
checkDependencies(symbol, snippet, deps, postset?.toString());
}
}
if (VERBOSE) {
printErr(`adding ${symbol} (referenced by ${dependent})`);
}
function addDependency(dep) {
if (typeof dep == 'function') {
return dep();
}
if (dep === '$noExitRuntime') {
error(
'noExitRuntime cannot be referenced via __deps mechanism. Use DEFAULT_LIBRARY_FUNCS_TO_INCLUDE or EXPORTED_RUNTIME_METHODS',
);
}
return addFromLibrary(dep, `${symbol}, referenced by ${dependent}`, dep === aliasTarget);
}
let contentText;
if (isFunction) {
if (
(USE_ASAN || USE_LSAN || UBSAN_RUNTIME) &&
LibraryManager.library[symbol + '__noleakcheck']
) {
contentText = modifyJSFunction(
snippet,
(args, body) => `(${args}) => withBuiltinMalloc(() => {${body}})`,
);
deps.push('$withBuiltinMalloc');
} else {
contentText = snippet;
}
if (contentText.match(/^\s*([^}]*)\s*=>/s)) {
contentText = `var ${mangled} = ` + contentText + ';';
} else if (contentText.startsWith('class ')) {
contentText = contentText.replace(/^class /, `class ${mangled} `);
} else {
contentText = contentText.replace(/function(?:\s+([^(]+))?\s*\(/, `function ${mangled}(`);
}
} else if (typeof snippet == 'string' && snippet.startsWith(';')) {
contentText = 'var ' + mangled + snippet;
if (snippet[snippet.length - 1] != ';' && snippet[snippet.length - 1] != '}') {
contentText += ';';
}
} else if (typeof snippet == 'undefined') {
var isDirectWasmExport = WASM_ESM_INTEGRATION && mangled == 'wasmTable';
if (isDirectWasmExport) {
contentText = '';
} else {
contentText = `var ${mangled};`;
}
} else {
if (typeof snippet == 'string' && snippet[0] == '=') {
snippet = snippet.slice(1);
}
contentText = `var ${mangled} = ${snippet};`;
}
if (MODULARIZE == 'instance' && (EXPORT_ALL || EXPORTED_FUNCTIONS.has(mangled)) && !isStub) {
contentText = 'export ' + contentText;
}
if (sig && RELOCATABLE) {
if (!WASM_BIGINT) {
sig = sig[0].replace('j', 'i') + sig.slice(1).replace(/j/g, 'ii');
}
contentText += `\n${mangled}.sig = '${sig}';`;
}
if (ASYNCIFY && isAsyncFunction) {
contentText += `\n${mangled}.isAsync = true;`;
}
if (isStub) {
contentText += `\n${mangled}.stub = true;`;
if (ASYNCIFY && MAIN_MODULE) {
contentText += `\nasyncifyStubs['${symbol}'] = undefined;`;
}
}
let commentText = '';
if (force) {
commentText += '/** @suppress {duplicate } */\n';
}
let docs = LibraryManager.library[symbol + '__docs'];
if (docs) {
commentText += docs + '\n';
}
if (EMIT_TSD) {
LibraryManager.libraryDefinitions[mangled] = {
docs: docs ?? null,
snippet: snippet ?? null,
};
}
const depsText = deps
? deps
.map(addDependency)
.filter((x) => x != '')
.join('\n') + '\n'
: '';
return depsText + commentText + contentText;
}
const JS = addFromLibrary(symbol, 'root reference (e.g. compiled C/C++ code)');
libraryItems.push(JS);
}
function includeSystemFile(fileName) {
writeOutput(getSystemIncludeFile(fileName));
}
function includeFile(fileName) {
writeOutput(getIncludeFile(fileName));
}
function finalCombiner() {
const splitPostSets = splitter(postSets, (x) => x.symbol && x.dependencies);
postSets = splitPostSets.leftIn;
const orderedPostSets = splitPostSets.splitOut;
let limit = orderedPostSets.length * orderedPostSets.length;
for (let i = 0; i < orderedPostSets.length; i++) {
for (let j = i + 1; j < orderedPostSets.length; j++) {
if (orderedPostSets[j].symbol in orderedPostSets[i].dependencies) {
const temp = orderedPostSets[i];
orderedPostSets[i] = orderedPostSets[j];
orderedPostSets[j] = temp;
i--;
limit--;
assert(limit > 0, 'Could not sort postsets!');
break;
}
}
}
postSets.push(...orderedPostSets);
const shellFile = MINIMAL_RUNTIME ? 'shell_minimal.js' : 'shell.js';
includeSystemFile(shellFile);
const preFile = MINIMAL_RUNTIME ? 'preamble_minimal.js' : 'preamble.js';
includeSystemFile(preFile);
writeOutput('// Begin JS library code\n');
for (const item of libraryItems.concat(postSets)) {
writeOutput(indentify(item || '', 2));
}
writeOutput('// End JS library code\n');
if (!MINIMAL_RUNTIME) {
includeSystemFile('postlibrary.js');
}
if (PTHREADS) {
writeOutput(`
// proxiedFunctionTable specifies the list of functions that can be called
// either synchronously or asynchronously from other threads in postMessage()d
// or internally queued events. This way a pthread in a Worker can synchronously
// access e.g. the DOM on the main thread.
var proxiedFunctionTable = [
${proxiedFunctionTable.join(',\n ')}
];
`);
}
writeOutput('// EMSCRIPTEN_END_FUNCS\n');
const postFile = MINIMAL_RUNTIME ? 'postamble_minimal.js' : 'postamble.js';
includeSystemFile(postFile);
for (const fileName of POST_JS_FILES) {
includeFile(fileName);
}
if (MODULARIZE && MODULARIZE != 'instance') {
includeSystemFile('postamble_modularize.js');
}
if (errorOccured()) {
throw Error('Aborting compilation due to previous errors');
}
writeOutput(
'//FORWARDED_DATA:' +
JSON.stringify({
librarySymbols,
warnings: warningOccured(),
asyncFuncs,
libraryDefinitions: LibraryManager.libraryDefinitions,
ATPRERUNS: ATPRERUNS.join('\n'),
ATMODULES: ATMODULES.join('\n'),
ATINITS: ATINITS.join('\n'),
ATPOSTCTORS: ATPOSTCTORS.join('\n'),
ATMAINS: ATMAINS.join('\n'),
ATPOSTRUNS: ATPOSTRUNS.join('\n'),
ATEXITS: ATEXITS.join('\n'),
}),
);
}
for (const sym of symbolsNeeded) {
symbolHandler(sym);
}
if (symbolsOnly) {
writeOutput(
JSON.stringify({
deps: symbolDeps,
asyncFuncs,
extraLibraryFuncs,
}),
);
} else {
finalCombiner();
}
if (errorOccured()) {
throw Error('Aborting compilation due to previous errors');
}
if (outputFile) await outputHandle.close();
}
addToCompileTimeContext({
extraLibraryFuncs,
addedLibraryItems,
preJS,
});