Browse Source

Merge pull request #44076 from Faless/js/4.x_gdnative

[HTML5] Optional GDNative Support
Rémi Verschelde 4 years ago
parent
commit
e20011b0da

+ 2 - 0
misc/dist/html/full-size.html

@@ -145,6 +145,7 @@ $GODOT_HEAD_INCLUDE
 			const EXECUTABLE_NAME = '$GODOT_BASENAME';
 			const MAIN_PACK = '$GODOT_BASENAME.pck';
 			const EXTRA_ARGS = JSON.parse('$GODOT_ARGS');
+			const GDNATIVE_LIBS = [$GODOT_GDNATIVE_LIBS];
 			const INDETERMINATE_STATUS_STEP_MS = 100;
 			const FULL_WINDOW = $GODOT_FULL_WINDOW;
 
@@ -263,6 +264,7 @@ $GODOT_HEAD_INCLUDE
 			} else {
 				setStatusMode('indeterminate');
 				engine.setCanvas(canvas);
+				engine.setGDNativeLibraries(GDNATIVE_LIBS);
 				engine.startGame(EXECUTABLE_NAME, MAIN_PACK, EXTRA_ARGS).then(() => {
 					setStatusMode('hidden');
 					initializing = false;

+ 5 - 5
modules/gdnative/gdnative_library_editor_plugin.cpp

@@ -308,11 +308,11 @@ GDNativeLibraryEditor::GDNativeLibraryEditor() {
 		platform_android.library_extension = "*.so";
 		platforms["Android"] = platform_android;
 
-		//		TODO: Javascript platform is not supported yet
-		//		NativePlatformConfig platform_html5;
-		//		platform_html5.name = "HTML5";
-		//		platform_html5.library_extension = "*.wasm";
-		//		platforms["Javascript"] = platform_html5;
+		NativePlatformConfig platform_html5;
+		platform_html5.name = "HTML5";
+		platform_html5.entries.push_back("wasm32");
+		platform_html5.library_extension = "*.wasm";
+		platforms["HTML5"] = platform_html5;
 
 		NativePlatformConfig platform_ios;
 		platform_ios.name = "iOS";

+ 21 - 1
modules/webrtc/library_godot_webrtc.js

@@ -90,6 +90,7 @@ const GodotRTCDataChannel = {
 		},
 	},
 
+	godot_js_rtc_datachannel_ready_state_get__sig: 'ii',
 	godot_js_rtc_datachannel_ready_state_get: function (p_id) {
 		const ref = IDHandler.get(p_id);
 		if (!ref) {
@@ -109,6 +110,7 @@ const GodotRTCDataChannel = {
 		}
 	},
 
+	godot_js_rtc_datachannel_send__sig: 'iiiii',
 	godot_js_rtc_datachannel_send: function (p_id, p_buffer, p_length, p_raw) {
 		const ref = IDHandler.get(p_id);
 		if (!ref) {
@@ -129,14 +131,17 @@ const GodotRTCDataChannel = {
 		return 0;
 	},
 
+	godot_js_rtc_datachannel_is_ordered__sig: 'ii',
 	godot_js_rtc_datachannel_is_ordered: function (p_id) {
 		return IDHandler.get_prop(p_id, 'ordered', true);
 	},
 
+	godot_js_rtc_datachannel_id_get__sig: 'ii',
 	godot_js_rtc_datachannel_id_get: function (p_id) {
 		return IDHandler.get_prop(p_id, 'id', 65535);
 	},
 
+	godot_js_rtc_datachannel_max_packet_lifetime_get__sig: 'ii',
 	godot_js_rtc_datachannel_max_packet_lifetime_get: function (p_id) {
 		const ref = IDHandler.get(p_id);
 		if (!ref) {
@@ -151,14 +156,17 @@ const GodotRTCDataChannel = {
 		return 65535;
 	},
 
+	godot_js_rtc_datachannel_max_retransmits_get__sig: 'ii',
 	godot_js_rtc_datachannel_max_retransmits_get: function (p_id) {
 		return IDHandler.get_prop(p_id, 'maxRetransmits', 65535);
 	},
 
-	godot_js_rtc_datachannel_is_negotiated: function (p_id, p_def) {
+	godot_js_rtc_datachannel_is_negotiated__sig: 'ii',
+	godot_js_rtc_datachannel_is_negotiated: function (p_id) {
 		return IDHandler.get_prop(p_id, 'negotiated', 65535);
 	},
 
+	godot_js_rtc_datachannel_label_get__sig: 'ii',
 	godot_js_rtc_datachannel_label_get: function (p_id) {
 		const ref = IDHandler.get(p_id);
 		if (!ref || !ref.label) {
@@ -167,6 +175,7 @@ const GodotRTCDataChannel = {
 		return GodotRuntime.allocString(ref.label);
 	},
 
+	godot_js_rtc_datachannel_protocol_get__sig: 'ii',
 	godot_js_rtc_datachannel_protocol_get: function (p_id) {
 		const ref = IDHandler.get(p_id);
 		if (!ref || !ref.protocol) {
@@ -175,11 +184,13 @@ const GodotRTCDataChannel = {
 		return GodotRuntime.allocString(ref.protocol);
 	},
 
+	godot_js_rtc_datachannel_destroy__sig: 'vi',
 	godot_js_rtc_datachannel_destroy: function (p_id) {
 		GodotRTCDataChannel.close(p_id);
 		IDHandler.remove(p_id);
 	},
 
+	godot_js_rtc_datachannel_connect__sig: 'viiiiii',
 	godot_js_rtc_datachannel_connect: function (p_id, p_ref, p_on_open, p_on_message, p_on_error, p_on_close) {
 		const onopen = GodotRuntime.get_func(p_on_open).bind(null, p_ref);
 		const onmessage = GodotRuntime.get_func(p_on_message).bind(null, p_ref);
@@ -188,6 +199,7 @@ const GodotRTCDataChannel = {
 		GodotRTCDataChannel.connect(p_id, onopen, onmessage, onerror, onclose);
 	},
 
+	godot_js_rtc_datachannel_close__sig: 'vi',
 	godot_js_rtc_datachannel_close: function (p_id) {
 		const ref = IDHandler.get(p_id);
 		if (!ref) {
@@ -280,6 +292,7 @@ const GodotRTCPeerConnection = {
 		},
 	},
 
+	godot_js_rtc_pc_create__sig: 'iiiiii',
 	godot_js_rtc_pc_create: function (p_config, p_ref, p_on_state_change, p_on_candidate, p_on_datachannel) {
 		const onstatechange = GodotRuntime.get_func(p_on_state_change).bind(null, p_ref);
 		const oncandidate = GodotRuntime.get_func(p_on_candidate).bind(null, p_ref);
@@ -302,6 +315,7 @@ const GodotRTCPeerConnection = {
 		return id;
 	},
 
+	godot_js_rtc_pc_close__sig: 'vi',
 	godot_js_rtc_pc_close: function (p_id) {
 		const ref = IDHandler.get(p_id);
 		if (!ref) {
@@ -310,6 +324,7 @@ const GodotRTCPeerConnection = {
 		ref.close();
 	},
 
+	godot_js_rtc_pc_destroy__sig: 'vi',
 	godot_js_rtc_pc_destroy: function (p_id) {
 		const ref = IDHandler.get(p_id);
 		if (!ref) {
@@ -321,6 +336,7 @@ const GodotRTCPeerConnection = {
 		IDHandler.remove(p_id);
 	},
 
+	godot_js_rtc_pc_offer_create__sig: 'viiii',
 	godot_js_rtc_pc_offer_create: function (p_id, p_obj, p_on_session, p_on_error) {
 		const ref = IDHandler.get(p_id);
 		if (!ref) {
@@ -335,6 +351,7 @@ const GodotRTCPeerConnection = {
 		});
 	},
 
+	godot_js_rtc_pc_local_description_set__sig: 'viiiii',
 	godot_js_rtc_pc_local_description_set: function (p_id, p_type, p_sdp, p_obj, p_on_error) {
 		const ref = IDHandler.get(p_id);
 		if (!ref) {
@@ -351,6 +368,7 @@ const GodotRTCPeerConnection = {
 		});
 	},
 
+	godot_js_rtc_pc_remote_description_set__sig: 'viiiiii',
 	godot_js_rtc_pc_remote_description_set: function (p_id, p_type, p_sdp, p_obj, p_session_created, p_on_error) {
 		const ref = IDHandler.get(p_id);
 		if (!ref) {
@@ -375,6 +393,7 @@ const GodotRTCPeerConnection = {
 		});
 	},
 
+	godot_js_rtc_pc_ice_candidate_add__sig: 'viiii',
 	godot_js_rtc_pc_ice_candidate_add: function (p_id, p_mid_name, p_mline_idx, p_sdp) {
 		const ref = IDHandler.get(p_id);
 		if (!ref) {
@@ -390,6 +409,7 @@ const GodotRTCPeerConnection = {
 	},
 
 	godot_js_rtc_pc_datachannel_create__deps: ['$GodotRTCDataChannel'],
+	godot_js_rtc_pc_datachannel_create__sig: 'iiii',
 	godot_js_rtc_pc_datachannel_create: function (p_id, p_label, p_config) {
 		try {
 			const ref = IDHandler.get(p_id);

+ 4 - 0
modules/websocket/library_godot_websocket.js

@@ -135,6 +135,7 @@ const GodotWebSocket = {
 		},
 	},
 
+	godot_js_websocket_create__sig: 'iiiiiiii',
 	godot_js_websocket_create: function (p_ref, p_url, p_proto, p_on_open, p_on_message, p_on_error, p_on_close) {
 		const on_open = GodotRuntime.get_func(p_on_open).bind(null, p_ref);
 		const on_message = GodotRuntime.get_func(p_on_message).bind(null, p_ref);
@@ -156,6 +157,7 @@ const GodotWebSocket = {
 		return GodotWebSocket.create(socket, on_open, on_message, on_error, on_close);
 	},
 
+	godot_js_websocket_send__sig: 'iiiii',
 	godot_js_websocket_send: function (p_id, p_buf, p_buf_len, p_raw) {
 		const bytes_array = new Uint8Array(p_buf_len);
 		let i = 0;
@@ -169,12 +171,14 @@ const GodotWebSocket = {
 		return GodotWebSocket.send(p_id, out);
 	},
 
+	godot_js_websocket_close__sig: 'viii',
 	godot_js_websocket_close: function (p_id, p_code, p_reason) {
 		const code = p_code;
 		const reason = GodotRuntime.parseString(p_reason);
 		GodotWebSocket.close(p_id, code, reason);
 	},
 
+	godot_js_websocket_destroy__sig: 'vi',
 	godot_js_websocket_destroy: function (p_id) {
 		GodotWebSocket.destroy(p_id);
 	},

+ 48 - 14
platform/javascript/SCsub

@@ -12,13 +12,8 @@ javascript_files = [
     "api/javascript_tools_editor_plugin.cpp",
 ]
 
-build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"]
-if env["threads_enabled"]:
-    build_targets.append("#bin/godot${PROGSUFFIX}.worker.js")
-
-build = env.add_program(build_targets, javascript_files)
-
-env.AddJSLibraries(
+sys_env = env.Clone()
+sys_env.AddJSLibraries(
     [
         "js/libs/library_godot_audio.js",
         "js/libs/library_godot_display.js",
@@ -29,12 +24,48 @@ env.AddJSLibraries(
 )
 
 if env["tools"]:
-    env.AddJSLibraries(["js/libs/library_godot_editor_tools.js"])
+    sys_env.AddJSLibraries(["js/libs/library_godot_editor_tools.js"])
 if env["javascript_eval"]:
-    env.AddJSLibraries(["js/libs/library_godot_eval.js"])
-for lib in env["JS_LIBS"]:
-    env.Append(LINKFLAGS=["--js-library", lib])
-env.Depends(build, env["JS_LIBS"])
+    sys_env.AddJSLibraries(["js/libs/library_godot_eval.js"])
+for lib in sys_env["JS_LIBS"]:
+    sys_env.Append(LINKFLAGS=["--js-library", lib])
+
+build = []
+if env["gdnative_enabled"]:
+    build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"]
+    # Reset libraries. The main runtime will only link emscripten libraries, not godot ones.
+    sys_env["LIBS"] = []
+    # We use IDBFS. Since Emscripten 1.39.1 it needs to be linked explicitly.
+    sys_env.Append(LIBS=["idbfs.js"])
+    # JS prepended to the module code loading the side library.
+    sys_env.Append(LINKFLAGS=["--pre-js", sys_env.File("js/dynlink.pre.js")])
+    # Configure it as a main module (dynamic linking support).
+    sys_env.Append(CCFLAGS=["-s", "MAIN_MODULE=1"])
+    sys_env.Append(LINKFLAGS=["-s", "MAIN_MODULE=1"])
+    sys_env.Append(CCFLAGS=["-s", "EXPORT_ALL=1"])
+    sys_env.Append(LINKFLAGS=["-s", "EXPORT_ALL=1"])
+    # Force exporting the standard library (printf, malloc, etc.)
+    sys_env["ENV"]["EMCC_FORCE_STDLIBS"] = "libc,libc++,libc++abi"
+    # The main emscripten runtime, with exported standard libraries.
+    sys = sys_env.Program(build_targets, ["javascript_runtime.cpp"])
+    sys_env.Depends(sys, "js/dynlink.pre.js")
+
+    # The side library, containing all Godot code.
+    wasm_env = env.Clone()
+    wasm_env.Append(CPPDEFINES=["WASM_GDNATIVE"])  # So that OS knows it can run GDNative libraries.
+    wasm_env.Append(CCFLAGS=["-s", "SIDE_MODULE=2"])
+    wasm_env.Append(LINKFLAGS=["-s", "SIDE_MODULE=2"])
+    wasm = wasm_env.add_program("#bin/godot.side${PROGSUFFIX}.wasm", javascript_files)
+    build = [sys[0], sys[1], wasm[0]]
+else:
+    build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"]
+    if env["threads_enabled"]:
+        build_targets.append("#bin/godot${PROGSUFFIX}.worker.js")
+    # We use IDBFS. Since Emscripten 1.39.1 it needs to be linked explicitly.
+    sys_env.Append(LIBS=["idbfs.js"])
+    build = sys_env.Program(build_targets, javascript_files + ["javascript_runtime.cpp"])
+
+sys_env.Depends(build[0], sys_env["JS_LIBS"])
 
 engine = [
     "js/engine/preloader.js",
@@ -61,8 +92,11 @@ out_files = [
 ]
 html_file = "#misc/dist/html/editor.html" if env["tools"] else "#misc/dist/html/full-size.html"
 in_files = [js_wrapped, build[1], html_file, "#platform/javascript/js/libs/audio.worklet.js"]
-if env["threads_enabled"]:
-    in_files.append(build[2])
+if env["gdnative_enabled"]:
+    in_files.append(build[2])  # Runtime
+    out_files.append(zip_dir.File(binary_name + ".side.wasm"))
+elif env["threads_enabled"]:
+    in_files.append(build[2])  # Worker
     out_files.append(zip_dir.File(binary_name + ".worker.js"))
 
 zip_files = env.InstallAs(out_files, in_files)

+ 15 - 10
platform/javascript/detect.py

@@ -23,6 +23,7 @@ def get_opts():
         # eval() can be a security concern, so it can be disabled.
         BoolVariable("javascript_eval", "Enable JavaScript eval interface", True),
         BoolVariable("threads_enabled", "Enable WebAssembly Threads support (limited browser support)", False),
+        BoolVariable("gdnative_enabled", "Enable WebAssembly GDNative support (produces bigger binaries)", False),
         BoolVariable("use_closure_compiler", "Use closure compiler to minimize JavaScript code", False),
     ]
 
@@ -85,10 +86,8 @@ def configure(env):
 
     # LTO
     if env["use_lto"]:
-        env.Append(CCFLAGS=["-s", "WASM_OBJECT_FILES=0"])
-        env.Append(LINKFLAGS=["-s", "WASM_OBJECT_FILES=0"])
-        env.Append(CCFLAGS=["-flto"])
-        env.Append(LINKFLAGS=["-flto"])
+        env.Append(CCFLAGS=["-flto=full"])
+        env.Append(LINKFLAGS=["-flto=full"])
 
     # Closure compiler
     if env["use_closure_compiler"]:
@@ -135,6 +134,9 @@ def configure(env):
     if env["javascript_eval"]:
         env.Append(CPPDEFINES=["JAVASCRIPT_EVAL_ENABLED"])
 
+    if env["threads_enabled"] and env["gdnative_enabled"]:
+        raise Exception("Threads and GDNative support can't be both enabled due to WebAssembly limitations")
+
     # Thread support (via SharedArrayBuffer).
     if env["threads_enabled"]:
         env.Append(CPPDEFINES=["PTHREAD_NO_RENAME"])
@@ -146,14 +148,15 @@ def configure(env):
     else:
         env.Append(CPPDEFINES=["NO_THREADS"])
 
+    if env["gdnative_enabled"]:
+        env.Append(CCFLAGS=["-s", "RELOCATABLE=1"])
+        env.Append(LINKFLAGS=["-s", "RELOCATABLE=1"])
+        env.extra_suffix = ".gdnative" + env.extra_suffix
+
     # Reduce code size by generating less support code (e.g. skip NodeJS support).
     env.Append(LINKFLAGS=["-s", "ENVIRONMENT=web,worker"])
 
-    # We use IDBFS in javascript_main.cpp. Since Emscripten 1.39.1 it needs to
-    # be linked explicitly.
-    env.Append(LIBS=["idbfs.js"])
-
-    env.Append(LINKFLAGS=["-s", "BINARYEN=1"])
+    # Wrap the JavaScript support code around a closure named Godot.
     env.Append(LINKFLAGS=["-s", "MODULARIZE=1", "-s", "EXPORT_NAME='Godot'"])
 
     # Allow increasing memory buffer size during runtime. This is efficient
@@ -164,12 +167,14 @@ def configure(env):
     # This setting just makes WebGL 2 APIs available, it does NOT disable WebGL 1.
     env.Append(LINKFLAGS=["-s", "USE_WEBGL2=1"])
 
+    # Do not call main immediately when the support code is ready.
     env.Append(LINKFLAGS=["-s", "INVOKE_RUN=0"])
 
     # Allow use to take control of swapping WebGL buffers.
     env.Append(LINKFLAGS=["-s", "OFFSCREEN_FRAMEBUFFER=1"])
 
-    # callMain for manual start, FS for preloading, PATH and ERRNO_CODES for BrowserFS.
+    # callMain for manual start.
     env.Append(LINKFLAGS=["-s", "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain']"])
+
     # Add code that allow exiting runtime.
     env.Append(LINKFLAGS=["-s", "EXIT_RUNTIME=1"])

+ 83 - 23
platform/javascript/export/export.cpp

@@ -85,36 +85,44 @@ public:
 		// Wrong protocol
 		ERR_FAIL_COND_MSG(req[0] != "GET" || req[2] != "HTTP/1.1", "Invalid method or HTTP version.");
 
-		String filepath = EditorSettings::get_singleton()->get_cache_dir().plus_file("tmp_js_export");
+		const String cache_path = EditorSettings::get_singleton()->get_cache_dir();
 		const String basereq = "/tmp_js_export";
-		String ctype = "";
+		String filepath;
+		String ctype;
 		if (req[1] == basereq + ".html") {
-			filepath += ".html";
+			filepath = cache_path.plus_file(req[1].get_file());
 			ctype = "text/html";
 		} else if (req[1] == basereq + ".js") {
-			filepath += ".js";
+			filepath = cache_path.plus_file(req[1].get_file());
 			ctype = "application/javascript";
 		} else if (req[1] == basereq + ".audio.worklet.js") {
-			filepath += ".audio.worklet.js";
+			filepath = cache_path.plus_file(req[1].get_file());
 			ctype = "application/javascript";
 		} else if (req[1] == basereq + ".worker.js") {
-			filepath += ".worker.js";
+			filepath = cache_path.plus_file(req[1].get_file());
 			ctype = "application/javascript";
 		} else if (req[1] == basereq + ".pck") {
-			filepath += ".pck";
+			filepath = cache_path.plus_file(req[1].get_file());
 			ctype = "application/octet-stream";
 		} else if (req[1] == basereq + ".png" || req[1] == "/favicon.png") {
 			// Also allow serving the generated favicon for a smoother loading experience.
 			if (req[1] == "/favicon.png") {
 				filepath = EditorSettings::get_singleton()->get_cache_dir().plus_file("favicon.png");
 			} else {
-				filepath += ".png";
+				filepath = basereq + ".png";
 			}
 			ctype = "image/png";
+		} else if (req[1] == basereq + ".side.wasm") {
+			filepath = cache_path.plus_file(req[1].get_file());
+			ctype = "application/wasm";
 		} else if (req[1] == basereq + ".wasm") {
-			filepath += ".wasm";
+			filepath = cache_path.plus_file(req[1].get_file());
 			ctype = "application/wasm";
-		} else {
+		} else if (req[1].ends_with(".wasm")) {
+			filepath = cache_path.plus_file(req[1].get_file()); // TODO dangerous?
+			ctype = "application/wasm";
+		}
+		if (filepath.empty() || !FileAccess::exists(filepath)) {
 			String s = "HTTP/1.1 404 Not Found\r\n";
 			s += "Connection: Close\r\n";
 			s += "\r\n";
@@ -205,7 +213,33 @@ class EditorExportPlatformJavaScript : public EditorExportPlatform {
 	Ref<ImageTexture> stop_icon;
 	int menu_options;
 
-	void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags);
+	enum ExportMode {
+		EXPORT_MODE_NORMAL = 0,
+		EXPORT_MODE_THREADS = 1,
+		EXPORT_MODE_GDNATIVE = 2,
+	};
+
+	String _get_template_name(ExportMode p_mode, bool p_debug) const {
+		String name = "webassembly";
+		switch (p_mode) {
+			case EXPORT_MODE_THREADS:
+				name += "_threads";
+				break;
+			case EXPORT_MODE_GDNATIVE:
+				name += "_gdnative";
+				break;
+			default:
+				break;
+		}
+		if (p_debug) {
+			name += "_debug.zip";
+		} else {
+			name += "_release.zip";
+		}
+		return name;
+	}
+
+	void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects);
 
 private:
 	Ref<EditorHTTPServer> server;
@@ -250,7 +284,7 @@ public:
 	~EditorExportPlatformJavaScript();
 };
 
-void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags) {
+void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects) {
 	String str_template = String::utf8(reinterpret_cast<const char *>(p_html.ptr()), p_html.size());
 	String str_export;
 	Vector<String> lines = str_template.split("\n");
@@ -258,6 +292,10 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re
 	String flags_json;
 	gen_export_flags(flags, p_flags);
 	flags_json = JSON::print(flags);
+	String libs;
+	for (int i = 0; i < p_shared_objects.size(); i++) {
+		libs += "\"" + p_shared_objects[i].path.get_file() + "\",";
+	}
 
 	for (int i = 0; i < lines.size(); i++) {
 		String current_line = lines[i];
@@ -265,6 +303,7 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re
 		current_line = current_line.replace("$GODOT_PROJECT_NAME", ProjectSettings::get_singleton()->get_setting("application/config/name"));
 		current_line = current_line.replace("$GODOT_HEAD_INCLUDE", p_preset->get("html/head_include"));
 		current_line = current_line.replace("$GODOT_FULL_WINDOW", p_preset->get("html/full_window_size") ? "true" : "false");
+		current_line = current_line.replace("$GODOT_GDNATIVE_LIBS", libs);
 		current_line = current_line.replace("$GODOT_DEBUG_ENABLED", p_debug ? "true" : "false");
 		current_line = current_line.replace("$GODOT_ARGS", flags_json);
 		str_export += current_line + "\n";
@@ -291,12 +330,19 @@ void EditorExportPlatformJavaScript::get_preset_features(const Ref<EditorExportP
 			r_features->push_back("etc2");
 		}
 	}
+	ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type");
+	if (mode == EXPORT_MODE_THREADS) {
+		r_features->push_back("threads");
+	} else if (mode == EXPORT_MODE_GDNATIVE) {
+		r_features->push_back("wasm32");
+	}
 }
 
 void EditorExportPlatformJavaScript::get_export_options(List<ExportOption> *r_options) {
 	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
 	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
 
+	r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "variant/export_type", PROPERTY_HINT_ENUM, "Regular,Threads,GDNative"), 0)); // Export type.
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_desktop"), true)); // S3TC
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_mobile"), false)); // ETC or ETC2, depending on renderer
 
@@ -320,11 +366,11 @@ Ref<Texture2D> EditorExportPlatformJavaScript::get_logo() const {
 bool EditorExportPlatformJavaScript::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const {
 	String err;
 	bool valid = false;
+	ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type");
 
 	// Look for export templates (first official, and if defined custom templates).
-
-	bool dvalid = exists_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_DEBUG, &err);
-	bool rvalid = exists_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_RELEASE, &err);
+	bool dvalid = exists_export_template(_get_template_name(mode, true), &err);
+	bool rvalid = exists_export_template(_get_template_name(mode, false), &err);
 
 	if (p_preset->get("custom_template/debug") != "") {
 		dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
@@ -377,11 +423,8 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
 	template_path = template_path.strip_edges();
 
 	if (template_path == String()) {
-		if (p_debug) {
-			template_path = find_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_DEBUG);
-		} else {
-			template_path = find_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_RELEASE);
-		}
+		ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type");
+		template_path = find_export_template(_get_template_name(mode, p_debug));
 	}
 
 	if (!DirAccess::exists(p_path.get_base_dir())) {
@@ -393,12 +436,24 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
 		return ERR_FILE_NOT_FOUND;
 	}
 
+	Vector<SharedObject> shared_objects;
 	String pck_path = p_path.get_basename() + ".pck";
-	Error error = save_pack(p_preset, pck_path);
+	Error error = save_pack(p_preset, pck_path, &shared_objects);
 	if (error != OK) {
 		EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + pck_path);
 		return error;
 	}
+	DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+	for (int i = 0; i < shared_objects.size(); i++) {
+		String dst = p_path.get_base_dir().plus_file(shared_objects[i].path.get_file());
+		error = da->copy(shared_objects[i].path, dst);
+		if (error != OK) {
+			EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + shared_objects[i].path.get_file());
+			memdelete(da);
+			return error;
+		}
+	}
+	memdelete(da);
 
 	FileAccess *src_f = nullptr;
 	zlib_filefunc_def io = zipio_create_io_from_file(&src_f);
@@ -437,14 +492,18 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
 			if (!custom_html.empty()) {
 				continue;
 			}
-			_fix_html(data, p_preset, p_path.get_file().get_basename(), p_debug, p_flags);
+			_fix_html(data, p_preset, p_path.get_file().get_basename(), p_debug, p_flags, shared_objects);
 			file = p_path.get_file();
 
 		} else if (file == "godot.js") {
 			file = p_path.get_file().get_basename() + ".js";
+
 		} else if (file == "godot.worker.js") {
 			file = p_path.get_file().get_basename() + ".worker.js";
 
+		} else if (file == "godot.side.wasm") {
+			file = p_path.get_file().get_basename() + ".side.wasm";
+
 		} else if (file == "godot.audio.worklet.js") {
 			file = p_path.get_file().get_basename() + ".audio.worklet.js";
 
@@ -475,7 +534,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
 		buf.resize(f->get_len());
 		f->get_buffer(buf.ptrw(), buf.size());
 		memdelete(f);
-		_fix_html(buf, p_preset, p_path.get_file().get_basename(), p_debug, p_flags);
+		_fix_html(buf, p_preset, p_path.get_file().get_basename(), p_debug, p_flags, shared_objects);
 
 		f = FileAccess::open(p_path, FileAccess::WRITE);
 		if (!f) {
@@ -577,6 +636,7 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese
 		DirAccess::remove_file_or_error(basepath + ".audio.worklet.js");
 		DirAccess::remove_file_or_error(basepath + ".pck");
 		DirAccess::remove_file_or_error(basepath + ".png");
+		DirAccess::remove_file_or_error(basepath + ".side.wasm");
 		DirAccess::remove_file_or_error(basepath + ".wasm");
 		DirAccess::remove_file_or_error(EditorSettings::get_singleton()->get_cache_dir().plus_file("favicon.png"));
 		return err;

+ 1 - 1
platform/javascript/godot_audio.h

@@ -48,7 +48,7 @@ extern void godot_audio_capture_stop();
 typedef int32_t GodotAudioState[4];
 extern void godot_audio_worklet_create(int p_channels);
 extern void godot_audio_worklet_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, GodotAudioState p_state);
-extern void godot_audio_worklet_state_add(GodotAudioState p_state, int p_idx, int p_value);
+extern int godot_audio_worklet_state_add(GodotAudioState p_state, int p_idx, int p_value);
 extern int godot_audio_worklet_state_get(GodotAudioState p_state, int p_idx);
 extern int godot_audio_worklet_state_wait(int32_t *p_state, int p_idx, int32_t p_expected, int p_timeout);
 

+ 1 - 1
platform/javascript/http_request.h

@@ -47,7 +47,7 @@ typedef enum {
 
 extern int godot_xhr_new();
 extern void godot_xhr_reset(int p_xhr_id);
-extern bool godot_xhr_free(int p_xhr_id);
+extern void godot_xhr_free(int p_xhr_id);
 
 extern int godot_xhr_open(int p_xhr_id, const char *p_method, const char *p_url, const char *p_user = nullptr, const char *p_password = nullptr);
 

+ 1 - 1
platform/javascript/javascript_main.cpp

@@ -75,7 +75,7 @@ void main_loop_callback() {
 }
 
 /// When calling main, it is assumed FS is setup and synced.
-int main(int argc, char *argv[]) {
+extern EMSCRIPTEN_KEEPALIVE int godot_js_main(int argc, char *argv[]) {
 	os = new OS_JavaScript();
 
 	// We must override main when testing is enabled

+ 35 - 0
platform/javascript/javascript_runtime.cpp

@@ -0,0 +1,35 @@
+/*************************************************************************/
+/*  javascript_runtime.cpp                                               */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+extern int godot_js_main(int argc, char *argv[]);
+
+int main(int argc, char *argv[]) {
+	return godot_js_main(argc, argv);
+}

+ 1 - 0
platform/javascript/js/dynlink.pre.js

@@ -0,0 +1 @@
+Module['dynamicLibraries'] = [Module['thisProgram'] + '.side.wasm'].concat(Module['dynamicLibraries'] ? Module['dynamicLibraries'] : []);

+ 10 - 3
platform/javascript/js/engine/engine.js

@@ -34,6 +34,7 @@ const Engine = (function () {
 		this.onExecute = null;
 		this.onExit = null;
 		this.persistentPaths = ['/userfs'];
+		this.gdnativeLibs = [];
 	}
 
 	Engine.prototype.init = /** @param {string=} basePath */ function (basePath) {
@@ -58,6 +59,10 @@ const Engine = (function () {
 		initPromise = new Promise(function (resolve, reject) {
 			config['locateFile'] = Utils.createLocateRewrite(loadPath);
 			config['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise);
+			// Emscripten configuration.
+			config['thisProgram'] = me.executableName;
+			config['noExitRuntime'] = true;
+			config['dynamicLibraries'] = me.gdnativeLibs;
 			Godot(config).then(function (module) {
 				module['initFS'](me.persistentPaths).then(function (fs_err) {
 					me.rtenv = module;
@@ -119,9 +124,6 @@ const Engine = (function () {
 				locale = navigator.languages ? navigator.languages[0] : navigator.language;
 				locale = locale.split('.')[0];
 			}
-			// Emscripten configuration.
-			me.rtenv['thisProgram'] = me.executableName;
-			me.rtenv['noExitRuntime'] = true;
 			// Godot configuration.
 			me.rtenv['initConfig']({
 				'resizeCanvasOnStart': me.resizeCanvasOnStart,
@@ -249,6 +251,10 @@ const Engine = (function () {
 		this.persistentPaths = persistentPaths;
 	};
 
+	Engine.prototype.setGDNativeLibraries = function (gdnativeLibs) {
+		this.gdnativeLibs = gdnativeLibs;
+	};
+
 	Engine.prototype.requestQuit = function () {
 		if (this.rtenv) {
 			this.rtenv['request_quit']();
@@ -277,6 +283,7 @@ const Engine = (function () {
 	Engine.prototype['setOnExit'] = Engine.prototype.setOnExit;
 	Engine.prototype['copyToFS'] = Engine.prototype.copyToFS;
 	Engine.prototype['setPersistentPaths'] = Engine.prototype.setPersistentPaths;
+	Engine.prototype['setGDNativeLibraries'] = Engine.prototype.setGDNativeLibraries;
 	Engine.prototype['requestQuit'] = Engine.prototype.requestQuit;
 	return Engine;
 }());

+ 2 - 0
platform/javascript/js/engine/utils.js

@@ -8,6 +8,8 @@ const Utils = { // eslint-disable-line no-unused-vars
 				return `${execName}.audio.worklet.js`;
 			} else if (path.endsWith('.js')) {
 				return `${execName}.js`;
+			} else if (path.endsWith('.side.wasm')) {
+				return `${execName}.side.wasm`;
 			} else if (path.endsWith('.wasm')) {
 				return `${execName}.wasm`;
 			}

+ 12 - 0
platform/javascript/js/libs/library_godot_audio.js

@@ -137,6 +137,7 @@ const GodotAudio = {
 		},
 	},
 
+	godot_audio_is_available__sig: 'i',
 	godot_audio_is_available__proxy: 'sync',
 	godot_audio_is_available: function () {
 		if (!(window.AudioContext || window.webkitAudioContext)) {
@@ -145,12 +146,14 @@ const GodotAudio = {
 		return 1;
 	},
 
+	godot_audio_init__sig: 'iiiii',
 	godot_audio_init: function (p_mix_rate, p_latency, p_state_change, p_latency_update) {
 		const statechange = GodotRuntime.get_func(p_state_change);
 		const latencyupdate = GodotRuntime.get_func(p_latency_update);
 		return GodotAudio.init(p_mix_rate, p_latency, statechange, latencyupdate);
 	},
 
+	godot_audio_resume__sig: 'v',
 	godot_audio_resume: function () {
 		if (GodotAudio.ctx && GodotAudio.ctx.state !== 'running') {
 			GodotAudio.ctx.resume();
@@ -158,6 +161,7 @@ const GodotAudio = {
 	},
 
 	godot_audio_capture_start__proxy: 'sync',
+	godot_audio_capture_start__sig: 'v',
 	godot_audio_capture_start: function () {
 		if (GodotAudio.input) {
 			return; // Already started.
@@ -168,6 +172,7 @@ const GodotAudio = {
 	},
 
 	godot_audio_capture_stop__proxy: 'sync',
+	godot_audio_capture_stop__sig: 'v',
 	godot_audio_capture_stop: function () {
 		if (GodotAudio.input) {
 			const tracks = GodotAudio.input['mediaStream']['getTracks']();
@@ -241,10 +246,12 @@ const GodotAudioWorklet = {
 		},
 	},
 
+	godot_audio_worklet_create__sig: 'vi',
 	godot_audio_worklet_create: function (channels) {
 		GodotAudioWorklet.create(channels);
 	},
 
+	godot_audio_worklet_start__sig: 'viiiii',
 	godot_audio_worklet_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_state) {
 		const out_buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size);
 		const in_buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size);
@@ -252,15 +259,18 @@ const GodotAudioWorklet = {
 		GodotAudioWorklet.start(in_buffer, out_buffer, state);
 	},
 
+	godot_audio_worklet_state_wait__sig: 'iiii',
 	godot_audio_worklet_state_wait: function (p_state, p_idx, p_expected, p_timeout) {
 		Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout);
 		return Atomics.load(HEAP32, (p_state >> 2) + p_idx);
 	},
 
+	godot_audio_worklet_state_add__sig: 'iiii',
 	godot_audio_worklet_state_add: function (p_state, p_idx, p_value) {
 		return Atomics.add(HEAP32, (p_state >> 2) + p_idx, p_value);
 	},
 
+	godot_audio_worklet_state_get__sig: 'iii',
 	godot_audio_worklet_state_get: function (p_state, p_idx) {
 		return Atomics.load(HEAP32, (p_state >> 2) + p_idx);
 	},
@@ -330,10 +340,12 @@ const GodotAudioScript = {
 		},
 	},
 
+	godot_audio_script_create__sig: 'iii',
 	godot_audio_script_create: function (buffer_length, channel_count) {
 		return GodotAudioScript.create(buffer_length, channel_count);
 	},
 
+	godot_audio_script_start__sig: 'viiiii',
 	godot_audio_script_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_cb) {
 		const onprocess = GodotRuntime.get_func(p_cb);
 		GodotAudioScript.start(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess);

+ 19 - 0
platform/javascript/js/libs/library_godot_display.js

@@ -280,6 +280,7 @@ const GodotDisplay = {
 		window_icon: '',
 	},
 
+	godot_js_display_is_swap_ok_cancel__sig: 'i',
 	godot_js_display_is_swap_ok_cancel: function () {
 		const win = (['Windows', 'Win64', 'Win32', 'WinCE']);
 		const plat = navigator.platform || '';
@@ -289,10 +290,12 @@ const GodotDisplay = {
 		return 0;
 	},
 
+	godot_js_display_alert__sig: 'vi',
 	godot_js_display_alert: function (p_text) {
 		window.alert(GodotRuntime.parseString(p_text)); // eslint-disable-line no-alert
 	},
 
+	godot_js_display_pixel_ratio_get__sig: 'f',
 	godot_js_display_pixel_ratio_get: function () {
 		return window.devicePixelRatio || 1;
 	},
@@ -300,14 +303,17 @@ const GodotDisplay = {
 	/*
 	 * Canvas
 	 */
+	godot_js_display_canvas_focus__sig: 'v',
 	godot_js_display_canvas_focus: function () {
 		GodotConfig.canvas.focus();
 	},
 
+	godot_js_display_canvas_is_focused__sig: 'i',
 	godot_js_display_canvas_is_focused: function () {
 		return document.activeElement === GodotConfig.canvas;
 	},
 
+	godot_js_display_canvas_bounding_rect_position_get__sig: 'vii',
 	godot_js_display_canvas_bounding_rect_position_get: function (r_x, r_y) {
 		const brect = GodotConfig.canvas.getBoundingClientRect();
 		GodotRuntime.setHeapValue(r_x, brect.x, 'i32');
@@ -317,6 +323,7 @@ const GodotDisplay = {
 	/*
 	 * Touchscreen
 	 */
+	godot_js_display_touchscreen_is_available__sig: 'i',
 	godot_js_display_touchscreen_is_available: function () {
 		return 'ontouchstart' in window;
 	},
@@ -324,6 +331,7 @@ const GodotDisplay = {
 	/*
 	 * Clipboard
 	 */
+	godot_js_display_clipboard_set__sig: 'ii',
 	godot_js_display_clipboard_set: function (p_text) {
 		const text = GodotRuntime.parseString(p_text);
 		if (!navigator.clipboard || !navigator.clipboard.writeText) {
@@ -336,6 +344,7 @@ const GodotDisplay = {
 		return 0;
 	},
 
+	godot_js_display_clipboard_get__sig: 'ii',
 	godot_js_display_clipboard_get: function (callback) {
 		const func = GodotRuntime.get_func(callback);
 		try {
@@ -354,6 +363,7 @@ const GodotDisplay = {
 	/*
 	 * Window
 	 */
+	godot_js_display_window_request_fullscreen__sig: 'v',
 	godot_js_display_window_request_fullscreen: function () {
 		const canvas = GodotConfig.canvas;
 		(canvas.requestFullscreen || canvas.msRequestFullscreen
@@ -362,10 +372,12 @@ const GodotDisplay = {
 		).call(canvas);
 	},
 
+	godot_js_display_window_title_set__sig: 'vi',
 	godot_js_display_window_title_set: function (p_data) {
 		document.title = GodotRuntime.parseString(p_data);
 	},
 
+	godot_js_display_window_icon_set__sig: 'vii',
 	godot_js_display_window_icon_set: function (p_ptr, p_len) {
 		let link = document.getElementById('-gd-engine-icon');
 		if (link === null) {
@@ -386,6 +398,7 @@ const GodotDisplay = {
 	/*
 	 * Cursor
 	 */
+	godot_js_display_cursor_set_visible__sig: 'vi',
 	godot_js_display_cursor_set_visible: function (p_visible) {
 		const visible = p_visible !== 0;
 		if (visible === GodotDisplayCursor.visible) {
@@ -399,14 +412,17 @@ const GodotDisplay = {
 		}
 	},
 
+	godot_js_display_cursor_is_hidden__sig: 'i',
 	godot_js_display_cursor_is_hidden: function () {
 		return !GodotDisplayCursor.visible;
 	},
 
+	godot_js_display_cursor_set_shape__sig: 'vi',
 	godot_js_display_cursor_set_shape: function (p_string) {
 		GodotDisplayCursor.set_shape(GodotRuntime.parseString(p_string));
 	},
 
+	godot_js_display_cursor_set_custom_shape__sig: 'viiiii',
 	godot_js_display_cursor_set_custom_shape: function (p_shape, p_ptr, p_len, p_hotspot_x, p_hotspot_y) {
 		const shape = GodotRuntime.parseString(p_shape);
 		const old_shape = GodotDisplayCursor.cursors[shape];
@@ -432,6 +448,7 @@ const GodotDisplay = {
 	/*
 	 * Listeners
 	 */
+	godot_js_display_notification_cb__sig: 'viiiii',
 	godot_js_display_notification_cb: function (callback, p_enter, p_exit, p_in, p_out) {
 		const canvas = GodotConfig.canvas;
 		const func = GodotRuntime.get_func(callback);
@@ -443,6 +460,7 @@ const GodotDisplay = {
 		});
 	},
 
+	godot_js_display_paste_cb__sig: 'vi',
 	godot_js_display_paste_cb: function (callback) {
 		const func = GodotRuntime.get_func(callback);
 		GodotDisplayListeners.add(window, 'paste', function (evt) {
@@ -453,6 +471,7 @@ const GodotDisplay = {
 		}, false);
 	},
 
+	godot_js_display_drop_files_cb__sig: 'vi',
 	godot_js_display_drop_files_cb: function (callback) {
 		const func = GodotRuntime.get_func(callback);
 		const dropFiles = function (files) {

+ 1 - 0
platform/javascript/js/libs/library_godot_editor_tools.js

@@ -30,6 +30,7 @@
 
 const GodotEditorTools = {
 	godot_js_editor_download_file__deps: ['$FS'],
+	godot_js_editor_download_file__sig: 'viii',
 	godot_js_editor_download_file: function (p_path, p_name, p_mime) {
 		const path = GodotRuntime.parseString(p_path);
 		const name = GodotRuntime.parseString(p_name);

+ 1 - 0
platform/javascript/js/libs/library_godot_eval.js

@@ -30,6 +30,7 @@
 
 const GodotEval = {
 	godot_js_eval__deps: ['$GodotRuntime'],
+	godot_js_eval__sig: 'iiiiiii',
 	godot_js_eval: function (p_js, p_use_global_ctx, p_union_ptr, p_byte_arr, p_byte_arr_write, p_callback) {
 		const js_code = GodotRuntime.parseString(p_js);
 		let eval_ret = null;

+ 15 - 0
platform/javascript/js/libs/library_godot_http_request.js

@@ -50,6 +50,7 @@ const GodotHTTPRequest = {
 		},
 	},
 
+	godot_xhr_new__sig: 'i',
 	godot_xhr_new: function () {
 		const newId = GodotHTTPRequest.getUnusedRequestId();
 		GodotHTTPRequest.requests[newId] = new XMLHttpRequest();
@@ -57,30 +58,36 @@ const GodotHTTPRequest = {
 		return newId;
 	},
 
+	godot_xhr_reset__sig: 'vi',
 	godot_xhr_reset: function (xhrId) {
 		GodotHTTPRequest.requests[xhrId] = new XMLHttpRequest();
 		GodotHTTPRequest.setupRequest(GodotHTTPRequest.requests[xhrId]);
 	},
 
+	godot_xhr_free__sig: 'vi',
 	godot_xhr_free: function (xhrId) {
 		GodotHTTPRequest.requests[xhrId].abort();
 		GodotHTTPRequest.requests[xhrId] = null;
 	},
 
+	godot_xhr_open__sig: 'viiiii',
 	godot_xhr_open: function (xhrId, method, url, p_user, p_password) {
 		const user = p_user > 0 ? GodotRuntime.parseString(p_user) : null;
 		const password = p_password > 0 ? GodotRuntime.parseString(p_password) : null;
 		GodotHTTPRequest.requests[xhrId].open(GodotRuntime.parseString(method), GodotRuntime.parseString(url), true, user, password);
 	},
 
+	godot_xhr_set_request_header__sig: 'viii',
 	godot_xhr_set_request_header: function (xhrId, header, value) {
 		GodotHTTPRequest.requests[xhrId].setRequestHeader(GodotRuntime.parseString(header), GodotRuntime.parseString(value));
 	},
 
+	godot_xhr_send_null__sig: 'vi',
 	godot_xhr_send_null: function (xhrId) {
 		GodotHTTPRequest.requests[xhrId].send();
 	},
 
+	godot_xhr_send_string__sig: 'vii',
 	godot_xhr_send_string: function (xhrId, strPtr) {
 		if (!strPtr) {
 			GodotRuntime.error('Failed to send string per XHR: null pointer');
@@ -89,6 +96,7 @@ const GodotHTTPRequest = {
 		GodotHTTPRequest.requests[xhrId].send(GodotRuntime.parseString(strPtr));
 	},
 
+	godot_xhr_send_data__sig: 'viii',
 	godot_xhr_send_data: function (xhrId, ptr, len) {
 		if (!ptr) {
 			GodotRuntime.error('Failed to send data per XHR: null pointer');
@@ -101,23 +109,28 @@ const GodotHTTPRequest = {
 		GodotHTTPRequest.requests[xhrId].send(HEAPU8.subarray(ptr, ptr + len));
 	},
 
+	godot_xhr_abort__sig: 'vi',
 	godot_xhr_abort: function (xhrId) {
 		GodotHTTPRequest.requests[xhrId].abort();
 	},
 
+	godot_xhr_get_status__sig: 'ii',
 	godot_xhr_get_status: function (xhrId) {
 		return GodotHTTPRequest.requests[xhrId].status;
 	},
 
+	godot_xhr_get_ready_state__sig: 'ii',
 	godot_xhr_get_ready_state: function (xhrId) {
 		return GodotHTTPRequest.requests[xhrId].readyState;
 	},
 
+	godot_xhr_get_response_headers_length__sig: 'ii',
 	godot_xhr_get_response_headers_length: function (xhrId) {
 		const headers = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders();
 		return headers === null ? 0 : GodotRuntime.strlen(headers);
 	},
 
+	godot_xhr_get_response_headers__sig: 'viii',
 	godot_xhr_get_response_headers: function (xhrId, dst, len) {
 		const str = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders();
 		if (str === null) {
@@ -126,11 +139,13 @@ const GodotHTTPRequest = {
 		GodotRuntime.stringToHeap(str, dst, len);
 	},
 
+	godot_xhr_get_response_length__sig: 'ii',
 	godot_xhr_get_response_length: function (xhrId) {
 		const body = GodotHTTPRequest.requests[xhrId].response;
 		return body === null ? 0 : body.byteLength;
 	},
 
+	godot_xhr_get_response__sig: 'viii',
 	godot_xhr_get_response: function (xhrId, dst, len) {
 		let buf = GodotHTTPRequest.requests[xhrId].response;
 		if (buf === null) {

+ 9 - 0
platform/javascript/js/libs/library_godot_os.js

@@ -75,14 +75,17 @@ const GodotConfig = {
 		},
 	},
 
+	godot_js_config_canvas_id_get__sig: 'vii',
 	godot_js_config_canvas_id_get: function (p_ptr, p_ptr_max) {
 		GodotRuntime.stringToHeap(`#${GodotConfig.canvas.id}`, p_ptr, p_ptr_max);
 	},
 
+	godot_js_config_locale_get__sig: 'vii',
 	godot_js_config_locale_get: function (p_ptr, p_ptr_max) {
 		GodotRuntime.stringToHeap(GodotConfig.locale, p_ptr, p_ptr_max);
 	},
 
+	godot_js_config_is_resize_on_start__sig: 'i',
 	godot_js_config_is_resize_on_start: function () {
 		return GodotConfig.resize_on_start ? 1 : 0;
 	},
@@ -239,19 +242,23 @@ const GodotOS = {
 		},
 	},
 
+	godot_js_os_finish_async__sig: 'vi',
 	godot_js_os_finish_async: function (p_callback) {
 		const func = GodotRuntime.get_func(p_callback);
 		GodotOS.finish_async(func);
 	},
 
+	godot_js_os_request_quit_cb__sig: 'vi',
 	godot_js_os_request_quit_cb: function (p_callback) {
 		GodotOS.request_quit = GodotRuntime.get_func(p_callback);
 	},
 
+	godot_js_os_fs_is_persistent__sig: 'i',
 	godot_js_os_fs_is_persistent: function () {
 		return GodotFS.is_persistent();
 	},
 
+	godot_js_os_fs_sync__sig: 'vi',
 	godot_js_os_fs_sync: function (callback) {
 		const func = GodotRuntime.get_func(callback);
 		GodotOS._fs_sync_promise = GodotFS.sync();
@@ -260,6 +267,7 @@ const GodotOS = {
 		});
 	},
 
+	godot_js_os_execute__sig: 'ii',
 	godot_js_os_execute: function (p_json) {
 		const json_args = GodotRuntime.parseString(p_json);
 		const args = JSON.parse(json_args);
@@ -270,6 +278,7 @@ const GodotOS = {
 		return 1;
 	},
 
+	godot_js_os_shell_open__sig: 'vi',
 	godot_js_os_shell_open: function (p_uri) {
 		window.open(GodotRuntime.parseString(p_uri), '_blank');
 	},

+ 22 - 2
platform/javascript/os_javascript.cpp

@@ -43,6 +43,7 @@
 #include "modules/websocket/remote_debugger_peer_websocket.h"
 #endif
 
+#include <dlfcn.h>
 #include <emscripten.h>
 #include <stdlib.h>
 
@@ -127,12 +128,24 @@ int OS_JavaScript::get_process_id() const {
 }
 
 bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) {
-	if (p_feature == "HTML5" || p_feature == "web")
+	if (p_feature == "HTML5" || p_feature == "web") {
 		return true;
+	}
 
 #ifdef JAVASCRIPT_EVAL_ENABLED
-	if (p_feature == "JavaScript")
+	if (p_feature == "JavaScript") {
+		return true;
+	}
+#endif
+#ifndef NO_THREADS
+	if (p_feature == "threads") {
 		return true;
+	}
+#endif
+#if WASM_GDNATIVE
+	if (p_feature == "wasm32") {
+		return true;
+	}
 #endif
 
 	return false;
@@ -187,6 +200,13 @@ bool OS_JavaScript::is_userfs_persistent() const {
 	return idb_available;
 }
 
+Error OS_JavaScript::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) {
+	String path = p_path.get_file();
+	p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW);
+	ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ". Error: " + dlerror());
+	return OK;
+}
+
 OS_JavaScript *OS_JavaScript::get_singleton() {
 	return static_cast<OS_JavaScript *>(OS::get_singleton());
 }

+ 1 - 0
platform/javascript/os_javascript.h

@@ -87,6 +87,7 @@ public:
 	String get_user_data_dir() const override;
 
 	bool is_userfs_persistent() const override;
+	Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) override;
 
 	void resume_audio();