Skip to content

WASM Example #414

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
azzamsa opened this issue Jan 14, 2023 · 37 comments · Fixed by #1074
Closed

WASM Example #414

azzamsa opened this issue Jan 14, 2023 · 37 comments · Fixed by #1074
Labels
📖 documentation Improvements or additions to documentation ⬆️ feature New feature or request ❔ help wanted Extra attention is needed

Comments

@azzamsa
Copy link

azzamsa commented Jan 14, 2023

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
Copy link
Collaborator

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.

@ahayzen-kdab ahayzen-kdab added 📖 documentation Improvements or additions to documentation ⬆️ feature New feature or request ❔ help wanted Extra attention is needed labels Jan 16, 2023
@maciek134
Copy link

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
Copy link

maciek134 commented Feb 12, 2023

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
Copy link

maciek134 commented Feb 13, 2023

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
'$<LINK_LIBRARY>' 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
Copy link
Collaborator

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
Copy link

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
Copy link
Collaborator

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 🤔 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
Copy link

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
Copy link
Collaborator

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 🤔 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
Copy link

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
Copy link
Contributor

Be-ing commented Feb 13, 2023

I had to use target_link_options(${APP_NAME}_lib INTERFACE "SHELL:-Wl,--whole-archive $&lt;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
'$<LINK_LIBRARY>' 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
Copy link
Contributor

Be-ing commented Feb 13, 2023

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

@ahayzen-kdab
Copy link
Collaborator

@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 🤔

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
Copy link
Collaborator

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 🤔

@ahayzen-kdab
Copy link
Collaborator

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 🤔

@ahayzen-kdab
Copy link
Collaborator

ahayzen-kdab commented Feb 14, 2023

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 added a commit to ahayzen-kdab/cxx-qt that referenced this issue Feb 14, 2023
ahayzen-kdab added a commit to ahayzen-kdab/cxx-qt that referenced this issue Feb 14, 2023
ahayzen-kdab added a commit to ahayzen-kdab/cxx-qt that referenced this issue Feb 14, 2023
ahayzen-kdab added a commit to ahayzen-kdab/cxx-qt that referenced this issue Feb 14, 2023
ahayzen-kdab added a commit to ahayzen-kdab/cxx-qt that referenced this issue Feb 14, 2023
ahayzen-kdab added a commit to ahayzen-kdab/cxx-qt that referenced this issue Feb 14, 2023
ahayzen-kdab added a commit to ahayzen-kdab/cxx-qt that referenced this issue Feb 14, 2023
Be-ing pushed a commit that referenced this issue Feb 14, 2023
@ahayzen-kdab
Copy link
Collaborator

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
Copy link
Contributor

Be-ing commented Feb 15, 2023

Maybe there's a bug in the cc crate?

@maciek134
Copy link

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
Copy link

This is what I did to the example (keep in mind I went the easy way and used the CMake one): maciek134@d878044
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
Copy link

maciek134 commented Feb 15, 2023

@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
Copy link
Collaborator

@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 🤔

@maciek134
Copy link

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
Copy link
Contributor

Be-ing commented Feb 17, 2023

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
Copy link

maciek134 commented Feb 18, 2023

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.

przempore pushed a commit to przempore/cxx-qt that referenced this issue Mar 15, 2023
@jimmyvanhest
Copy link
Contributor

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:

  • For some targets the moc compiler requires the -Muri="component-uri" option. Because it would not hurt the moc compiler I supplied these options to the moc compiler for all targets.
  • The qtdatetime part of the qt core module is not available for WASM. I excluded the related build files and rust modules when the target os equals emscripten.
  • The qt prl files for WASM contain object files which should be supplied to the builder. I changed the specifics of the build process a bit so that it would do so.
  • Fixed an issue in prl file parsing where QT_INSTALL_PREFIX was treated the same as QT_INSTALL_LIBS.

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

  • When the makefile is invoked to build anything it will automatically check if the required tools are installed in the repository. Notably qt will require(not ask) the user to supply an email and password. Please check the make file $(PACKAGEDIR)/Qt rule for this process or ask me for more clarification if it is needed.
  • rust is configured to create a static library so it can easily integrate with qmake.
  • the qmake project file generation(found in scripts/qmake-build.sh) will look in the src dir for all c/c++ source files and qrc files. After the project file is generated some changes to it are made to link with the static library produced by rust and add some more directives required by qmake for a qml project.

Small pain points:

  • When creating the qmake project file it would have been nicer to add a custom target for the rust build process. It would have eased the passing of compiler flags to rustc but I could not get this to work properly so i opted to manually specify this in the root make file.
  • cxx-qt generates some code to import plugins to the final application but it seems to have dropped somehow in the build process(seems to have something to do with +whole-archive but not sure how that works). I created a workaround for that to generate my own code to import all plugins found in the static library build by rust. see scripts/generate_q_static_plugin_import.sh

@ahayzen-kdab
Copy link
Collaborator

@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
Copy link
Collaborator

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
Copy link
Contributor

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
Copy link
Collaborator

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
Copy link

mneilly commented Oct 21, 2023

@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
Copy link
Collaborator

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 🤔

@mneilly
Copy link

mneilly commented Oct 25, 2023

I got past the issue with not finding the qml file by linking the generated *qml_module_resources.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
Copy link

mneilly commented Oct 28, 2023

Ok, I got cargo_without_cmake 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
Copy link
Contributor

Be-ing commented Nov 1, 2023

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
Copy link
Contributor

Be-ing commented Nov 2, 2023

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.

mattkdab added a commit to mattkdab/cxx-qt that referenced this issue Sep 16, 2024
ahayzen-kdab pushed a commit to mattkdab/cxx-qt that referenced this issue Sep 16, 2024
ahayzen-kdab pushed a commit that referenced this issue Sep 16, 2024
@mattkdab
Copy link
Member

@azzamsa @maciek134 @jimmyvanhest @mneilly wasm builds are now possible for a subset of use cases :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📖 documentation Improvements or additions to documentation ⬆️ feature New feature or request ❔ help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants