From 97722271462ae56ae23fa2e2e93c3880220541c4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 17 Feb 2026 15:07:11 +0100 Subject: [PATCH] Add shared libraries support for Web to load game module (C++) --- .../Cooker/Platform/Web/WebPlatformTools.cpp | 15 ++++++++++++++- Source/Engine/Platform/Web/WebPlatform.cpp | 15 +++++++++++++-- Source/Tools/Flax.Build/Build/EngineTarget.cs | 6 +++++- .../Tools/Flax.Build/Platforms/Web/WebPlatform.cs | 7 +++++-- .../Flax.Build/Platforms/Web/WebToolchain.cs | 13 ++++++++++++- 5 files changed, 49 insertions(+), 7 deletions(-) diff --git a/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp b/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp index a2ad52b67..a9ac29503 100644 --- a/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp @@ -70,7 +70,7 @@ bool WebPlatformTools::OnPostProcess(CookingData& data) { Array files; FileSystem::DirectoryGetFiles(files, data.OriginalOutputPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly); - for (String& file : files) + for (const String& file : files) { if (file.EndsWith(TEXT(".js"))) { @@ -89,6 +89,19 @@ bool WebPlatformTools::OnPostProcess(CookingData& data) return true; } + // Move .wasm assemblies into the data files in order for dlopen to work (blocking) + { + Array files; + FileSystem::DirectoryGetFiles(files, data.OriginalOutputPath, TEXT("*.wasm"), DirectorySearchOption::AllDirectories); + StringView gameWasm = StringUtils::GetFileNameWithoutExtension(gameJs); + for (const String& file : files) + { + if (StringUtils::GetFileNameWithoutExtension(file) == gameWasm) + continue; // Skip the main game module + FileSystem::MoveFile(data.DataOutputPath / StringUtils::GetFileName(file), file, true); + } + } + // Pack data files into a single file using Emscripten's file_packager tool { CreateProcessSettings procSettings; diff --git a/Source/Engine/Platform/Web/WebPlatform.cpp b/Source/Engine/Platform/Web/WebPlatform.cpp index 822aa21e5..f9c703a22 100644 --- a/Source/Engine/Platform/Web/WebPlatform.cpp +++ b/Source/Engine/Platform/Web/WebPlatform.cpp @@ -11,11 +11,13 @@ #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Platform/CPUInfo.h" #include "Engine/Platform/MemoryStats.h" +#include "Engine/Profiler/ProfilerCPU.h" #if !BUILD_RELEASE #include "Engine/Core/Types/StringView.h" #include "Engine/Utilities/StringConverter.h" #endif #include +#include #include #include #include @@ -279,16 +281,25 @@ bool WebPlatform::SetEnvironmentVariable(const String& name, const String& value void* WebPlatform::LoadLibrary(const Char* filename) { - return nullptr; + PROFILE_CPU(); + ZoneText(filename, StringUtils::Length(filename)); + const StringAsANSI<> filenameANSI(filename); + void* result = dlopen(filenameANSI.Get(), RTLD_NOW); + if (!result) + { + LOG(Error, "Failed to load {0} because {1}", filename, String(dlerror())); + } + return result; } void WebPlatform::FreeLibrary(void* handle) { + dlclose(handle); } void* WebPlatform::GetProcAddress(void* handle, const char* symbol) { - return nullptr; + return dlsym(handle, symbol); } #endif diff --git a/Source/Tools/Flax.Build/Build/EngineTarget.cs b/Source/Tools/Flax.Build/Build/EngineTarget.cs index e22a924d9..8f20c9e8f 100644 --- a/Source/Tools/Flax.Build/Build/EngineTarget.cs +++ b/Source/Tools/Flax.Build/Build/EngineTarget.cs @@ -148,8 +148,12 @@ namespace Flax.Build { if (OutputType == TargetOutputType.Executable && !Configuration.BuildBindingsOnly) { - if (buildOptions.Platform.Target == TargetPlatform.Android) + switch (buildOptions.Platform.Target) + { + case TargetPlatform.Android: + case TargetPlatform.Web: return false; + } if (!buildOptions.Platform.HasModularBuildSupport) return false; return !IsMonolithicExecutable || (!buildOptions.Platform.HasExecutableFileReferenceSupport && UseSymbolsExports); diff --git a/Source/Tools/Flax.Build/Platforms/Web/WebPlatform.cs b/Source/Tools/Flax.Build/Platforms/Web/WebPlatform.cs index f2b67c0e1..9f184d472 100644 --- a/Source/Tools/Flax.Build/Platforms/Web/WebPlatform.cs +++ b/Source/Tools/Flax.Build/Platforms/Web/WebPlatform.cs @@ -17,10 +17,13 @@ namespace Flax.Build.Platforms public override bool HasRequiredSDKsInstalled { get; } /// - public override bool HasSharedLibrarySupport => false; + public override bool HasSharedLibrarySupport => true; /// - public override bool HasModularBuildSupport => false; + public override bool HasModularBuildSupport => true; + + /// + public override bool HasExecutableFileReferenceSupport => true; /// public override bool HasDynamicCodeExecutionSupport => false; diff --git a/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs b/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs index a39a50bb4..49f3dac8f 100644 --- a/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs @@ -132,6 +132,9 @@ namespace Flax.Build.Platforms if (options.LinkEnv.LinkTimeCodeGeneration) args.Add("-flto"); + if (options.LinkEnv.Output == LinkerOutput.SharedLibrary) + args.Add("-fPIC"); + var sanitizers = options.CompileEnv.Sanitizers; if (sanitizers.HasFlag(Sanitizer.Address)) args.Add("-fsanitize=address"); @@ -259,6 +262,13 @@ namespace Flax.Build.Platforms // Setup file access (Game Cooker packs files with file_packager tool) args.Add("-sFORCE_FILESYSTEM"); args.Add("-sLZ4"); + + // https://emscripten.org/docs/compiling/Dynamic-Linking.html#dynamic-linking + // TODO: use -sMAIN_MODULE=2 and -sSIDE_MODULE=2 to strip unused code (mark public APIs with EMSCRIPTEN_KEEPALIVE) + if (options.LinkEnv.Output == LinkerOutput.Executable) + args.Add("-sMAIN_MODULE"); + else + args.Add("-sSIDE_MODULE"); } args.Add("-Wl,--start-group"); @@ -266,6 +276,7 @@ namespace Flax.Build.Platforms // Input libraries var libraryPaths = new HashSet(); var dynamicLibExt = Platform.SharedLibraryFileExtension; + var executableExt = Platform.ExecutableFileExtension; foreach (var library in options.LinkEnv.InputLibraries.Concat(options.Libraries)) { var dir = Path.GetDirectoryName(library); @@ -279,7 +290,7 @@ namespace Flax.Build.Platforms { args.Add(string.Format("\"-l{0}\"", library)); } - else if (string.IsNullOrEmpty(ext)) + else if (ext == executableExt) { // Skip executable }