KDAB / cxx-qt

Safe interop between Rust and Qt
https://kdab.github.io/cxx-qt/book/
978 stars 67 forks source link

WASM Example #414

Open azzamsa opened 1 year ago

azzamsa commented 1 year ago

Hi.

Qt now supports compiling to WASM. Are we able to do this with CXX-Qt? If this is true. I think we need to add a WASM example.

Thanks for CXX-Qt.

ahayzen-kdab commented 1 year ago

None of us have tried yet, but i think it would be interesting to explore in the future.

If a normal CMake Qt project can be built then it should be possible once the right Rust toolchain is picked. Or maybe there is potentially a route with a cargo build too.

maciek134 commented 1 year ago

I have a Qt WebAssembly project I'd be interested to integrate Rust into. Starting with a PoC sounds sensible so I'll try to provide an example if nobody beats me to it.

maciek134 commented 1 year ago

Small update, Qt 5 seems like too much pain - 5.15 uses emscripten 1.39.8, which uses llvm 11, while rust is now at llvm 15. As I understand it such a big difference is asking for trouble (and I get relocation errors when linking anyway). Downgrading to rust 1.51.0 that used llvm 11 has another set of issues I don't have time to deal with :D

Qt 6 seems much more promising, since 6.4 uses emscripten 3.1.14 with llvm 15 so I'll try that later.

maciek134 commented 1 year ago

Based on the qml_minimal example, built with Qt 6.4.2, emscripten 3.1.14 and rust 1.67.0 with wasm32-unknown-emscripten: image Success, but:

I had to use target_link_options(${APP_NAME}_lib INTERFACE "SHELL:-Wl,--whole-archive $<TARGET_FILE:${CRATE}-static> -Wl,--no-whole-archive") instead of "$<LINK_LIBRARY:WHOLE_ARCHIVE,${CRATE}-static>", otherwise CMake complained that

Feature 'WHOLE_ARCHIVE', specified through generator-expression '$' to link target 'example_qml_minimal', is not supported for the 'CXX' link language.

Remove QmlImportScanner from Qt components (and register the object in C++) since it's not present there in cross-builds (there is probably a way to include the x86 one but it's almost 4am :)

And (the bad one) I had to comment out the assert_alignment_and_size macro to get cxx-qt-lib to compile, otherwise I get

cargo:warning=src/types/qcolor.cpp:17:1: error: static_assert failed due to requirement 'sizeof(QColor) == (sizeof(unsigned long[2]))' "unexpected QColor size!"
  cargo:warning=assert_alignment_and_size(QColor, alignof(std::size_t), sizeof(std::size_t[2]));
  cargo:warning=^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  cargo:warning=src/types/assertion_utils.h:13:3: note: expanded from macro 'assert_alignment_and_size'
  cargo:warning=  static_assert(sizeof(TYPE) == (SIZE), "unexpected " #TYPE " size!");
  cargo:warning=  ^             ~~~~~~~~~~~~~~~~~~~~~~
  cargo:warning=src/types/qvariant.cpp:27:1: error: static_assert failed due to requirement 'alignof(QVariant) <= (alignof(unsigned long))' "unexpectedly large QVariant alignment!"
  cargo:warning=assert_alignment_and_size(QVariant,
  cargo:warning=^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  cargo:warning=src/types/assertion_utils.h:11:3: note: expanded from macro 'assert_alignment_and_size'
  cargo:warning=  static_assert(alignof(TYPE) <= (ALIGNMENT),                                  \
  cargo:warning=  ^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  cargo:warning=src/types/qvariant.cpp:27:1: error: static_assert failed due to requirement 'sizeof(QVariant) == (sizeof(unsigned long[4]))' "unexpected QVariant size!"
  cargo:warning=assert_alignment_and_size(QVariant,
  cargo:warning=^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  cargo:warning=src/types/assertion_utils.h:13:3: note: expanded from macro 'assert_alignment_and_size'
  cargo:warning=  static_assert(sizeof(TYPE) == (SIZE), "unexpected " #TYPE " size!");
  cargo:warning=  ^             ~~~~~~~~~~~~~~~~~~~~~~
  cargo:warning=2 errors generated.

I'll investigate why the assertions fail next, I guess something to do with wasm being 32bit only?

ahayzen-kdab commented 1 year ago

Awesome progress! The CMake issue is related to #442. The alignment issue could be fun with qreal and pointers how they could be 32bit or 64bit (which could cause more fun with ifdefs and manual C++ bridges :-/ ). Seems unfortunate that wasm picks 32bit for some types by default when it has 64bit available :-/

maciek134 commented 1 year ago

Re #442 - I'm running CMake 3.25.2, the expression is recognized but for some reason not allowed when compiling with emscripten (the example compiles fine for x86_64)

wasm separates 32bit and 64bit completely, and I'm building for wasm32 since I don't think wasm64-unknown-emscripten even exists in Rust? But that is definitely the issue, assertions expect a pointer size of 8 bytes while it's 4 in wasm32. Since they count basic types, wouldn't it be better to assert byte counts rather than multiples of the pointer size? Unless this verifies an assumption that is made on the Rust side.

ahayzen-kdab commented 1 year ago

Re #442 - I'm running CMake 3.25.2, the expression is recognized but for some reason not allowed when compiling with emscripten (the example compiles fine for x86_64)

Hmm this probably needs discussing in that issue what solution can work everywhere :thinking: or whether there is another way around the WHOLE_ARCHIVE thing (cc @Be-ing ).

wasm separates 32bit and 64bit completely, and I'm building for wasm32 since I don't think wasm64-unknown-emscripten even exists in Rust? But that is definitely the issue, assertions expect a pointer size of 8 bytes while it's 4 in wasm32. Since they count basic types, wouldn't it be better to assert byte counts rather than multiples of the pointer size? Unless this verifies an assumption that is made on the Rust side.

Normally when the Qt types uses pointers internally we have N usize in rust as the size of the struct then in C++ we check that the size matches N size_t. But for QColor it uses a union of ushorts, so comparing usize is probably not correct in this case and should instead be (5 * u16) + u32 + padding.

Are you able to attach all the types that error when compiling so that we can understand if it's all types, or some that aren't quite asserted correctly ?

maciek134 commented 1 year ago

Are you able to attach all the types that error when compiling so that we can understand if it's all types, or some that aren't quite asserted correctly ?

Yup, will do after work. At a glance it seems it's only QColor and QVariant, but the compilation might have failed before it got to others.

ahayzen-kdab commented 1 year ago

Are you able to attach all the types that error when compiling so that we can understand if it's all types, or some that aren't quite asserted correctly ?

Yup, will do after work. At a glance it seems it's only QColor and QVariant, but the compilation might have failed before it got to others.

Thanks! That's hopefully good news as those two use unions and other types, but also are ones that we have used size_t to determine the size. When on a 32bit platform the size might be subtly different. When you have time it would be good to know what it thinks the actual size of these types is so then we can work out if we can calculate the size differently or if we need an ifdef + rust cfg for wasm/32bit.

Once things are building we should consider how/if to build for the wasm target in CI :thinking: so that we ensure it keeps building. Building for the Rust target itself seems possible, getting a build of Qt for wasm seems more fun :-)

maciek134 commented 1 year ago

For Qt5 there were docker images, I couldn't find one for Qt6 (though I didn't look too long) so I had to register on their website and use the installer. Thankfully I didn't need to recompile Qt itself since the prebuilt wasm32 toolkit is enough. I'll probably make a docker image myself since I prefer to work with it anyway.

Be-ing commented 1 year ago

I had to use target_link_options(${APP_NAME}_lib INTERFACE "SHELL:-Wl,--whole-archive $<TARGET_FILE:${CRATE}-static> -Wl,--no-whole-archive") instead of "$<LINK_LIBRARY:WHOLE_ARCHIVE,${CRATE}-static>", otherwise CMake complained that

Feature 'WHOLE_ARCHIVE', specified through generator-expression '$' to link target 'example_qml_minimal', is not supported for the 'CXX' link language.

Considering that manually specifying the linker arguments worked, this seems to only be a limitation of CMake, not the linker for WASM. Could you report this issue upstream on the CMake issue tracker?

Be-ing commented 1 year ago

There is a wasm32-emscripten vcpkg triplet. I have no idea if Qt builds with it though.

ahayzen-kdab commented 1 year ago

@maciek134 are you able to describe further some of your setup so that I can help debug issues?

I've got Qt wasm installed from 6.4.2, i've got emsdk installed, then with rustup it seems there is only a wasm32-unknown-emscripten target and not a toolchain ? (or i'm not getting the right combination). So I set the target to wasm32-unknown-emscript in ~/.cargo/config.

When i try to build this fails to build the first few crate dependencies with lots of errors, i think it might be trying to build some things for x86 still though :thinking:

Any ideas what i could be doing wrong?

I installed emscripten like this

cd ~/
git clone https://github.com/emscripten-core/emsdk.git .emsdk
cd .emsdk

./emsdk install latest
./emsdk activate latest

~/.cargo/config.toml looks like this

[build]
target = "wasm32-unknown-emscripten"

rustup show looks like this

Default host: x86_64-unknown-linux-gnu
rustup home:  /var/home/andrew/.rustup

installed targets for active toolchain
--------------------------------------

wasm32-unknown-emscripten
x86_64-unknown-linux-gnu

active toolchain
----------------

stable-x86_64-unknown-linux-gnu (default)
rustc 1.67.1 (d5a82bbd2 2023-02-07)

I'm building like this

source ~/.emsdk/emsdk_env.sh

mkdir -p build-wasm/
cd build-wasm/
~/.var/Qt/6.4.2/wasm_32/bin/qt-cmake ../
cmake --build .

The actual build itself fails with lots of unknown arguments / file types etc in wasm-ld when trying to compile basic crates like quote, syn etc.

ahayzen-kdab commented 1 year ago

Yes the .o files it is trying to use are ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), with debug_info, not stripped according to file. So something likely wrong in my rustup setup :thinking:

ahayzen-kdab commented 1 year ago

Aha, if i run $ cargo run --target wasm32-unknown-emscripten -p qml-minimal-no-cmake then it gets further so then i can see the size asserts issue at least and see if i can dig into those.

Does this mean that the CMake build with corrision is not passing something through correctly ? although i do see --target set in the errors when using CMake :thinking:

ahayzen-kdab commented 1 year ago

So the size changes in #447 now work on 32bit platforms like wasm. The next fun i have on my system is that something in the time methods is confused between 32bit and 64bit :-)

  = note: wasm-ld: error: function signature mismatch: time
          >>> defined as (i32) -> i32 in /var/home/andrew/.var/Qt/6.4.2/wasm_32/lib/libQt6Core.a(qlocaltime.cpp.o)
          >>> defined as (i32) -> i64 in /var/home/andrew/.emsdk/upstream/emscripten/cache/sysroot/lib/wasm32-emscripten/libc-debug.a(emscripten_time.o)

          wasm-ld: error: function signature mismatch: mktime
          >>> defined as (i32) -> i32 in /var/home/andrew/.var/Qt/6.4.2/wasm_32/lib/libQt6Core.a(qglobal.cpp.o)
          >>> defined as (i32) -> i64 in /var/home/andrew/.emsdk/upstream/emscripten/cache/sysroot/lib/wasm32-emscripten/libc-debug.a(emscripten_time.o)
          emcc: error: '/var/home/andrew/.emsdk/upstream/bin/wasm-ld @/tmp/emscripten_qs5iynpj.rsp.utf-8' failed (returned 1)
ahayzen-kdab commented 1 year ago

cargo run --target wasm32-unknown-emscripten -p qml-minimal-no-cmake

Moving to emsdk 3.1.14 changes the error to undefined symbols instead.

  = note: error: undefined symbol: emscripten_fetch (referenced by top-level compiled C/C++ code)
          warning: Link with `-sLLD_REPORT_UNDEFINED` to get more information on undefined symbols
          warning: To disable errors for undefined symbols use `-sERROR_ON_UNDEFINED_SYMBOLS=0`
          warning: _emscripten_fetch may need to be added to EXPORTED_FUNCTIONS if it arrives from a system library
          error: undefined symbol: emscripten_fetch_attr_init (referenced by top-level compiled C/C++ code)
          warning: _emscripten_fetch_attr_init may need to be added to EXPORTED_FUNCTIONS if it arrives from a system library
          error: undefined symbol: emscripten_fetch_close (referenced by top-level compiled C/C++ code)
          warning: _emscripten_fetch_close may need to be added to EXPORTED_FUNCTIONS if it arrives from a system library
          error: undefined symbol: emscripten_fetch_get_response_headers (referenced by top-level compiled C/C++ code)
          warning: _emscripten_fetch_get_response_headers may need to be added to EXPORTED_FUNCTIONS if it arrives from a system library
          error: undefined symbol: emscripten_fetch_get_response_headers_length (referenced by top-level compiled C/C++ code)
          warning: _emscripten_fetch_get_response_headers_length may need to be added to EXPORTED_FUNCTIONS if it arrives from a system library
          Error: Aborting compilation due to previous errors
          emcc: error: '/var/home/andrew/.emsdk/node/14.18.2_64bit/bin/node /var/home/andrew/.emsdk/upstream/emscripten/src/compiler.js /tmp/tmpcq6amavh.json' failed (returned 1)
Be-ing commented 1 year ago

Maybe there's a bug in the cc crate?

maciek134 commented 1 year ago

I'll give you steps I did later today, didn't have much time lately. I definitely set the target for corrosion through a CMake variable though.

maciek134 commented 1 year ago

This is what I did to the example (keep in mind I went the easy way and used the CMake one): https://github.com/maciek134/cxx-qt/commit/d8780440442122a891f3ce46e85ae7e1fbb6ca81 Updating cxx-qt to c48dca7 fixed the assertions too, so no ugly hacks anymore :)

I used emscripten 3.1.4, Qt 6.4.2 (Qt downloaded from their website, not recompiled). Built with

$ ~/Qt/6.4.2/wasm_32/bin/qt-cmake -B build .
$ cmake --build ./build

Something is still wrong with the order somewhere, since on first compilation I get

/home/klh/Workspace/qt6wasm/cpp/main.cpp:14:10: fatal error: 'cxx-qt-gen/my_object.cxxqt.h' file not found                                                                                                                                                                                
#include "cxx-qt-gen/my_object.cxxqt.h"
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

And I guess it will always be one compilation old after that.

maciek134 commented 1 year ago

@ahayzen-kdab

The next fun i have on my system is that something in the time methods is confused between 32bit and 64bit :-)

sounds like the emscripten libc is compiled with __TIMESIZE == 64 and Qt has the equivalent of __TIMESIZE == 32? Not sure why I don't get those though.

ahayzen-kdab commented 1 year ago

@ahayzen-kdab

The next fun i have on my system is that something in the time methods is confused between 32bit and 64bit :-)

sounds like the emscripten libc is compiled with __TIMESIZE == 64 and Qt has the equivalent of __TIMESIZE == 32? Not sure why I don't get those though.

This was when i was using the latest version of emsdk, switching to emsdk 3.1.14 solved that problem :-)

Thanks for the commit i'll see if that gets my CMake building. And we should look at why the order is wrong and requires two compilations :thinking:

maciek134 commented 1 year ago

For the no-cmake build, I think passing -sFETCH to emscripten's linker should solve your fetch issue (so I guess println!("cargo:rustc-link-arg=-sFETCH");?)

I can't get it to work though, first it tries to pick up my system Qt installation and those are x64 so it fails:

note: wasm-ld: error: unknown file type: /usr/lib/libQt6Qml.so
      wasm-ld: error: unknown file type: /usr/lib/libQt6Network.so
      wasm-ld: error: unknown file type: /usr/lib/libQt6Gui.so
      wasm-ld: error: unknown file type: /usr/lib/libQt6Core.so

So if I add my wasm_32 installation (println!("cargo:rustc-link-search=/home/klh/Qt/6.4.2/wasm_32/lib")) those disappear, but looks like it also tries to link GLX and OpenGL:

wasm-ld: error: unknown file type: /usr/lib/libGLX.so
wasm-ld: error: unknown file type: /usr/lib/libOpenGL.so

which I have no idea how to disable.

Be-ing commented 1 year ago

Set the QMAKE environment variable to the path of the qmake executable for your emscripten-built Qt to get cargo build to use the correct Qt installation.

maciek134 commented 1 year ago

Ok, that builds fine now, but it won't load with QtLoader:

Application exit (ReferenceError: createQtAppInstance is not defined)

~in CMake you have to use qt_add_executable instead of add_executable to get all the necessary helpers and exported methods, I guess ~/Qt/6.4.2/wasm_32/lib/cmake/Qt6Core/Qt6WasmMacros.cmake would have to be re-implemented in Rust, though that is pretty small. There is also ~/Qt/6.4.2/wasm_32/lib/cmake/Qt6Core/Qt6CoreMacros.cmake that probably has some needed stuff.~

That was not it, using just add_executable in CMake required only adding -sEXPORTED_RUNTIME_METHODS=UTF16ToString,stringToUTF16 to the linker flags, so something else is missing in the Rust-only build for the createQtAppInstance function to be defined/exported.

Ah, there we go: ~/Qt/6.4.2/wasm_32/lib/cmake/Qt6/QtWasmHelpers.cmake, also includes -sFETCH=1. Adding link options from this file made the app load, but now I get these in the console:

qtloader.js:370 qt.qpa.plugin: Could not find the Qt platform plugin "wasm" in ""
qtloader.js:370 This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem.
qtloader.js:370 Aborted(native code called abort())

Just to put all my research here, adding Q_IMPORT_PLUGIN(QWasmIntegrationPlugin) to QtBuild::register_qml_types and linking qwasm (had to add ~/Qt/6.4.2/wasm_32/plugins/platforms/ to the library search path) doesn't solve the issue.

jimmyvanhest commented 1 year ago

I have been testing with running qt with wasm, android and linux and recently I tried using rust for components with cxx-qt.

I got something working with which I'm not fully happy but at least it's something and wanted to share it here in the hope it might give someone a better insight in how to solve this properly.

What I have is a small skeleton project which will run and build distribution artifacts for Linux, android and WASM all invoked from a single make file as an central entry point. It will manage the Qt installation, android SDK etc for you. The process I used to build everything was to delegate most of it to qmake because it knows best how to finally put everything together.

when integrating rust components with cxx-qt in this project I had to create a fork with some changes to make it all work.

Below are a list of changes I made to cxx-qt to make everything work for me:

Things to note in general for the skeleton project(for which I would probably needed to make a README but didn't):

Small pain points:

ahayzen-kdab commented 1 year ago

@jimmyvanhest Awesome work ! It'd be great to try and get the changes in your fork of cxx-qt into upstream.

Are you able to submit each of the changes as separate pull requests ? (try to keep them as atomic / small as possible to aid reviews) Some of them sound like they can be fixes that are applied generally. Then others that are wasm specific, maybe we put those behind a feature ? We can also discuss the changes more clearly in pull requests.

ahayzen-kdab commented 1 year ago

Note to self, features are supposed to be additive, so we may want the features for wasm to be the opposite way around. Eg have QDateTime behind a feature to enable it rather than disable it. Then this could be enabled by default but for wasm builds they can choose to have no default features / a wasm subset of features that work etc.

jimmyvanhest commented 1 year ago

I have split out my work in 3 prs #599 #600 #601. If you have some issues with those let me know there.

ahayzen-kdab commented 1 year ago

Once we have a book / documentation of the build process ( cc @Be-ing ) and those changes are in, it'd be good to document the steps to make a wasm build so others can follow :-)

mneilly commented 9 months ago

@maciek134 FWIW, if I link QWasmIntegrationPlugin_init.cpp.o I get by the problem with not finding the platform plugin. Now, if only it could load my qml file from qrc... the same path works with native builds.

ahayzen-kdab commented 8 months ago

Wonder if the build changes to being a QML module and then the path changes for QML / qrc stuff helps at all there, or if just in general the qrc paths have issues :thinking:

mneilly commented 8 months ago

I got past the issue with not finding the qml file by linking the generated _*qml_moduleresources.qrc.o directly in build.rs. Not sure of the proper change yet. Now it fails with:

module "QtQuick.Controls" is not installed module "QtQuick.Window" is not installed module "QtQuick" is not installed

QtQuick appears to be absent from the generated .wasm file.

This is what I currently have (I also commented out the rust object in the QML file for now as it could not be found):

diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index bffc9042..2c364f2e 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -443,6 +443,11 @@ impl QtBuild {
         if emscripten_targeted {
             let platforms_path = format!("{}/platforms", self.qmake_query("QT_INSTALL_PLUGINS"));
             println!("cargo:rustc-link-search={platforms_path}");
+           println!("cargo:rustc-link-arg=-sFETCH=1");
+           println!("cargo:rustc-link-arg=-sEXPORT_NAME=createQtAppInstance");
+           println!("cargo:rustc-link-arg=-sMODULARIZE=1");
+           println!("cargo:rustc-link-arg=-sEXPORTED_RUNTIME_METHODS=UTF16ToString,stringToUTF16,JSEvents,specialHTMLTargets");
+           println!("cargo:rustc-link-arg=-sINITIAL_MEMORY=50MB");
             self.cargo_link_qt_library(
                 "qwasm",
                 &prefix_path,
@@ -870,6 +875,7 @@ Q_IMPORT_PLUGIN({plugin_class_name});
                 output_path.to_str().unwrap(),
                 "--name",
                 input_path.file_name().unwrap().to_str().unwrap(),
+               // Compiled Qt with -qt-zlib
+               "--no-zstd"
             ])
             .output()
             .unwrap_or_else(|_| panic!("rcc failed for {}", input_path.display()));
diff --git a/examples/cargo_without_cmake/build.rs b/examples/cargo_without_cmake/build.rs
index 79a91b6a..0d7de32c 100644
--- a/examples/cargo_without_cmake/build.rs
+++ b/examples/cargo_without_cmake/build.rs
@@ -5,8 +5,22 @@

 // ANCHOR: book_cargo_executable_build_rs
 use cxx_qt_build::{CxxQtBuilder, QmlModule};
+use std::env;

 fn main() {
+
+    if env::var("CARGO_CFG_TARGET_OS").unwrap() == "emscripten" {
+       // To get past: qt.qpa.plugin: Could not find the Qt platform plugin "wasm" in ""
+       println!("cargo:rustc-link-arg=/usr/local/Qt-6.5.3-wasm/plugins/platforms/objects-Release/QWasmIntegrationPlugin_init/QWasmIntegrationPlugin_init.cpp.o");
+       // To get past: qrc:/qt/qml/com/kdab/cxx_qt/demo/qml/main.qml: No such file or directory
+       println!("cargo:rustc-link-arg=/home/mneilly/RustProjects/third_party/GUI/cxx-qt/target/wasm32-unknown-emscripten/debug/build/qml-minimal-no-cmake-10348596234362b9/out/92a3a0d8bc2d2b31-qml_module_resources.qrc.o");
+       
+       // Current errors:
+       // qrc:/qt/qml/com/kdab/cxx_qt/demo/qml/main.qml:10:1: module "QtQuick.Window" is not installed
+       // qrc:/qt/qml/com/kdab/cxx_qt/demo/qml/main.qml:9:1: module "QtQuick.Controls" is not installed
+       // qrc:/qt/qml/com/kdab/cxx_qt/demo/qml/main.qml:8:1: module "QtQuick" is not installed
+    }
+    
     CxxQtBuilder::new()
         // Link Qt's Network library
         // - Qt Core is always linked
@@ -14,6 +28,8 @@ fn main() {
         // - Qt Qml is linked by enabling the qt_qml Cargo feature (default).
         // - Qt Qml requires linking Qt Network on macOS
         .qt_module("Network")
+       .qt_module("Quick")
+       .qt_module("QuickControls2")
         .qml_module(QmlModule {
             uri: "com.kdab.cxx_qt.demo",
             rust_files: &["src/cxxqt_object.rs"],
mneilly commented 8 months ago

Ok, I got _cargo_withoutcmake working with e466628. These are not fixes but hacks to try and figure out what is and isn't missing/working.

image

There are a few things I ran into.

whole-archive isn't passed along to emscripten and llvm so the Qt libraries, qt-static-initializers and cxx-qt-generated get dropped in the wasm. I modified emsdk's emsdk/upstream/emscripten/tools/building.py to alter its arguments to use --whole-archive lib*.a --no-whole-archive. Presumably, if this can be provided in cxx-qt and make its way from cxx-qt to rustc, emscripten and llvm that would be the right thing to do.

I was also running into failures because libraries were included on the command line more than once and compilation failed with duplicate symbols so I had building.py filter out duplicates. The real fix is probably to find out why they are duplicated to begin with.

Using EMCC_DEBUG=1 leaves an rsp file in tmp that reveals the lack of whole-archive and the duplicate libs and include paths.

emscripten_g11mql6f.rsp.utf-8.txt

This is the change I made to emsdk's building.py:

*** building.py~        2022-11-07 12:04:30.000000000 -0800
--- building.py 2023-10-28 00:12:18.186423795 -0700
*************** def link_lld(args, target, external_symb
*** 240,245 ****
--- 240,264 ----
    # grouping.
    args = [a for a in args if a not in ('--start-group', '--end-group')]

+   new_args = []
+   tmp = {}
+   for arg in args:
+     if arg in tmp:
+       continue
+     tmp[arg] = True
+     new_args.append(arg)
+   args = new_args
+ 
+   new_args = []
+   for arg in args:
+     if arg.endswith(".a"):
+       new_args.append("--whole-archive")
+       new_args.append(arg)
+       new_args.append("--no-whole-archive")
+     else:
+       new_args.append(arg)
+   args = new_args
+   
    # Emscripten currently expects linkable output (SIDE_MODULE/MAIN_MODULE) to
    # include all archive contents.
    if settings.LINKABLE:

I'm using a bash script to run cargo and create the qtloader.js and html file from the Qt templates. Presumably, this is something cxx-qt should do.

These are the cxx-qt hacks I made. It primarily includes adding libraries and objects and providing some emscripten settings. I don't know how these should get included and what differs between wasm and native which obviously works...

diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs
index bffc9042..be2f9f64 100644
--- a/crates/qt-build-utils/src/lib.rs
+++ b/crates/qt-build-utils/src/lib.rs
@@ -443,6 +443,47 @@ impl QtBuild {
         if emscripten_targeted {
             let platforms_path = format!("{}/platforms", self.qmake_query("QT_INSTALL_PLUGINS"));
             println!("cargo:rustc-link-search={platforms_path}");
+            println!("cargo:rustc-link-arg=-sEXPORTED_RUNTIME_METHODS=UTF16ToString,stringToUTF16,JSEvents,specialHTMLTargets");
+            println!("cargo:rustc-link-arg=-sEXPORT_NAME=createQtAppInstance");
+            println!("cargo:rustc-link-arg=-sFETCH=1");
+            println!("cargo:rustc-link-arg=-sINITIAL_MEMORY=50MB");
+            println!("cargo:rustc-link-arg=-sALLOW_MEMORY_GROWTH");
+            println!("cargo:rustc-link-arg=-sASYNCIFY_IMPORTS=qt_asyncify_suspend_js,qt_asyncify_resume_js");
+            println!("cargo:rustc-link-arg=-sDISABLE_EXCEPTION_CATCHING=1");
+            println!("cargo:rustc-link-arg=-sERROR_ON_UNDEFINED_SYMBOLS=1");
+            println!("cargo:rustc-link-arg=-sMAX_WEBGL_VERSION=2");
+            println!("cargo:rustc-link-arg=-sMODULARIZE=1");
+            println!("cargo:rustc-link-arg=-sWASM_BIGINT=1");
+            // println!("cargo:rustc-link-arg=-sDEMANGLE_SUPPORT");
+
+            // These already come in through libcxx-qt-generated.a
+            // println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Controls/impl/objects-Release/QuickControls2Impl_resources_1/.rcc/qrc_qmake_QtQuick_Controls_impl.cpp.o");
+            // println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Controls/objects-Release/QuickControls2_resources_1/.rcc/qrc_qmake_QtQuick_Controls.cpp.o");
+            // println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Templates/objects-Release/QuickTemplates2_resources_1/.rcc/qrc_qmake_QtQuick_Templates.cpp.o");
+
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQml/Base/libqmlplugin.a");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQml/Base/objects-Release/qmlplugin_init/qmlplugin_init.cpp.o");
+
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Controls/Basic/impl/libqtquickcontrols2basicstyleimplplugin.a");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Controls/Basic/impl/objects-Release/qtquickcontrols2basicstyleimplplugin_init/qtquickcontrols2basicstyleimplplugin_init.cpp.o");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Controls/Basic/impl/objects-Release/qtquickcontrols2basicstyleimplplugin_resources_1/.rcc/qrc_qmake_QtQuick_Controls_Basic_impl.cpp.o");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Controls/Basic/libqtquickcontrols2basicstyleplugin.a");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Controls/Basic/objects-Release/qtquickcontrols2basicstyleplugin_init/qtquickcontrols2basicstyleplugin_init.cpp.o");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Controls/Basic/objects-Release/qtquickcontrols2basicstyleplugin_resources_1/.rcc/qrc_qmake_QtQuick_Controls_Basic.cpp.o");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Controls/Basic/objects-Release/qtquickcontrols2basicstyleplugin_resources_2/.rcc/qrc_qtquickcontrols2basicstyleplugin_raw_qml_0.cpp.o");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Controls/impl/libqtquickcontrols2implplugin.a");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Controls/impl/objects-Release/qtquickcontrols2implplugin_init/qtquickcontrols2implplugin_init.cpp.o");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Controls/libqtquickcontrols2plugin.a");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Controls/objects-Release/qtquickcontrols2plugin_init/qtquickcontrols2plugin_init.cpp.o");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Templates/libqtquicktemplates2plugin.a");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Templates/objects-Release/qtquicktemplates2plugin_init/qtquicktemplates2plugin_init.cpp.o");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Window/libquickwindowplugin.a");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/Window/objects-Release/quickwindow_init/quickwindow_init.cpp.o");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/libqtquick2plugin.a");
+            println!("cargo:rustc-link-arg={platforms_path}/../../qml/QtQuick/objects-Release/qtquick2plugin_init/qtquick2plugin_init.cpp.o");
+
+            println!("cargo:rustc-link-arg={platforms_path}/objects-Release/QWasmIntegrationPlugin_init/QWasmIntegrationPlugin_init.cpp.o");
+
             self.cargo_link_qt_library(
                 "qwasm",
                 &prefix_path,
@@ -870,6 +911,8 @@ Q_IMPORT_PLUGIN({plugin_class_name});
                 output_path.to_str().unwrap(),
                 "--name",
                 input_path.file_name().unwrap().to_str().unwrap(),
+               // Compiled Qt with -qt-zlib
+               "--no-zstd"
             ])
             .output()
             .unwrap_or_else(|_| panic!("rcc failed for {}", input_path.display()));

diff --git a/examples/cargo_without_cmake/build.rs b/examples/cargo_without_cmake/build.rs
index 79a91b6a..1ae4e3bf 100644
--- a/examples/cargo_without_cmake/build.rs
+++ b/examples/cargo_without_cmake/build.rs
@@ -14,6 +14,10 @@ fn main() {
         // - Qt Qml is linked by enabling the qt_qml Cargo feature (default).
         // - Qt Qml requires linking Qt Network on macOS
         .qt_module("Network")
+        .qt_module("Quick")
+        .qt_module("QuickControls2")
+        .qt_module("QuickControls2Impl")
+        .qt_module("QuickTemplates2")
         .qml_module(QmlModule {
             uri: "com.kdab.cxx_qt.demo",
             rust_files: &["src/cxxqt_object.rs"],

diff --git a/examples/cargo_without_cmake/build.sh b/examples/cargo_without_cmake/build.sh
new file mode 100755
index 00000000..1b5a2b6f
--- /dev/null
+++ b/examples/cargo_without_cmake/build.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+EMSDK=$HOME/CLionProjects/third_party/emsdk
+QT_DIR=/usr/local/Qt-6.5.3-wasm
+TARGET_DIR=../../target/wasm32-unknown-emscripten/debug/
+
+source ${EMSDK}/emsdk_env.sh
+
+#export EMCC_DEBUG=1
+export QMAKE=${QT_DIR}/bin/qmake
+cargo build --target wasm32-unknown-emscripten --verbose
+
+cp ${QT_DIR}/plugins/platforms/qtlogo.svg ${TARGET_DIR}
+cp ${QT_DIR}/plugins/platforms/qtloader.js ${TARGET_DIR}
+sed 's/@APPNAME@/qml_minimal_no_cmake/g' ${QT_DIR}/plugins/platforms/wasm_shell.html > ${TARGET_DIR}/qml_minimal_no_cmake.html
+cp ${TARGET_DIR}/qml-minimal-no-cmake.js ${TARGET_DIR}/qml_minimal_no_cmake.js 

diff --git a/examples/cargo_without_cmake/run.sh b/examples/cargo_without_cmake/run.sh
new file mode 100755
index 00000000..255e26ee
--- /dev/null
+++ b/examples/cargo_without_cmake/run.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+source ${EMSDK}/emsdk_env.sh
+cd ../../target/wasm32-unknown-emscripten/debug/
+emrun --browser=firefox qml_minimal_no_cmake.html
Be-ing commented 8 months ago

whole-archive isn't passed along to emscripten and llvm so the Qt libraries, qt-static-initializers and cxx-qt-generated get dropped in the wasm. I modified emsdk's emsdk/upstream/emscripten/tools/building.py to alter its arguments to use --whole-archive lib*.a --no-whole-archive. Presumably, if this can be provided in cxx-qt and make its way from cxx-qt to rustc, emscripten and llvm that would be the right thing to do.

This seems like an issue in rustc? Have you discussed this upstream?

Be-ing commented 8 months ago

I was also running into failures because libraries were included on the command line more than once and compilation failed with duplicate symbols so I had building.py filter out duplicates. The real fix is probably to find out why they are duplicated to begin with.

I think this problem will go away when +whole-archive is actually supported for WASM. Only the libraries that CXX-Qt specifies +whole-archive for should be linked with --whole-archive by the linker, not every .a file.