Skip to content

Commit 7aca82a

Browse files
committed
Assert if MODULARIZE factory function is used with new.
Once we start using `async` for for this function then `new` stops working: emscripten-core#23157. Using `new` in this was was always an antipattern but there was nothing stopping users from doing this.
1 parent 739fe37 commit 7aca82a

8 files changed

+80
-58
lines changed

ChangeLog.md

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ See docs/process.md for more on how version tagging works.
2020

2121
3.1.75 (in development)
2222
-----------------------
23+
- When using `-sMODULARIZE` we now assert if the factory function is called with
24+
the JS `new` keyword. e.g. `var a = new Module()` rather than `var b =
25+
Module()`. This paves the way for marking the function as `async` which does
26+
not allow `new` to beused. This usage of `new` here was never documented and
27+
is considered and antipattern. (#)
2328
- `PATH.basename()` no longer calls `PATH.normalize()`, so that
2429
`PATH.basename("a/.")` returns `"."` instead of `"a"` and
2530
`PATH.basename("a/b/..")` returns `".."` instead of `"a"`. This is in line with

test/code_size/hello_webgl2_wasm.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.html": 454,
33
"a.html.gz": 328,
4-
"a.js": 4532,
5-
"a.js.gz": 2315,
4+
"a.js": 4527,
5+
"a.js.gz": 2311,
66
"a.wasm": 10402,
77
"a.wasm.gz": 6703,
8-
"total": 15388,
9-
"total_gz": 9346
8+
"total": 15383,
9+
"total_gz": 9342
1010
}
+4-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"a.html": 346,
33
"a.html.gz": 262,
4-
"a.js": 22200,
5-
"a.js.gz": 11583,
6-
"total": 22546,
7-
"total_gz": 11845
4+
"a.js": 22195,
5+
"a.js.gz": 11581,
6+
"total": 22541,
7+
"total_gz": 11843
88
}

test/code_size/hello_webgl_wasm.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.html": 454,
33
"a.html.gz": 328,
4-
"a.js": 4070,
5-
"a.js.gz": 2158,
4+
"a.js": 4065,
5+
"a.js.gz": 2155,
66
"a.wasm": 10402,
77
"a.wasm.gz": 6703,
8-
"total": 14926,
9-
"total_gz": 9189
8+
"total": 14921,
9+
"total_gz": 9186
1010
}
+4-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"a.html": 346,
33
"a.html.gz": 262,
4-
"a.js": 21726,
5-
"a.js.gz": 11413,
6-
"total": 22072,
7-
"total_gz": 11675
4+
"a.js": 21721,
5+
"a.js.gz": 11409,
6+
"total": 22067,
7+
"total_gz": 11671
88
}

test/test_browser.py

+13-22
Original file line numberDiff line numberDiff line change
@@ -4795,28 +4795,19 @@ def test_browser_run_from_different_directory(self):
47954795

47964796
# Similar to `test_browser_run_from_different_directory`, but asynchronous because of `-sMODULARIZE`
47974797
def test_browser_run_from_different_directory_async(self):
4798-
for args, creations in [
4799-
(['-sMODULARIZE'], [
4800-
'Module();', # documented way for using modularize
4801-
'new Module();' # not documented as working, but we support it
4802-
]),
4803-
]:
4804-
print(args)
4805-
# compile the code with the modularize feature and the preload-file option enabled
4806-
self.compile_btest('browser_test_hello_world.c', ['-o', 'test.js', '-O3'] + args)
4807-
ensure_dir('subdir')
4808-
shutil.move('test.js', Path('subdir/test.js'))
4809-
shutil.move('test.wasm', Path('subdir/test.wasm'))
4810-
for creation in creations:
4811-
print(creation)
4812-
# Make sure JS is loaded from subdirectory
4813-
create_file('test-subdir.html', '''
4814-
<script src="subdir/test.js"></script>
4815-
<script>
4816-
%s
4817-
</script>
4818-
''' % creation)
4819-
self.run_browser('test-subdir.html', '/report_result?0')
4798+
# compile the code with the modularize feature and the preload-file option enabled
4799+
self.compile_btest('browser_test_hello_world.c', ['-o', 'test.js', '-O3', '-sMODULARIZE'])
4800+
ensure_dir('subdir')
4801+
shutil.move('test.js', Path('subdir/test.js'))
4802+
shutil.move('test.wasm', Path('subdir/test.wasm'))
4803+
# Make sure JS is loaded from subdirectory
4804+
create_file('test-subdir.html', '''
4805+
<script src="subdir/test.js"></script>
4806+
<script>
4807+
Module();
4808+
</script>
4809+
''')
4810+
self.run_browser('test-subdir.html', '/report_result?0')
48204811

48214812
# Similar to `test_browser_run_from_different_directory`, but
48224813
# also also we eval the initial code, so currentScript is not present. That prevents us

test/test_other.py

+6
Original file line numberDiff line numberDiff line change
@@ -6774,6 +6774,12 @@ def test_modularize_strict(self):
67746774
output = self.run_js('run.js')
67756775
self.assertEqual(output, 'hello, world!\n')
67766776

6777+
def test_modularize_new_misuse(self):
6778+
self.run_process([EMCC, test_file('hello_world.c'), '-sMODULARIZE', '-sEXPORT_NAME=Foo'])
6779+
create_file('run.js', 'var m = require("./a.out.js"); new m();')
6780+
err = self.run_js('run.js', assert_returncode=NON_ZERO)
6781+
self.assertContained('Foo() must not be called with new\n', err)
6782+
67776783
@parameterized({
67786784
'': ([],),
67796785
'export_name': (['-sEXPORT_NAME=Foo'],),

tools/link.py

+40-20
Original file line numberDiff line numberDiff line change
@@ -2385,6 +2385,15 @@ def modularize():
23852385
if settings.EXPORT_ES6 and settings.ENVIRONMENT_MAY_BE_NODE:
23862386
async_emit = 'async '
23872387

2388+
# Both pthreads and wasm workers rely on _scriptName in the absence of `import.meta`
2389+
add_script_name_wrapper = settings.SHARED_MEMORY and not (settings.EXPORT_ES6 and settings.USE_ES6_IMPORT_META)
2390+
add_assertion_wrapper = settings.ASSERTIONS and not settings.MODULARIZE == 'instance'
2391+
2392+
if add_script_name_wrapper or add_assertion_wrapper:
2393+
wrapper_name = settings.EXPORT_NAME + '_inner';
2394+
else:
2395+
wrapper_name = settings.EXPORT_NAME;
2396+
23882397
if settings.MODULARIZE == 'instance':
23892398
wrapper_function = '''
23902399
export default async function init(moduleArg = {}) {
@@ -2398,8 +2407,8 @@ def modularize():
23982407
'generated_js': generated_js
23992408
}
24002409
else:
2401-
wrapper_function = '''
2402-
%(maybe_async)sfunction(moduleArg = {}) {
2410+
wrapper_function = '''\
2411+
%(maybe_async)sfunction %(wrapper_name)s(moduleArg = {}) {
24032412
var moduleRtn;
24042413
24052414
%(generated_js)s
@@ -2408,26 +2417,18 @@ def modularize():
24082417
}
24092418
''' % {
24102419
'maybe_async': async_emit,
2411-
'generated_js': generated_js
2420+
'generated_js': generated_js,
2421+
'wrapper_name': wrapper_name
24122422
}
24132423

2414-
if settings.MINIMAL_RUNTIME and not settings.PTHREADS:
2415-
# Single threaded MINIMAL_RUNTIME programs do not need access to
2416-
# document.currentScript, so a simple export declaration is enough.
2417-
src = f'/** @nocollapse */ var {settings.EXPORT_NAME} = {wrapper_function};'
2418-
else:
2424+
if add_script_name_wrapper:
24192425
script_url_node = ''
2420-
# When MODULARIZE this JS may be executed later,
2421-
# after document.currentScript is gone, so we save it.
2422-
# In EXPORT_ES6 + PTHREADS the 'thread' is actually an ES6 module
2423-
# webworker running in strict mode, so doesn't have access to 'document'.
2424-
# In this case use 'import.meta' instead.
2425-
if settings.EXPORT_ES6 and settings.USE_ES6_IMPORT_META:
2426-
script_url = 'import.meta.url'
2427-
else:
2428-
script_url = "typeof document != 'undefined' ? document.currentScript?.src : undefined"
2429-
if settings.ENVIRONMENT_MAY_BE_NODE:
2430-
script_url_node = "if (typeof __filename != 'undefined') _scriptName = _scriptName || __filename;"
2426+
# When MODULARIZE this JS may be executed later, after
2427+
# document.currentScript is gone, so we save it. This is only
2428+
# needed if the program actually requires `_scriptName`
2429+
script_url = "typeof document != 'undefined' ? document.currentScript?.src : undefined"
2430+
if settings.ENVIRONMENT_MAY_BE_NODE:
2431+
script_url_node = "if (typeof __filename != 'undefined') _scriptName = _scriptName || __filename;"
24312432
if settings.MODULARIZE == 'instance':
24322433
src = '''\
24332434
var _scriptName = %(script_url)s;
@@ -2443,14 +2444,33 @@ def modularize():
24432444
var %(EXPORT_NAME)s = (() => {
24442445
var _scriptName = %(script_url)s;
24452446
%(script_url_node)s
2446-
return (%(wrapper_function)s);
2447+
return %(wrapper_function)s
24472448
})();
24482449
''' % {
24492450
'EXPORT_NAME': settings.EXPORT_NAME,
24502451
'script_url': script_url,
24512452
'script_url_node': script_url_node,
24522453
'wrapper_function': wrapper_function,
24532454
}
2455+
elif add_assertion_wrapper:
2456+
src = '''\
2457+
var %(EXPORT_NAME)s = (() => {
2458+
%(wrapper_function)s
2459+
// Return a small, never-async wrapper around %(wrapper_name)s which
2460+
// checks for callers using `new`.
2461+
return function(arg) {
2462+
if (new.target) throw new Error("%(EXPORT_NAME)s() must not be called with new");
2463+
return %(wrapper_name)s(arg);
2464+
}
2465+
})();
2466+
''' % {
2467+
'EXPORT_NAME': settings.EXPORT_NAME,
2468+
'wrapper_function': wrapper_function,
2469+
'wrapper_name': wrapper_name,
2470+
}
2471+
else:
2472+
# No wrapper required. A simple export declaration is enough.
2473+
src = f'/** @nocollapse */ {wrapper_function};'
24542474

24552475
# Given the async nature of how the Module function and Module object
24562476
# come into existence in AudioWorkletGlobalScope, store the Module

0 commit comments

Comments
 (0)