Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ commands:
description: "bootstrap"
steps:
- run: "$EMSDK_PYTHON ./bootstrap.py"
- build-emcc-native
pip-install:
description: "pip install"
parameters:
Expand Down Expand Up @@ -131,6 +132,21 @@ commands:
export PATH="`pwd`/node-v15.14.0-linux-x64/bin:${PATH}"
npm install jsvu -g
jsvu --os=default --engines=v8
build-emcc-native:
description: "Build emcc_native"
steps:
- run:
name: Install dependencies (Linux)
command: |
if command -v apt-get >/dev/null 2>&1; then
apt-get install -q -y cmake ninja-build
fi
- run:
name: Build emcc_native
command: |
cmake -B out/build_emcc_native -S tools/emcc_native -DCMAKE_BUILD_TYPE=Release
cmake --build out/build_emcc_native --config Release
cmake --install out/build_emcc_native --config Release
install-emsdk:
description: "Install emsdk"
steps:
Expand All @@ -147,6 +163,8 @@ commands:
cd ~/emsdk
./emsdk install ${EMSDK_VERSION}
./emsdk activate ${EMSDK_VERSION}
# Hack: Replace emsdk_path expressions with $CFGDIR so emcc_native can parse without fallback
python -c 's = open(".emscripten").read(); open(".emscripten", "w").write(s.replace("emsdk_path + " + chr(39), chr(39) + "$CFGDIR").replace("emsdk_path + " + chr(34), chr(34) + "$CFGDIR"))' 2>/dev/null || python3 -c 's = open(".emscripten").read(); open(".emscripten", "w").write(s.replace("emsdk_path + " + chr(39), chr(39) + "$CFGDIR").replace("emsdk_path + " + chr(34), chr(34) + "$CFGDIR"))'
# Write the version of clang into a file for use in the ccache key
./upstream/bin/clang --version > clang_version.txt
echo "clang version:"
Expand Down Expand Up @@ -1188,6 +1206,10 @@ jobs:
shell: bash.exe -eo pipefail
steps:
- checkout
- run:
name: Install packages
command: |
choco install -y cmake.portable ninja
- run:
name: "build pylauncher"
shell: cmd.exe
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ jobs:
echo "Be sure that you have installed the current emsdk version. See test/emsdk_version.txt ($(cat test/emsdk_version.txt))."
exit 1
fi
- name: Check emcc_native generated settings
run: |
./tools/emcc_native/gen_settings.py --check

clang-format-diff:
# This job is disabled until we can make it more precise
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ coverage.xml
# Test output
/out/

# Native emcc launcher lives here.
/bin/

# When updating the website we check it out here.
/site/emscripten-site/

Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ install:
./tools/install.py $(DESTDIR)
npm install --omit=dev --prefix $(DESTDIR)

emcc_native:
cmake -B out/build_emcc_native -S tools/emcc_native -G Ninja
cmake --build out/build_emcc_native
cmake --install out/build_emcc_native

# Create an distributable archive of emscripten suitable for use
# by end users. This archive excludes node_modules as it can include native
# modules which can't be safely pre-packaged.
$(DISTFILE): install
tar cf $@ --exclude=node_modules -C `dirname $(DESTDIR)` `basename $(DESTDIR)`

.PHONY: dist install
.PHONY: dist install emcc_native
2 changes: 1 addition & 1 deletion docs/design/03-native-clang-frontend.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Design Doc: Native Launcher / Clang Frontend

- **Status**: Draft
- **Status**: Phase 1 Completed
- **Bug**: https://github.com/emscripten-core/emscripten/issues/26453

## Context
Expand Down
6 changes: 3 additions & 3 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -12277,9 +12277,9 @@ def test_xclang_flag(self):
self.run_process([EMCC, '-c', '-o', 'out.o', '-Xclang', '-include', '-Xclang', 'foo.h', test_file('hello_world.c')])

def test_emcc_size_parsing(self):
create_file('foo.h', ' ')
self.assert_fail([EMCC, '-sTOTAL_MEMORY=X', 'foo.h'], 'error: invalid byte size `X`. Valid suffixes are: kb, mb, gb, tb')
self.assert_fail([EMCC, '-sTOTAL_MEMORY=11PB', 'foo.h'], 'error: invalid byte size `11PB`. Valid suffixes are: kb, mb, gb, tb')
create_file('foo.c', ' ')
self.assert_fail([EMCC, '-sTOTAL_MEMORY=X', 'foo.c'], 'error: invalid byte size `X`. Valid suffixes are: kb, mb, gb, tb')
self.assert_fail([EMCC, '-sTOTAL_MEMORY=11PB', 'foo.c'], 'error: invalid byte size `11PB`. Valid suffixes are: kb, mb, gb, tb')

def test_native_call_before_init(self):
self.set_setting('ASSERTIONS')
Expand Down
3 changes: 3 additions & 0 deletions tools/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

get_cflags(): In addition to compiler flags this function also returns pre-processor
flags. For example, include paths and macro definitions.

NOTE: Default compiler flag construction logic here is also implemented natively
in tools/emcc_native/driver.cpp. Keep changes in sync between both places!
"""

import os
Expand Down
53 changes: 53 additions & 0 deletions tools/emcc_native/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
cmake_minimum_required(VERSION 3.20)
project(emcc_native CXX)

if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Default install prefix to Emscripten root" FORCE)
endif()

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

if(MSVC)
add_compile_options(/W4 /WX)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
else()
add_compile_options(-Wall -Wextra -Werror)
endif()

enable_testing()

add_library(native_launcher_lib OBJECT
config.cpp
driver.cpp
exec.cpp
)

include_directories(${CMAKE_CURRENT_SOURCE_DIR})

# Build `emcc` native launcher
add_executable(emcc main.cpp)
target_link_libraries(emcc PRIVATE native_launcher_lib)

# Create `em++` executable (symlink on Unix, copy on Windows)
if(WIN32)
set(CREATE_EMXX_COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:emcc> $<TARGET_FILE_DIR:emcc>/em++$<TARGET_FILE_SUFFIX:emcc>)
else()
set(CREATE_EMXX_COMMAND ${CMAKE_COMMAND} -E create_symlink $<TARGET_FILE_NAME:emcc> $<TARGET_FILE_DIR:emcc>/em++$<TARGET_FILE_SUFFIX:emcc>)
endif()

add_custom_command(
TARGET emcc POST_BUILD
COMMAND ${CREATE_EMXX_COMMAND}
COMMENT "Creating em++ launcher executable"
)

# Build unit tests
add_executable(native_tests tests/test_native.cpp)
target_link_libraries(native_tests PRIVATE native_launcher_lib)

add_test(NAME native_tests COMMAND native_tests)
Comment thread
sbc100 marked this conversation as resolved.

install(TARGETS emcc DESTINATION bin)
install(PROGRAMS $<TARGET_FILE_DIR:emcc>/em++$<TARGET_FILE_SUFFIX:emcc> DESTINATION bin)
96 changes: 96 additions & 0 deletions tools/emcc_native/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Native Clang Frontend Launcher (`emcc_native`)

`emcc_native` is a high-performance C++ launcher for Emscripten's compiler
drivers (`emcc` and `em++`). It drastically reduces compiler startup latency for
compile-only invocations (`-c`, `-S`, `-E`, `-M`, `-MM`) by bypassing Python and directly
executing `clang` / `clang++`.

## Overview & Architecture

When running large CMake or Ninja builds, `emcc` is launched hundreds or
thousands of times. Executing the Python interpreter for each single compile
unit adds overhead (especially on Windows).

`emcc_native` provides native executables (`bin/emcc`, `bin/em++`) that:
1. **Directly invoke Clang** for pure compilation steps (`-c`, `-S`, `-E`, `-M`, `-MM`), injecting:
- Target triple (`-target wasm32-unknown-emscripten` or `wasm64-unknown-emscripten`)
- Frontend exceptions flag (`-fignore-exceptions`)
- Default LLVM backend flags (e.g. `-mllvm -enable-emscripten-sjlj`)
- Emscripten sysroot (`--sysroot=<cache>/sysroot`)
- Clang sysroot include paths (e.g. `-Xclang -iwithsysroot/include/compat`)
- SIMD/SSE/NEON preprocessor macros (`-D__SSE__=1`, `-D__SSE2__=1`,
`-D__ARM_NEON__=1`, etc.) when architecture flags are specified
- Visibility flag (`-fvisibility=default` when `-fPIC` is passed without
`-fvisibility`)
2. **Ignore compile-unused linker flags**: Link-only flags (`--js-library`,
`--embed-file`, etc.) and linker settings (`-sEXPORTED_FUNCTIONS`, etc.) are
ignored during compilation (with diagnostic warnings matching `emcc.py`),
allowing compile steps with link flags to run natively.
3. **Fall back to Python** (`emcc.py` / `em++.py`) when link-phase invocations
are run, or when compile-time `-s` settings or system flags (`--clear-cache`,
`--build`, `--tracing`, etc.) are present.

## Building

Building requires CMake 3.20+ and a C++20 compiler.

```bash
cmake -B out/build_emcc_native -S tools/emcc_native
cmake --build out/build_emcc_native
cmake --install out/build_emcc_native
```

The output executables (`emcc`, `em++`) will be installed in `./bin`.

## Running Tests

To run the unit and integration tests:

```bash
ctest --test-dir out/build_emcc_native --output-on-failure
```

## Code Generation

Compile-time settings, link-only flags, and Emscripten warning options are
generated in `generated_settings.h`. To update this header from Python
definitions, run:

```bash
./tools/emcc_native/gen_settings.py
```

To verify whether `generated_settings.h` is up to date:

```bash
./tools/emcc_native/gen_settings.py --check
```

## Configuration & Environment Variables

- `EMCC_NATIVE`:
- Set to `0` to disable the native driver and unconditionally fall back to `emcc.py`.
- Set to `1` to force strict native mode; if an invocation requires falling back to Python, `emcc_native` will print the fallback reason and exit with an error (useful for debugging).
- `EMCC_DEBUG`: When set (e.g. `EMCC_DEBUG=1`), logs launcher decision details (whether direct Clang execution or Python fallback was selected, reason, target binary, and command arguments).
- `EMSDK_PYTHON`: Path to the Python executable (defaults to `python3` or `python.exe` on Windows).
- `EM_CACHE`: Path to Emscripten cache directory (defaults to `<emscripten_root>/cache`).
- `EM_CONFIG`: Path to `.emscripten` configuration file (reads `LLVM_ROOT` and `CACHE`).
- `EM_LLVM_ROOT`: Environment variable override for the directory containing LLVM binaries (`clang`, `clang++`).

## CI Benchmark Results

Compile-time performance is automatically benchmarked on CI across Linux,
macOS, and Windows (`embuilder build libc --force` compiling 1,075 files
sequentially with `EMCC_CORES=1`, `EMCC_USE_NINJA=0`, and
`EMCC_BATCH_BUILD=0`).

| Platform | Before (Python Baseline) | After (Native Launcher) | Improvement | Speedup |
| :---------: | :----------------------: | :---------------------: | :------------------------: | :-------: |
| **Linux** | 181.96 s (169.3 ms/file) | 64.82 s (60.3 ms/file) | -117.14 s (-109.0 ms/file) | **2.81x** |
| **Windows** | 343.90 s (319.9 ms/file) | 105.12 s (97.8 ms/file) | -238.78 s (-222.1 ms/file) | **3.27x** |
| **macOS** | 162.08 s (150.6 ms/file) | 64.73 s (60.1 ms/file) | -97.35 s (-90.5 ms/file) | **2.50x** |

As expected, because process creation and `python.exe` startup carry
significantly higher overhead on Windows than on POSIX systems, the speedup on
Windows CI (**3.27x**, saving over 222 ms per invocation) is even larger than on
Linux and macOS.
128 changes: 128 additions & 0 deletions tools/emcc_native/benchmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
# Copyright 2026 The Emscripten Authors. All rights reserved.
# Emscripten is available under two separate licenses, the MIT license and the
# University of Illinois/NCSA Open Source License. Both these licenses can be
# found in the LICENSE file.

"""Benchmark Emscripten compiler invocation speed across CI platforms.

Measures the elapsed time required to compile small source files (by default,
building libc via embuilder with batching and Ninja disabled so each C file is
invoked individually). Running this script before compiling the native launcher
benchmarks the Python driver baseline; running it after compiling benchmarks
the native C++ launcher.
"""

import argparse
import os
import subprocess
import sys
import time

script_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(os.path.dirname(script_dir))
sys.path.insert(0, root_dir)

from tools.utils import WINDOWS


def find_native_launcher():
ext = '.exe' if WINDOWS else ''
native_bin = os.path.join(root_dir, 'bin', 'emcc' + ext)
if os.path.exists(native_bin):
return native_bin
return None


def run_benchmark(target='libc', cores=1, iterations=1):
native_launcher = find_native_launcher()
if native_launcher:
mode = f'Native Launcher ({os.path.relpath(native_launcher, root_dir)})'
else:
mode = 'Python Launcher (baseline)'

env = os.environ.copy()
env['EMCC_CORES'] = str(cores)
env['EMCC_USE_NINJA'] = '0'
env['EMCC_BATCH_BUILD'] = '0'
env.pop('EM_COMPILER_WRAPPER', None)
if native_launcher:
if 'EMCC_NATIVE' not in env:
env['EMCC_NATIVE'] = '1'
else:
env.pop('EMCC_NATIVE', None)

embuilder_py = os.path.join(root_dir, 'embuilder.py')
cmd = [sys.executable, embuilder_py, 'build', target, '--force']

print('=' * 60)
print('Emscripten Compiler Benchmark')
print('=' * 60)
print(f'Mode: {mode}')
print(f'Target: {target}')
print(f'Iterations: {iterations}')
print(f'Settings: EMCC_CORES={cores}, EMCC_USE_NINJA=0, EMCC_BATCH_BUILD=0')
print('=' * 60)

times = []
for i in range(1, iterations + 1):
if iterations > 1:
print(f'\n--- Iteration {i} of {iterations} ---')
start_time = time.perf_counter()
res = subprocess.run(cmd, env=env, check=False)
elapsed = time.perf_counter() - start_time
if res.returncode != 0:
print(f'Error: benchmark command failed with exit code {res.returncode}')
return res.returncode
times.append(elapsed)
print(f'Iteration {i} took: {elapsed:.3f} s')

print('\n' + '=' * 60)
print('Benchmark Summary')
print('=' * 60)
print(f'Mode: {mode}')
if iterations == 1:
print(f'Total Time: {times[0]:.3f} s')
else:
avg_time = sum(times) / len(times)
min_time = min(times)
max_time = max(times)
print(f'Average Time: {avg_time:.3f} s')
print(f'Min Time: {min_time:.3f} s')
print(f'Max Time: {max_time:.3f} s')
print('=' * 60)
return 0


def main():
parser = argparse.ArgumentParser(
description='Benchmark Emscripten compiler invocation speed.',
)
parser.add_argument(
'target',
nargs='?',
default='libc',
help='Library target to build (default: libc)',
)
parser.add_argument(
'--cores',
type=int,
default=1,
help='Number of cores for EMCC_CORES (default: 1)',
)
parser.add_argument(
'-n',
'--iterations',
type=int,
default=1,
help='Number of benchmark iterations to run (default: 1)',
)
args = parser.parse_args()

return run_benchmark(
target=args.target, cores=args.cores, iterations=args.iterations,
)


if __name__ == '__main__':
sys.exit(main())
Loading
Loading