From 2065db23e167df56fb509754502a6539552241db Mon Sep 17 00:00:00 2001 From: iphydf Date: Tue, 28 Jan 2025 23:21:30 +0000 Subject: [PATCH] feat(Web): Preliminary support for running qTox in the browser. --- .github/workflows/build-test-deploy.yaml | 28 +++++++++++++++++ CMakeLists.txt | 11 +++++-- audio/src/backend/openal.cpp | 10 +++--- cmake/Dependencies.cmake | 8 ++++- docker-compose.yml | 3 ++ platform/wasm/build.sh | 17 +++++++++++ src/appmanager.cpp | 39 +++++++++++++++++++++++- src/core/toxlogger.cpp | 4 ++- src/ipc.cpp | 22 ++++++------- src/ipc.h | 2 +- src/main.cpp | 7 +++-- src/persistence/profilelocker.cpp | 1 + src/platform/posixsignalnotifier.cpp | 2 +- 13 files changed, 128 insertions(+), 26 deletions(-) create mode 100755 platform/wasm/build.sh diff --git a/.github/workflows/build-test-deploy.yaml b/.github/workflows/build-test-deploy.yaml index 0f8e50a9eb..d68a4635b8 100644 --- a/.github/workflows/build-test-deploy.yaml +++ b/.github/workflows/build-test-deploy.yaml @@ -712,6 +712,34 @@ jobs: - name: Build qTox run: .ci-scripts/build-qtox-macos.sh user "${{ matrix.arch }}" "12.0" + build-wasm: + name: WebAssembly + needs: [update-nightly-tag] + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Cache compiler output + uses: actions/cache@v4 + with: + path: ".cache/ccache" + key: ${{ github.job }}-ccache + - name: Run build + run: docker compose run --rm wasm-builder platform/wasm/build.sh + - name: Upload zip + uses: actions/upload-artifact@v4 + with: + name: qtox-wasm.zip + path: | + _build-wasm/qtloader.js + _build-wasm/qtlogo.svg + _build-wasm/qtox.html + _build-wasm/qtox.js + _build-wasm/qtox.wasm + build-windows: name: Windows needs: [update-nightly-tag] diff --git a/CMakeLists.txt b/CMakeLists.txt index 73d491455c..0c567cd8de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,8 +140,11 @@ endif() # Apply resource compilation options. set(CMAKE_AUTORCC_OPTIONS ${AUTORCC_OPTIONS}) -# Disable exceptions (Qt doesn't use them, we don't need them). -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions") +# Emscripten needs -fexceptions at link time. +if(NOT EMSCRIPTEN) + # Disable exceptions (Qt doesn't use them, we don't need them). + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions") +endif() # Hardening flags (ASLR, warnings, etc) set(CMAKE_POSITION_INDEPENDENT_CODE True) @@ -704,6 +707,10 @@ if(FULLY_STATIC) target_link_options(${PROJECT_NAME} PRIVATE -static-pie) endif() +if(EMSCRIPTEN) + target_link_options(${PROJECT_NAME} PRIVATE -sASYNCIFY -lidbfs.js) +endif() + if(BUILD_TESTING) include(Testing) endif() diff --git a/audio/src/backend/openal.cpp b/audio/src/backend/openal.cpp index a07bfde0fb..a17cfb6552 100644 --- a/audio/src/backend/openal.cpp +++ b/audio/src/backend/openal.cpp @@ -18,7 +18,7 @@ #include -#if defined(QT_STATIC) +#if defined(QT_STATIC) && !defined(Q_OS_WASM) extern "C" { typedef void alsoftLogCallback(void* userptr, char level, const char* message, int length) noexcept; @@ -28,7 +28,7 @@ extern "C" * @note This function is only available in statically linked builds where we know for sure we * link against openal-soft. */ - void alsoft_set_log_callback(alsoftLogCallback* callback, void* userptr) noexcept; + void al_set_log_callback(alsoftLogCallback* callback, void* userptr) noexcept; } #endif @@ -58,7 +58,7 @@ constexpr unsigned int BUFFER_COUNT = 16; constexpr uint32_t AUDIO_CHANNELS = 2; namespace logcat { -#if defined(QT_STATIC) +#if defined(QT_STATIC) && !defined(Q_OS_WASM) Q_LOGGING_CATEGORY(openal, "openal") #endif Q_LOGGING_CATEGORY(audio, "qtox.audio") @@ -69,8 +69,8 @@ OpenAL::OpenAL(IAudioSettings& _settings) : settings{_settings} , audioThread{new QThread} { -#if defined(QT_STATIC) - alsoft_set_log_callback( +#if defined(QT_STATIC) && !defined(Q_OS_WASM) + al_set_log_callback( [](void* userptr, char level, const char* message, int length) noexcept { std::ignore = userptr; // OpenAL passes .data() and .size() to the callback, diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index 22d37243b9..ab42e1fb72 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -133,6 +133,10 @@ search_dependency(LIBQRENCODE PACKAGE libqrencode) search_dependency(LIBSWSCALE PACKAGE libswscale) search_dependency(SQLCIPHER PACKAGE sqlcipher) +if(EMSCRIPTEN) + search_dependency(TOMCRYPT LIBRARY tomcrypt) +endif() + if(APPLE) search_dependency(LIBCRYPTO PACKAGE libcrypto) endif() @@ -212,7 +216,9 @@ if(NOT WIN32) endif() if(QT_FEATURE_static) - add_dependency(Qt6::QOffscreenIntegrationPlugin) + if(NOT EMSCRIPTEN) + add_dependency(Qt6::QOffscreenIntegrationPlugin) + endif() if(LINUX) add_dependency( Qt6::QLinuxFbIntegrationPlugin diff --git a/docker-compose.yml b/docker-compose.yml index d745b200c2..f10afe3e40 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,3 +74,6 @@ services: windows_builder.x86_64: image: toxchat/qtox:windows-builder.x86_64 <<: *shared_params + wasm_builder: + image: toxchat/qtox:wasm-builder + <<: *shared_params diff --git a/platform/wasm/build.sh b/platform/wasm/build.sh new file mode 100755 index 0000000000..bc5650a23d --- /dev/null +++ b/platform/wasm/build.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +source "/work/emsdk/emsdk_env.sh" + +export PKG_CONFIG_PATH="/work/lib/pkgconfig" + +emcmake cmake \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -DCMAKE_FIND_ROOT_PATH="/work;/work/qt" \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DSTRICT_OPTIONS=ON \ + -DBUILD_TESTING=OFF \ + -GNinja \ + -B_build-wasm \ + -H. + +cmake --build _build-wasm diff --git a/src/appmanager.cpp b/src/appmanager.cpp index 5cea1fdc38..cfa602a95c 100644 --- a/src/appmanager.cpp +++ b/src/appmanager.cpp @@ -28,6 +28,34 @@ #include +#ifdef Q_OS_WASM +#include + +namespace { +const QString wasmConfigPath = QStringLiteral("/home/web_user/qTox"); + +bool mountIndexedDbFilesystem() +{ + EM_ASM(console.log('Creating config directory: /home/web_user/qTox'); + FS.mkdir('/home/web_user/qTox'); + // TODO(iphydf): Figure out why this blocks profile creation. + // console.log('Mounting IndexedDB filesystem to /home/web_user/qTox'); + // FS.mount(IDBFS, {}, '/home/web_user/qTox'); + // console.log('Syncing filesystem'); + // FS.syncfs(true, function(err) { + // if (err) { + // console.error('Failed to sync filesystem:', err); + // } else { + // console.log('Filesystem mounted'); + // } + // }); + ); + + return true; +} +} // namespace +#endif + namespace { // logMessageHandler and associated data must be static due to qInstallMessageHandler's // inability to register a void* to get back to a class @@ -249,6 +277,10 @@ int AppManager::startGui(QCommandLineParser& parser) logFileFile.storeRelaxed(mainLogFilePtr); // atomically set the logFile #endif +#ifdef Q_OS_WASM + mountIndexedDbFilesystem(); +#endif + // Windows platform plugins DLL hell fix QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath()); QApplication::addLibraryPath("platforms"); @@ -421,12 +453,17 @@ int AppManager::run() {{"u", "update-check"}, tr("Checks whether this program is running the latest qTox version.")}, #endif // UPDATE_CHECK_ENABLED }); +#ifdef Q_OS_WASM + // Set to portable mode and TCP-only for WASM. + parser.process({"qtox", "-D", wasmConfigPath, "-U", "off", "-L", "off"}); +#else parser.process(*qapp); +#endif if (parser.isSet("portable")) { // We don't go through settings here, because we're not making qTox // portable (which moves files around). Instead, we start up in - // portable mode as a one-off. + // portable mode from the beginning without having to move any files. settings->getPaths().setPortable(true); settings->getPaths().setPortablePath(parser.value("portable")); } diff --git a/src/core/toxlogger.cpp b/src/core/toxlogger.cpp index a65608a9ce..c902aed0cd 100644 --- a/src/core/toxlogger.cpp +++ b/src/core/toxlogger.cpp @@ -29,7 +29,9 @@ void onLogMessage(Tox* tox, Tox_Log_Level level, const char* file, uint32_t line switch (level) { case TOX_LOG_LEVEL_TRACE: - return; // trace level generates too much noise to enable by default + // trace level generates too much noise to enable by default + QMessageLogger(file, line, func).debug(toxcore) << message; + return; case TOX_LOG_LEVEL_DEBUG: QMessageLogger(file, line, func).debug(toxcore) << message; break; diff --git a/src/ipc.cpp b/src/ipc.cpp index 036bc27506..214db9fd05 100644 --- a/src/ipc.cpp +++ b/src/ipc.cpp @@ -18,7 +18,7 @@ #endif namespace { -#ifndef ANDROID +#if QT_CONFIG(sharedmemory) #ifdef Q_OS_WIN const char* getCurUsername() { @@ -58,7 +58,7 @@ QString getIpcKey() IPC::IPC(uint32_t profileId_) : profileId{profileId_} -#ifndef ANDROID +#if QT_CONFIG(sharedmemory) , globalMemory{getIpcKey()} #endif { @@ -81,7 +81,7 @@ IPC::IPC(uint32_t profileId_) std::uniform_int_distribution distribution; globalId = distribution(rng); qDebug() << "Our global IPC ID is" << globalId; -#ifdef ANDROID +#if !QT_CONFIG(sharedmemory) return; #else if (globalMemory.create(sizeof(IPCMemory))) { @@ -108,7 +108,7 @@ IPC::IPC(uint32_t profileId_) IPC::~IPC() { -#ifndef ANDROID +#if QT_CONFIG(sharedmemory) if (!globalMemory.lock()) { qWarning() << "Failed to lock in ~IPC"; return; @@ -139,7 +139,7 @@ time_t IPC::postEvent(const QString& name, const QByteArray& data, uint32_t dest return 0; } -#ifdef ANDROID +#if !QT_CONFIG(sharedmemory) std::ignore = dest; return 0; #else @@ -175,7 +175,7 @@ time_t IPC::postEvent(const QString& name, const QByteArray& data, uint32_t dest bool IPC::isCurrentOwner() { -#ifdef ANDROID +#if !QT_CONFIG(sharedmemory) return false; #else if (globalMemory.lock()) { @@ -207,7 +207,7 @@ void IPC::unregisterEventHandler(const QString& name) bool IPC::isEventAccepted(time_t time) { -#ifdef ANDROID +#if !QT_CONFIG(sharedmemory) std::ignore = time; return false; #else @@ -250,7 +250,7 @@ bool IPC::waitUntilAccepted(time_t postTime, int32_t timeout /*=-1*/) bool IPC::isAttached() const { -#ifdef ANDROID +#if !QT_CONFIG(sharedmemory) return false; #else return globalMemory.isAttached(); @@ -305,7 +305,7 @@ bool IPC::runEventHandler(IPCEventHandler handler, const QByteArray& arg, void* void IPC::processEvents() { -#ifdef ANDROID +#if !QT_CONFIG(sharedmemory) timer.start(); return; #else @@ -368,7 +368,7 @@ void IPC::processEvents() */ bool IPC::isCurrentOwnerNoLock() { -#ifdef ANDROID +#if !QT_CONFIG(sharedmemory) return false; #else const void* const data = globalMemory.data(); @@ -382,7 +382,7 @@ bool IPC::isCurrentOwnerNoLock() IPC::IPCMemory* IPC::global() { -#ifdef ANDROID +#if !QT_CONFIG(sharedmemory) return nullptr; #else return static_cast(globalMemory.data()); diff --git a/src/ipc.h b/src/ipc.h index f47e319eb2..ca935a8f5b 100644 --- a/src/ipc.h +++ b/src/ipc.h @@ -81,7 +81,7 @@ public slots: QTimer timer; uint64_t globalId; uint32_t profileId; -#ifndef ANDROID +#if QT_CONFIG(sharedmemory) QSharedMemory globalMemory; #endif mutable std::mutex eventHandlersMutex; diff --git a/src/main.cpp b/src/main.cpp index 50565fbce8..0411005dde 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,16 +22,17 @@ int main(int argc, char* argv[]) } #ifdef QT_STATIC -// All platforms support offscreen rendering for testing. -Q_IMPORT_PLUGIN(QOffscreenIntegrationPlugin) - #if defined(Q_OS_LINUX) Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin) +Q_IMPORT_PLUGIN(QOffscreenIntegrationPlugin) Q_IMPORT_PLUGIN(QVncIntegrationPlugin) Q_IMPORT_PLUGIN(QWaylandIntegrationPlugin) Q_IMPORT_PLUGIN(QXcbIntegrationPlugin) #elif defined(Q_OS_MACOS) Q_IMPORT_PLUGIN(QCocoaIntegrationPlugin) +Q_IMPORT_PLUGIN(QOffscreenIntegrationPlugin) +#elif defined(Q_OS_WASM) +Q_IMPORT_PLUGIN(QWasmIntegrationPlugin) #else #error "No static linking supported for platform" #endif diff --git a/src/persistence/profilelocker.cpp b/src/persistence/profilelocker.cpp index cf19eb9c1a..663e3c71f5 100644 --- a/src/persistence/profilelocker.cpp +++ b/src/persistence/profilelocker.cpp @@ -10,6 +10,7 @@ #include #include +#include /** * @class ProfileLocker diff --git a/src/platform/posixsignalnotifier.cpp b/src/platform/posixsignalnotifier.cpp index 31553b9d11..b14b972494 100644 --- a/src/platform/posixsignalnotifier.cpp +++ b/src/platform/posixsignalnotifier.cpp @@ -5,7 +5,7 @@ #include "posixsignalnotifier.h" -#ifndef Q_OS_WIN +#if !defined(Q_OS_WIN) && !defined(Q_OS_WASM) #include "src/platform/stacktrace.h" #include