Skip to content

Conversation

pavelsavara
Copy link
Member

@pavelsavara pavelsavara commented Oct 1, 2025

  • add host.native subset back for coreCLR browser
  • don't build browserhost for clr.runtime subset
  • make browserhost dependent on relevant .a files of libs.native+clr.runtime subsets, instead of CMake target and add_subdirectory
  • update eng/liveBuilds.targets
  • introduce CORERUN_LIBS_ONLY to make libs build smaller when used from coreCLR CMake
  • added CLR_ARTIFACTS_BIN_DIR, CMAKE_BUILD_LIBRARIES_CONFIGURATION, CMAKE_NET_CORE_APP_CURRENT_VERSION, CMAKE_BUILD_RUNTIME_CONFIGURATION
    • alternatively we can build the path SHARED_LIB_DESTINATION and SHARED_CLR_DESTINATION in MSBuild and pass that instead.

Contributes to #120206

@pavelsavara pavelsavara added this to the 11.0.0 milestone Oct 1, 2025
@pavelsavara pavelsavara self-assigned this Oct 1, 2025
@pavelsavara pavelsavara added arch-wasm WebAssembly architecture area-Host os-browser Browser variant of arch-wasm labels Oct 1, 2025
@pavelsavara pavelsavara changed the title [browser] build [browser] build more coreCLR dependencies Oct 1, 2025
Copy link
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

@pavelsavara pavelsavara changed the title [browser] build more coreCLR dependencies [browser] build browserhost in host.native subset Oct 3, 2025
@pavelsavara

This comment was marked as outdated.

@jkoritzinsky

This comment was marked as outdated.

@pavelsavara

This comment was marked as outdated.

@jkoritzinsky
Copy link
Member

For Linux and Windows, we were hitting issues with linker incompatibilities and a ton of complexity because of the flags we pass around and the fact that we shuffle pieces between different machines in the PR builds.

For wasm, we already have an established path of shipping static libs, so I'm not as concerned that we'll hit the same problem.

If you're able to pre-link everything with the new host like how we do the singlefilehost, I'd recommend that. If you can't, then splitting it is fine.

@pavelsavara
Copy link
Member Author

pavelsavara commented Oct 13, 2025

This sort of implicit dependency can be really finicky.
In particular, I'm not a fan of CLR_ARTIFACTS_BIN_DIR and CMAKE_BUILD_LIBRARIES_CONFIGURATION

@jkoritzinsky I'm not fan either but I don't have better idea. Let's discuss options or invent something else

A) ideally we would have only one CMake root for all native builds, on all targets.

  • we have 6 or more CMake roots: coreclr, libs, host, mono, mono cross, runtime tests ... what else ?
  • we have to manually sync build flags among them, that sucks
  • there are multiple flavors of batch files starting those native builds, some of them are really ugly
  • but lot of other things would probably break when we try to refactor this
  • I feel that's not on critical path for WASM right now. Other volunteers ?

B) singlefilehost style, static linking of host and libs within coreclr root:

  • this is poor version of A)
  • does it matter that singlefilehost doesn't respect any of the libs.native and host.native CMake root arguments ?
  • for example, this setup doesn't respect -lc Release which is necessary for src/tests. Interpreter doesn't run in Release yet.
  • are we going to install lib and host .a files from that root, so that we could link them later out-of tree ?
  • if so, are we need to match with liveBuild/runtime pack expectations for libs
  • if not, we are shipping .a files which are not used/tested by default build.
  • are we going to eliminate libs.native and host.native on WASM because they would go un-used ?
  • slow inner loop for corerun

C) what I'm doing in this PR. 3 cmake roots and loose dependency via msbuild

  • this is what Mono does: link .a files later
  • if you are testing with browserhost in your inner loop you need to make sure that you re-build dependencies yourself.
  • that is also benefit at the same time, you don' have rebuild libs if you only make changes to host. Or typescript only.
  • compiler flags may drift apart with time.
    • I plan to introduce further unification to single place, probably eng/native.props like on this PR.
  • inner loop builds of corerun are faster
  • it produces all the .a files that would be necessary for the workload build on customer machine
  • CLR_ARTIFACTS_BIN_DIR & CMAKE_BUILD_LIBRARIES_CONFIGURATION are smell. Is there better style ?

cc: @jkotas @AaronRobinsonMSFT @janvorli @akoeplinger thoughts ?

I will take non-controversial changes from this PR into different PR in the meantime and focus this PR only on this topic.

@pavelsavara

This comment was marked as outdated.

@jkoritzinsky
Copy link
Member

A) ideally we would have only one CMake root for all native builds, on all targets.

  • we have 6 or more CMake roots: coreclr, libs, host, mono, mono cross, runtime tests ... what else ?
  • we have to manually sync build flags among them, that sucks
  • there are multiple flavors of batch files starting those native builds, some of them are really ugly
  • but lot of other things would probably break when we try to refactor this
  • I feel that's not on critical path for WASM right now. Other volunteers ?

This is something I see as possible in the future, but due to the fact that the Mono CMake tree is designed fundamentally differently from the rest of the CMake trees makes unification difficult.

B) singlefilehost style, static linking of host and libs within coreclr root:

  • this is poor version of A)
  • does it matter that singlefilehost doesn't respect any of the libs.native and host.native CMake root arguments ?
  • for example, this setup doesn't respect -lc Release which is necessary for src/tests. Interpreter doesn't run in Release yet.

This is not a problem, as the Debug/Release versions of the native libs shouldn't differ in contract (so combining debug libs.native in the host with release managed libs should be fine, or we aren't sufficiently testing singlefilehost in CI).

  • are we going to install lib and host .a files from that root, so that we could link them later out-of tree ?

Don't we need to do this anyway for linking the out-of-tree browserhost (built in our CMake in browserhost/host)?

  • if so, are we need to match with liveBuild/runtime pack expectations for libs
  • if not, we are shipping .a files which are not used/tested by default build.
  • are we going to eliminate libs.native and host.native on WASM because they would go un-used ?

My preference would be to build the browserhost in the CoreCLR CMake, and browserhost/host in the host.native CMake (similar split to singlefilehost vs apphost), but we could also put it all into the CoreCLR CMake and turn off host.native.

For libs.native, how does Mono WASM get the native libs binaries?

  • slow inner loop for corerun

C) what I'm doing in this PR. 3 cmake roots and loose dependency via msbuild

  • this is what Mono does: link .a files later

Does Mono always link the .a files later (and never build a pre-linked-together browserhost equivalent)?

  • if you are testing with browserhost in your inner loop you need to make sure that you re-build dependencies yourself.
  • that is also benefit at the same time, you don' have rebuild libs if you only make changes to host. Or typescript only.

If the innerloop for making a runtime change would be clr.runtime+host.native, that would be slower than just clr.runtime. For the other changes, Ninja (or Make)'s up to date checks should prevent over-rebuilding and keep the innerloop fast.

  • compiler flags may drift apart with time.

    • I plan to introduce further unification to single place, probably eng/native.props like on this PR.

Generally, we've kept flags consistent by centralizing on eng/native/configurecompiler.cmake and the other .cmake files in that folder (again, Mono is the odd one out here).

  • inner loop builds of corerun are faster

corerun builds should already be extra fast if you build clr.hosts (builds corerun and all dependencies, only copies updated corerun to output dir).

  • it produces all the .a files that would be necessary for the workload build on customer machine
  • CLR_ARTIFACTS_BIN_DIR & CMAKE_BUILD_LIBRARIES_CONFIGURATION are smell. Is there better style ?

@pavelsavara

This comment was marked as outdated.

@pavelsavara
Copy link
Member Author

@jkoritzinsky I rebased this PR on top of option B) that was already merged into main branch.

Now it should be easier to discuss option C)

I don't insist it needs to be done this way, but it's much closer to what customers would run if they link the .a files on their dev environment using workload (LLVM/emcc).

It also makes coreCLR CMake innner dev loop significantly faster. Emcc linking is slow and this way we only link corerun but not corehost in there. Effectively in half of the time.

I also updated the top description.

Comment on lines +90 to +91
# WASM-TODO set GEN_PINVOKE to 1 once we generate them. static entrypoint.c prevents C linker from trimming, when IL trimming trims PInvokes
set(GEN_PINVOKE 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of my concerns is that if you resolve this TODO, then the mechanism with which this PR references the libraries native bits will get the wrong libraries native bits (the ones for NativeAOT, not the ones that build with the GEN_PINVOKE=1 logic).

Maybe that's a product problem as well (and this PR is just exposing it), I'm not sure.

Copy link
Member Author

@pavelsavara pavelsavara Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are happy to use libs with entrypoint.c when building coreRun.

We should use generated pinvoke instead, when we produce runtime pack (so that for example TCP sockets are trimmed). That means that libraries built by libs.native subset should not use entrypoint.c (once we have the generator). This makes this PR important.

We would run the generator in the src\native\corehost\corehost.proj MSBuild. We would compile the generated .c file as part of host.native CMake and it would serve same purpose as entrypoint.c, but just with DLLImports that are really used in the final application.

This maps to what we do for Mono in browser.proj which is mono.wasmruntime subset. It logically maps to host.native for coreCLR.

  • The "final application" for runtime pack is dotnet.native.wasm that has all features supported by libraries managed code on browser tartget.
  • The "final application" for customer app, would have full trimming, and so it would drop C code that is not being used by that app. This is why we ship workload and why we link on customer machine.

cc @radekdoulik

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why we ship workload

Nit: I do not think this has anything to do with shipping as a workload. NativeAOT works the same way, links on customer machine, and it ships as a package.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How could we run a generator in corehost.proj that produces a .c file that only has the entrypoints used by the final application?

Doesn't the generator have to run on the customer machine? Wouldn't a generator that runs in our build need to include everything (effectively reproducing the entrypoints.c files we have today)?

I don't see a reason for us to move the browserhost out of the CoreCLR CMake for the purpose of running a tool that will generate the same information we already have with less complicated infrastructure.

Sure, we need to run the tool for customer apps, but that's what tests are for. We should have tests that validate this experience instead of adding more complexity to the build for the purpose of adding test coverage (when we really should have dedicated tests).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ties into my idea where the browserhost target does not link against the browserhost.Static target directly and just includes the same sources and flags (and stays within the CoreCLR build) as there's fundamental differences between the two.

I think that the browserhost target should build with CoreCLR and link against the libs.native builds that have entrypoints.c.

The static lib browserhost.Static should build within host.native. As it's a static lib, it doesn't need libs.native or clr.native to have been built beforehand to build.

Then, when constructing the package to ship the static assets (transport package/workload/runtime pack/whatever), the no-entrypoints.c libs.native assets can be pulled in (as well as the static CoreCLR assets) to go into the package (just like how NativeAOT pulls things together).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to have some sort of pre-built entrypoints.c file for the add_executable target if that one is supposed to bundle the native libraries within itself and not require the user to link on their machine.

Yes, but the entrypoint.c set as-is is too broad.

That makes sense.

Do you want to run the tooling after building the libs.sfx subset and running library trimming? Does that actually get us the result we want?

Yes, I think so.

Could we instead structure this as a "validation test" where we check in the result (so we can build browserhost in clr.runtime) and validate post-trimming that the tooling generates the correct results?

Yes, I think this is my B1) alternative. I don't love it, tho.

Would it be in constant flux in git or we assume it's very stable ?

I would assume that this would be very stable (likely only changing with fundamental feature rewrites or new APIs). Any change here would likely be reflected later in one of our size benchmarks.

If we want to trim QCalls that call into CoreCLR as well, this is the only solution that will work cleanly in our infrastructure.

qcallentrypoints.cpp are already full of #ifdef

Could you please elaborate how we could validate this ? Do you mean that we somehow check the list of native symbols and make sure that all of them are used in IL trimmed corelib ? That would work.

For the WASM build, we could have a separate qcallentrypoints.cpp that is built with the same tooling as the entrypoints.c generator. Since QCalls are fundamentally just P/Invokes, the same tooling should work.

@jkotas brings up a good point about the interp thunks for the P/Invoke transitions.

I think we can do this as a first pass and revisit in the future. I really don't like that this introduces a dependency from the native builds on the managed builds (we already have the reverse for NativeAOT), but that's something we could address in a future PR (ie the B1 alternative above).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build libs.sfx -> generate call stubs for everything used by libs.sfx -> link dotnet.native.wasm together with the unmanaged runtime bits

The generation of these call stubs can take care of generating trimmed down equivalent of entrypoints.c too.

I think this describes the process pretty well. In mono the generator is a msbuild task, that scans the assemblies and generates the C source files.

I am in the process of porting the generator to the coreclr. I will have the interp to native thunks ready soon and then will take a look at the pinvoke and entrypoints generation.

For the question of whether to generate entrypoints files or not, I think it makes sense to generate them. The alternative would be to keep the ifdefs up to date. That seems fragile.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can do this as a first pass and revisit in the future. I really don't like that this introduces a dependency from the native builds on the managed builds (we already have the reverse for NativeAOT), but that's something we could address in a future PR (ie the B1 alternative above).

I see the pain point with the dependency direction. If the changes to generated entrypoints are rare, we can keep the generated entrypoints in the repo (B1) and add a step after native and managed are built? This step would compare generated ones with the repo ones, fail the build if they differ and request manual update to the sources in the repo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can keep the generated entrypoints in the repo (B1)

This would be a temporary solution. We are going to have this sort of dependency once partial R2R gets online anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think long-term (.NET 12/13 or later) the solution will look as follows:

  1. We'll have one CMake tree for everything but the linked-together browserhost. This will run before building any managed code. (This is a general goal I have for the future).
  2. We'll build the managed code.
  3. We'll link together the prebuilt browserhost after having built the managed code (likely as part of some project in the src/installer/sfx tree so we can pull in the partial R2R).

As @jkotas says, the partial R2R dependency forces us to go down a different route. But longer term (not in this PR), we can avoid going back into our CMake tree to do the final link (and instead do something like what our NAOT targets do or a slimmed down version of what the workload targets do)

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR restructures the browser (WASM) build for CoreCLR to separate the browserhost build into the host.native subset, aligning it with the standard build pattern. Previously, browserhost was built within the CoreCLR runtime subset, which violated the architecture where host components should be built separately.

Key Changes:

  • Restored host.native subset for CoreCLR browser builds
  • Removed browserhost from clr.runtime subset
  • Changed browserhost dependencies from CMake targets to static library file paths
  • Introduced CORERUN_LIBS_ONLY flag to reduce native library build scope when building for CoreCLR

Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/coreclr/CMakeLists.txt Moved browser-specific build configuration from the runtime subset to the libs-native section and removed browserhost as a runtime dependency
src/native/corehost/browserhost/CMakeLists.txt Changed from CMake target dependencies to explicit .a file dependencies; updated paths to use SHARED_LIB_DESTINATION and added new install targets
src/native/corehost/browserhost/sample/CMakeLists.txt Updated sample paths from STATIC_LIB_DESTINATION to SHARED_LIB_DESTINATION
src/native/libs/CMakeLists.txt Added conditional compilation guards using CORERUN_LIBS_ONLY to exclude non-essential libraries
src/native/libs/System.Native/CMakeLists.txt Added dependency declarations for timezone data libraries
src/native/libs/Common/JavaScript/CMakeLists.txt Added installation of package.json to sharedFramework
eng/native/configurepaths.cmake Added CLR_ARTIFACTS_BIN_DIR variable definition
eng/native.props Added CMake arguments for version and configuration information
eng/liveBuilds.targets Updated artifact paths and file collections to reflect the new browserhost location in host.native subset
eng/Subsets.props Added host.native to default CoreCLR browser subsets and reorganized libs.native subset definition

Comment on lines +584 to +588
<!-- Libraries native set -->
<ItemGroup Condition="$(_subset.Contains('+libs.native+'))">
<ProjectToBuild Include="$(SharedNativeRoot)libs\build-native.proj" Category="libs" />
</ItemGroup>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of moving libs.native up, can you move the host subsets to build right before libs.pretest? If we are going to need to run tooling on the outputs of libs.sfx to build the host, the host subsets should come after the libs.sfx subset.

@pavelsavara
Copy link
Member Author

I think we can do this as a first pass and revisit in the future. I really don't like that this introduces a dependency from the native builds on the managed builds (we already have the reverse for NativeAOT), but that's something we could address in a future PR (ie the B1 alternative above).

I see the pain point with the dependency direction. If the changes to generated entrypoints are rare, we can keep the generated entrypoints in the repo (B1) and add a step after native and managed are built? This step would compare generated ones with the repo ones, fail the build if they differ and request manual update to the sources in the repo.

We don't have to rush this PR, if we think we are going to return do B1 anyway.

Before I close this, does any of you feel that C) with pre-generated wasm-pinvoke.c in git would be better than B1 ?
I feel like that for local build time reasons, but I don't insist.

@jkotas
Copy link
Member

jkotas commented Oct 21, 2025

pre-generated wasm-pinvoke.c in git would be better than B1

I would avoid pre-generated wasm-pinvoke.c in git.

I feel like that for local build time reasons, but I don't insist.

This should not be that bad if the generation is done in incremental build friendly fashion.

If you are touching on that the incremental builds are slow, I think it is a general problem that we have been introducing by representing more dependencies correctly in the build system. It makes the incremental builds slower and slower. It has been very apparent in some of the inner loop experiences under libraries.

I think the solution for that is to introduce gestures that one can use to skip the slow dependency checks, such as --no-dependencies switch for dotnet build.

@pavelsavara
Copy link
Member Author

Well, my current incremental experience on windows.

PS D:\runtime> .\build.cmd -bl -os browser -subset clr.runtime -c Debug
Determining projects to restore..

...

[5/13] Building C object gc/unix/CMakeFiles/gc_pal.dir/D_/runtime/artifacts/obj/_version.c.o
  [6/13] Building C object hosts/corerun/CMakeFiles/corerun.dir/D_/runtime/artifacts/obj/_version.c.o
  [7/13] Linking CXX static library dlls\mscoree\coreclr\libcoreclr_static.a
  [8/13] Building CXX object corehost/hostmisc/CMakeFiles/hostmisc.dir/utils.cpp.o
  [9/13] Linking CXX static library corehost\hostmisc\libhostmisc.a
  [10/13] Linking CXX executable hosts\corerun\corerun.js
...
[12/13] Linking CXX executable corehost\browserhost\dotnet.native.js
Time Elapsed 00:00:44.14
  • This is second run of this command in the row, with no changes on disk in between.
  • Something touched _version.c and that triggers compilation
  • That triggers 2x linking of the final app corerun and corehost
  • Emcc linking is the slowest part. This is why I don't want to link 2 apps in my inner loop.
  • Overall 44 seconds on brand new i7 HP laptop - for no change.

@janvorli
Copy link
Member

It seems this is yet another time when the version.c is compiled unconditionally, triggering relinking. We should fix that.

@SingleAccretion
Copy link
Contributor

Emcc linking is the slowest part

Side note: this is because of #119639. Adding -sWASM_BIGINT=1 will make it much faster.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-Host os-browser Browser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants