Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
80bccc9
Add some primitive Emscripten support, based on WASI
kinke Aug 10, 2026
e5da7df
CI: Add Emscripten job
kinke Aug 10, 2026
1f50a08
[try building Phobos too]
kinke Aug 10, 2026
9ed0812
[ignore unittest failures, and try running the Phobos debug testrunne…
kinke Aug 10, 2026
a4e9abd
[fix linking with emcc]
kinke Aug 10, 2026
f75de5e
[enable addon-package smoke test for Emscripten too]
kinke Aug 10, 2026
a825d0c
[add importc_compare test]
kinke Aug 10, 2026
834c6d1
[druntime: Fix some issues reported by importc_compare]
kinke Aug 10, 2026
918f918
[fix core.sys.posix.sys.types and core.stdc.limits]
kinke Aug 12, 2026
8771d6e
[fix core.sys.posix.sys.stat]
kinke Aug 12, 2026
e4616bf
[fix core.sys.posix.sys.socket]
kinke Aug 12, 2026
f342c2c
Emscripten: Switch from predefined CRuntime_WASI to CRuntime_Musl
kinke Aug 13, 2026
06c29f2
[CI: Try mounting host timezone DB into node process]
kinke Aug 13, 2026
346bf95
[finish removal of wasm-ld linker special case]
kinke Aug 13, 2026
2d81f40
[add -Xcc=-nostdlib to 55-target-wasm-naked.conf]
kinke Aug 13, 2026
7ef92df
[avoid warning wrt. ignored -Xcc switches when using -link-internally]
kinke Aug 13, 2026
ca0e64f
[fix core.sync.event unittest failure]
kinke Aug 14, 2026
4459e8d
[try using default Posix timezone data dir]
kinke Aug 14, 2026
2830c39
[Phobos: Hack around lacking TZ env var support]
kinke Aug 14, 2026
7cbdd31
[polish]
kinke Aug 15, 2026
9beaff6
[changelog]
kinke Aug 15, 2026
33d6afb
[polish phobos]
kinke Aug 15, 2026
aad1338
[fix core.stdc.limits]
kinke Aug 16, 2026
1956ff6
[fix remaining mismatching constants reported by importc_compare, exc…
kinke Aug 16, 2026
e18b055
[oh, Phobos depends on wchar_t being a Unicode char type, not int]
kinke Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/actions/1a-setup-emscripten/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: "Emscripten: Install additional prerequisites"
inputs:
emsdk_version:
required: false
default: '6.0.6'
runs:
using: composite
steps:
- name: "Install emsdk" # into ../emsdk
shell: bash
run: |
set -eux
cd ..

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install '${{ inputs.emsdk_version }}'
./emsdk activate '${{ inputs.emsdk_version }}'
3 changes: 1 addition & 2 deletions .github/actions/4b-test-lit/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ runs:
fi

cd ../build
# temporarily add LLVM bin dir to PATH, so that e.g. wasm-ld is found
PATH="$PWD/../llvm/bin:$PATH" ctest -V -R "lit-tests"
ctest -V -R "lit-tests"

- name: 'Windows: Run LIT testsuite'
if: runner.os == 'Windows'
Expand Down
65 changes: 65 additions & 0 deletions .github/actions/5a-emscripten/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: "Emscripten: Cross-compile druntime & phobos incl. test runners, install libs + .conf, run druntime & phobos unittests"
inputs:
arch:
required: true
runs:
using: composite
steps:
- name: Cross-compile druntime & phobos incl. test runners & install libs + .conf
shell: bash
run: |
set -eux
cd ..

os='emscripten'
arch='${{ inputs.arch }}'
triple="$arch-unknown-$os"

bootstrap-ldc/bin/ldc-build-runtime \
--ninja \
--dFlags="-mtriple=$triple" \
--linkerFlags="--preload-file;/usr/share/zoneinfo" \
--ldcSrcDir="$PWD/ldc" \
--installWithSuffix="-$os-$arch" \
--testrunners \
CMAKE_INSTALL_PREFIX="$PWD/install" \
RT_CONF_TRIPLE_REGEX="$arch-.*-$os" \
CMAKE_TOOLCHAIN_FILE="$PWD/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake" \
BUILD_LTO_LIBS=ON

- name: Run druntime unittests
shell: bash
run: |
set -eux
cd ../ldc-build-runtime.tmp

../emsdk/node/*/bin/node ./druntime-test-runner-debug.js
../emsdk/node/*/bin/node ./druntime-test-runner.js

- name: Run phobos unittests
shell: bash
run: |
set -eux
cd ../ldc-build-runtime.tmp

../emsdk/node/*/bin/node ./phobos2-test-runner-debug.js
../emsdk/node/*/bin/node ./phobos2-test-runner.js

- name: Run `importc_compare`
shell: bash
run: |
set -eux
cd ..

os='emscripten'
arch='${{ inputs.arch }}'
triple="$arch-unknown-$os"

bootstrap-ldc/bin/ldc2 \
-defaultlib=druntime-ldc -L-Linstall/lib-$os-$arch \
-gcc="$PWD/emsdk/upstream/emscripten/emcc" \
--mtriple=$triple \
-I$PWD/ldc/runtime/druntime/test/importc_compare/src \
$PWD/ldc/runtime/druntime/test/importc_compare/src/importc_compare.d

emsdk/node/*/bin/node ./importc_compare.js
47 changes: 37 additions & 10 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ jobs:
matrix:
include:
- job_name: WASI wasm32
os: wasi
arch: wasm32
- job_name: Emscripten wasm32
os: emscripten
arch: wasm32
name: ${{ matrix.job_name }}
runs-on: ubuntu-22.04-arm
Expand All @@ -404,26 +408,37 @@ jobs:
clang_major: ${{ env.CLANG_MAJOR }}
arch: aarch64
- name: 'WASI: Install additional prerequisites'
if: matrix.os == 'wasi'
uses: ./.github/actions/1a-setup-wasi
- name: 'Emscripten: Install additional prerequisites'
if: matrix.os == 'emscripten'
uses: ./.github/actions/1a-setup-emscripten
- name: Build bootstrap LDC
uses: ./.github/actions/2-build-bootstrap
- name: 'WASIp1: Cross-compile druntime & phobos incl. test runners & run druntime & phobos unittests'
if: matrix.os == 'wasi'
uses: ./.github/actions/5a-wasi
with:
os: wasip1
arch: ${{ matrix.arch }}
- name: 'WASIp2: Cross-compile druntime & phobos incl. test runners & run druntime & phobos unittests'
if: matrix.os == 'wasi'
uses: ./.github/actions/5a-wasi
with:
os: wasip2
arch: ${{ matrix.arch }}
- name: 'Emscripten: Cross-compile druntime & phobos incl. test runners & run druntime & phobos unittests'
if: matrix.os == 'emscripten'
uses: ./.github/actions/5a-emscripten
with:
arch: ${{ matrix.arch }}
- name: Make installed .conf files portable
uses: ./.github/actions/5b-make-portable
- name: Create addon package & upload artifact
uses: ./.github/actions/7-package
with:
arch: ${{ matrix.arch }}
os: wasi
os: ${{ matrix.os }}
is_addon: true
- name: Smoke-test add-on package
run: |
Expand All @@ -433,17 +448,29 @@ jobs:
tar xf ${{ github.workspace }}/artifacts/*-addon-*.tar.xz
# generate hello.d
echo 'void main() { import std.stdio; writefln!"Hello world, %d bits"(size_t.sizeof * 8); }' > hello.d
# put WASI-SDK clang onto PATH
export PATH="$PWD/../wasi-sdk/bin:$PATH"
# build & run hello-world
for os in wasip1 wasip2; do
# test LTO too

if [[ '${{ matrix.os }}' == wasi ]]; then
# put WASI-SDK clang onto PATH
export PATH="$PWD/../wasi-sdk/bin:$PATH"
# build & run hello-world
for os in wasip1 wasip2; do
# test LTO too
for args in "" "-O -flto=full -defaultlib=druntime-ldc-lto,phobos2-ldc-lto"; do
bin/ldc2 -mtriple=${{ matrix.arch }}-unknown-$os $args hello.d
ls -l hello.wasm
../wasmtime/wasmtime -W exceptions ./hello.wasm
done
done
elif [[ '${{ matrix.os }}' == emscripten ]]; then
# put emcc onto PATH
export PATH="$PWD/../emsdk/upstream/emscripten:$PATH"
# build & run hello-world
for args in "" "-O -flto=full -defaultlib=druntime-ldc-lto,phobos2-ldc-lto"; do
bin/ldc2 -mtriple=${{ matrix.arch }}-unknown-$os $args hello.d
ls -l hello.wasm
../wasmtime/wasmtime -W exceptions ./hello.wasm
bin/ldc2 -mtriple=${{ matrix.arch }}-unknown-emscripten $args hello.d
ls -l hello.{js,wasm}
../emsdk/node/*/bin/node ./hello.js
done
done
fi


merge-macos:
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
- `-foptimize-nothrow` is the default setting when targeting Windows or WebAssembly, to make hitting https://github.com/ldc-developers/ldc/issues/3504 much less likely.
- Support for [LLVM 22](https://releases.llvm.org/22.1.0/docs/ReleaseNotes.html). The prebuilt packages use v22.1.8. (#5097, #5102, #5172)
- Minimum LLVM version raised to 18. (#5094)
- DRuntime and Phobos now support [WASI](https://github.com/WebAssembly/WASI) preview levels 1 and 2, based on [`wasi-libc`](https://github.com/WebAssembly/wasi-libc). Two caveats being that WASI is strictly single-threaded (currently), and that fibers are unavailable. (#5186, #5229, #5230, #5242)
- DRuntime and Phobos now support [WASI](https://github.com/WebAssembly/WASI) preview levels 1 and 2, based on [`wasi-libc`](https://github.com/WebAssembly/wasi-libc), as well as [Emscripten](https://emscripten.org/). Two caveats being that WASI (and our Emscripten support) is strictly single-threaded (currently), and that fibers are unavailable. (#5186, #5229, #5230, #5242, #5259)
- New prebuilt **addon packages** (`ldc2-*-addon-<target OS>.tar.xz`) for simple cross-compilation. Simply unpack them directly into your LDC installation directory, and then cross-compile via `-mtriple=…` (or `dub --target=…`): (#5240)
- Windows: `x86_64-windows-msvc`, `i686-windows-msvc`; no SDK required.
- Android: `aarch64-linux-android30`, `armv7a-linux-androideabi29`, `x86_64-linux-android29`, `i686-linux-android29`; needs an [Android NDK](https://developer.android.com/ndk/downloads) (r27) and its `toolchains/llvm/prebuilt/*/bin/` dir in your PATH.
- WASI: `wasm32-unknown-wasip1`, `wasm32-unknown-wasip2`; needs a [WASI SDK](https://github.com/webassembly/wasi-sdk) and its `bin/` dir in your PATH. (#5207, #5242)
- Emscripten: `wasm32-unknown-emscripten`; needs a [Emscripten SDK](https://github.com/emscripten-core/emsdk) and its `upstream/emscripten/` dir in your PATH. (#5259)
- The `@ldc.attributes.restrict` UDA is now supported for dynamic-array parameters too. Builtin array ops (`c[] = a[] * b[]` etc.) use it, enabling auto-vectorization. Note that for such array ops, the dlang spec prescribes that the slice on the left and any slices on the right must not overlap (https://dlang.org/spec/arrays.html#array-operations), and this knowledge is now used by the optimizer; e.g., `x[] *= x[]` is invalid. (#5148, #5168, #5176)
- DMD-style inline assembly: `asm { naked; }` is now much less of a special case wrt. codegen - such naked functions are now emitted as if `asm { naked; }` was replaced with the `@naked` function UDA. IR-wise, they are not emitted as module-level asm blobs anymore, but regular IR functions. This should lift a few former `asm { naked; }`-specific restrictions and fixed at least one LTO issue. (#5041)
- **dcompute**: Added support for Native device code Embedding (PTX and SPIR-V) into the host executable's `.rodata` section. (#5140)
Expand Down
11 changes: 7 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -952,17 +952,20 @@ makeConfSection(NAME "51-target-wasm-base"
${wasm_eh_switches}
)

# Add a convenience config for *naked* wasm targets, omitting druntime/Phobos
# Add a convenience config for *naked* wasm targets, omitting druntime/Phobos/libc
# (and preferring built-in lld for linking) automatically.
set(wasm_naked_link_internally)
set(wasm_naked_switches)
if(LDC_WITH_LLD)
set(wasm_naked_link_internally -link-internally)
list(APPEND wasm_naked_switches -link-internally)
else()
# clang as default cross-linker driver usually works, with -nostdlib
list(APPEND wasm_naked_switches -Xcc=-nostdlib)
endif()
makeConfSection(NAME "55-target-wasm-naked"
SECTION "^wasm(32|64)-.*-(unknown|none)"
SWITCHES
-defaultlib=
${wasm_naked_link_internally}
${wasm_naked_switches}
LIB_DIRS OVERRIDE
)

Expand Down
23 changes: 12 additions & 11 deletions driver/linker-gcc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,8 @@ void ArgsBuilder::addLinker() {
//////////////////////////////////////////////////////////////////////////////

void ArgsBuilder::addUserSwitches() {
const bool isEmcc = global.params.targetTriple->isOSEmscripten();

// additional linker and cc switches (preserve order across both lists)
for (unsigned ilink = 0, icc = 0;;) {
unsigned linkpos = ilink < opts::linkerSwitches.size()
Expand All @@ -654,7 +656,14 @@ void ArgsBuilder::addUserSwitches() {
if (!(str.starts_with("-l") || str.starts_with("-L") ||
str.starts_with("-Wl,") || str.starts_with("-shared") ||
str.starts_with("-static"))) {
args.push_back("-Xlinker");
// avoid `-Xlinker <p>` for Emscripten's emcc, seems buggy; use
// `-Wl,<p>` instead
if (isEmcc) {
args.push_back("-Wl," + p);
continue;
} else {
args.push_back("-Xlinker");
}
}
args.push_back(p);
} else if (ccpos < linkpos) {
Expand Down Expand Up @@ -807,16 +816,8 @@ int linkObjToBinaryGcc(llvm::StringRef outputPath,
#endif

// build command-line for gcc-compatible linker driver
// exception: invoke (ld-compatible) linker directly for WebAssembly targets
std::string tool;
std::unique_ptr<ArgsBuilder> argsBuilder;
if (global.params.targetTriple->isOSBinFormatWasm() && !global.params.targetTriple->isOSWASI()) {
argsBuilder = std::make_unique<LdArgsBuilder>();
tool = getProgram("wasm-ld", &opts::linker);
} else {
argsBuilder = std::make_unique<ArgsBuilder>();
tool = getCC(argsBuilder->args);
}
auto argsBuilder = std::make_unique<ArgsBuilder>();
std::string tool = getCC(argsBuilder->args);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is e.g. also still to polish, causing the Alpine CI job failures (no integrated LLD, but external wasm-ld).

This all dates back to the very early wasm support, like the wasm-defaults .conf file - we used to only support bare-metal wasm, so no druntime and Phobos to link, and no need for a C compiler as linker driver, since there wasn't a wasm libc support either. So we used to invoke wasm-ld (unless we have an integrated LLD) with the ld CLI interface, for all (non-WASI) wasm targets.

We can keep this behavior for backwards-compatibility, but then only when targeting an unknown/none OS. We already do that in 55-target-wasm-naked.conf nowadays, via the -link-internally (=> use integrated lld with wasm-ld CLI interface) - but only if LLD integration was enabled at build-time.

So the compiler special case here would only be needed without LLD integration. Not sure that's worth the trouble, incl. the link_WebAssembly lit complication (looking for an external wasm-ld), which I haven't gotten rid of yet.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In theory, using most modern system clang as link driver for bare-metal Wasm should be fine? wasm-ld is a standard part of LLVM distributions nowadays.

One notable exception is (at least) macOS 15's clang. So you'll have to have either MacPorts/Homebrew LLVM/Clang available to use as -gcc, or use wasi-sdk's (or emsdk's).

But if we use -link-internally by default, then you probably won't notice the difference most of the time.

@kinke kinke Aug 13, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oh right - I've just tested this with my system-default clang 18, and it works with an extra -Xcc=-nostdlib:

$ bin/ldc2 -mtriple=wasm32-unknown-unknown -v ../ldc/tests/codegen/wasm.d -link-internally=false -Xcc=-nostdlib -Xcc=-v
[…]
Did not find C cross-compiler: `wasm32-unknown-unknown-gcc`
Did not find C cross-compiler: `wasm32-unknown-unknown-clang`
Found C cross-compiler: `/usr/bin/clang`
/usr/bin/clang --target=wasm32-unknown-unknown wasm.o -o wasm.wasm -Xlinker -z -Xlinker stack-size=1048576 -Xlinker --stack-first -nostdlib -Wl,--gc-sections
[…]
 "/usr/bin/wasm-ld-18" -m wasm32 -L/usr/lib wasm.o -z stack-size=1048576 --stack-first --gc-sections -o wasm.wasm

So yeah, that should be good enough. Noone has requested an ld CLI interface for other targets either, so...


// build arguments
argsBuilder->build(outputPath, defaultLibNames);
Expand Down
5 changes: 2 additions & 3 deletions driver/linker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,8 @@ static std::string getOutputPath() {
extension = target.dll_ext.ptr;
} else if (triple.isOSWindows()) {
extension = "exe";
} else if (triple.getArch() == llvm::Triple::wasm32 ||
triple.getArch() == llvm::Triple::wasm64) {
extension = "wasm";
} else if (triple.isWasm()) {
extension = triple.isOSEmscripten() ? "js" : "wasm";
}

if (global.params.exefile.length) {
Expand Down
4 changes: 2 additions & 2 deletions driver/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -960,8 +960,8 @@ void registerPredefinedTargetVersions() {
#endif
case llvm::Triple::Emscripten:
VersionCondition::addPredefinedGlobalIdent("Emscripten");
// Emscripten uses musl and libc++, so mimic a musl Linux platform:
VersionCondition::addPredefinedGlobalIdent("linux");
VersionCondition::addPredefinedGlobalIdent("WASI");
VersionCondition::addPredefinedGlobalIdent("WASIp1");
VersionCondition::addPredefinedGlobalIdent("Posix");
VersionCondition::addPredefinedGlobalIdent("CRuntime_Musl");
VersionCondition::addPredefinedGlobalIdent("CppRuntime_LLVM");
Expand Down
2 changes: 2 additions & 0 deletions driver/tool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ llvm::SmallVector<std::string, 2> findCCFallback() {
choices = {{"wasm32-wasi-clang"}};
break;
}
} else if (triple.isOSEmscripten()) {
choices = { { "emcc" } };
} else {
choices = {
{ opts::mTargetTriple + "-gcc" },
Expand Down
4 changes: 4 additions & 0 deletions runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ if("${TARGET_SYSTEM}" MATCHES "WASI|Emscripten")

append("-Wl,-z,stack-size=1048576 -Wl,--stack-first" LD_FLAGS)

if("${TARGET_SYSTEM}" MATCHES "Emscripten")
append("-sALLOW_MEMORY_GROWTH" LD_FLAGS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we want to enable this by default in ldc2.conf?

If not maybe this block should be moved outside of
(normally inherited from... if block to be less confusing

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah I'm not sure how likely this is going to be needed for user apps as well. IIRC, Emscripten defaults to something like 20 MB, which does sound really low for D with GC.

@kinke kinke Aug 16, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Do you know how wasmtime/wasi-libc handles this? All dynamic by default?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe so.

endif()

list(APPEND D_FLAGS --wasm-enable-eh)
if(LLVM_VERSION_MAJOR GREATER_EQUAL 20)
list(APPEND D_FLAGS --wasm-use-legacy-eh=false)
Expand Down
1 change: 1 addition & 0 deletions runtime/druntime/src/core/demangle.d
Original file line number Diff line number Diff line change
Expand Up @@ -3003,6 +3003,7 @@ CXX_DEMANGLER getCXXDemangler() nothrow @trusted
version (OpenBSD) import core.sys.openbsd.dlfcn : RTLD_DEFAULT;
version (Darwin) import core.sys.darwin.dlfcn : RTLD_DEFAULT;
version (Solaris) import core.sys.solaris.dlfcn : RTLD_DEFAULT;
version (Emscripten) import core.sys.posix.dlfcn : RTLD_DEFAULT;
version (CRuntime_WASI) import core.sys.posix.dlfcn : RTLD_DEFAULT;

if (auto found = cast(CXX_DEMANGLER) dlsym(RTLD_DEFAULT, "__cxa_demangle"))
Expand Down
4 changes: 3 additions & 1 deletion runtime/druntime/src/core/stdc/config.d
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,9 @@ version (unittest)
// https://musl.libc.org/time64.html
version (CRuntime_Musl)
{
version (CRuntime_Musl_Pre_Time64)
version (Emscripten)
enum muslRedirTime64 = false;
else version (CRuntime_Musl_Pre_Time64)
enum muslRedirTime64 = false;
else
{
Expand Down
Loading
Loading