|
1 | | -# [Clangd](https://clangd.llvm.org) in Browser |
| 1 | +# clangd browser runtime |
2 | 2 |
|
3 | | -Finally, I made clangd work in browser. You can now get C++ IntelliSense directly without installing any native compiler or relying on a remote server. |
| 3 | +This project packages Monaco and the WebAssembly build of clangd as a reusable browser component. It creates an editor only inside the element supplied by the host page; it does not use an iframe, register a service worker, replace page-level DOM, or own Run/output UI. |
4 | 4 |
|
5 | | -This repo contains a build script for clangd on WebAssembly (TBD), and an example of using clangd WASM module together with [Monaco Editor](https://microsoft.github.io/monaco-editor) powered by it's Language Client port (thanks [TypeFox/monaco-languageclient](https://github.com/TypeFox/monaco-languageclient)). |
| 5 | +## Use the hosted runtime |
6 | 6 |
|
7 | | -## How to build |
| 7 | +The preferred deployment uses separate origins: |
8 | 8 |
|
9 | | -You can take a look at `build.sh` for how to build clangd to WASM. Once `clangd.wasm` generated at `public/wasm`, you can build those frontend code using `pnpm install` and `pnpm build`. |
| 9 | +```text |
| 10 | +https://www.example.com/ host page |
| 11 | +https://clangd.example.com/ contents of dist/ |
| 12 | +``` |
10 | 13 |
|
11 | | -## About the example page |
| 14 | +The host can be ordinary HTML and JavaScript: |
12 | 15 |
|
13 | | -I've publish a [GitHub Page](https://clangd.guyutongxue.site/) for demonstrating how clangd works in your browser. |
| 16 | +```html |
| 17 | +<div class="editor-shell"> |
| 18 | + <textarea id="fallback">int main() { return 0; }</textarea> |
| 19 | + <div id="monaco-host"></div> |
| 20 | +</div> |
14 | 21 |
|
15 | | -Notice that clangd is a multi-thread program, so we need `SharedArrayBuffer` -- and more over, a strict context called `crossOriginIsolated`. GitHub Pages do not send COOP/COEP headers for us to enabling that context, so I served this site through CloudFlare with a custom rule adding those headers. If you want to deploy this project by yourself, make sure correct COOP/COEP header is set on the server side, or you can use [`coi-serviceworker`](https://github.com/gzuidhof/coi-serviceworker). |
| 22 | +<script type="module"> |
| 23 | + const fallback = document.querySelector("#fallback"); |
| 24 | + const host = document.querySelector("#monaco-host"); |
| 25 | + const { createCppEditor } = |
| 26 | + await import("https://clangd.example.com/editor.js"); |
16 | 27 |
|
17 | | -You can pass URL search parameters to control the initial state of this page ([see here](./docs/params.md)), or embed it in your website ([see here](./docs/embed.md), **your website needs to be cross-origin-isolated too**) |
| 28 | + const editor = await createCppEditor({ |
| 29 | + element: host, |
| 30 | + value: fallback.value, |
| 31 | + theme: "dark", |
| 32 | + onStatus(status) { |
| 33 | + console.log(status); |
| 34 | + }, |
| 35 | + }); |
18 | 36 |
|
19 | | -## Acknowledgement |
| 37 | + // Capture edits made while Monaco loaded, then swap only the editor nodes. |
| 38 | + editor.setValue(fallback.value); |
| 39 | + host.style.visibility = "visible"; |
| 40 | + editor.layout(); |
| 41 | + fallback.remove(); |
20 | 42 |
|
21 | | -- [soedirgo/llvm-wasm](https://github.com/soedirgo/llvm-wasm) provides a very detailed guide on how to build LLVM to wasm. Better than famous works like [binji/wasm-clang](https://github.com/binji/wasm-clang) or [tbfleming/cib](https://github.com/tbfleming/cib) (but also thank you guys :P); |
22 | | -- [nokotan/vscode-clangd](https://github.com/nokotan/vscode-clangd) patch vscode's clangd extension with WASM binary to make it work on vscode-web. Some bugs but useful; no build scripts or further documentation; |
23 | | -- [ConorBobbleHat/clangd-wasm](https://github.com/ConorBobbleHat/clangd-wasm) and [ConorBobbleHat/clangd-wasm-core](https://github.com/ConorBobbleHat/clangd-wasm-core); build scripts provided but lack examples on how to use them. |
| 43 | + await editor.clangdReady; |
| 44 | +</script> |
| 45 | +``` |
| 46 | + |
| 47 | +`createCppEditor()` resolves as soon as Monaco is usable. clangd downloads and starts independently; `editor.clangdReady` resolves after the language client connects and rejects if clangd is unavailable. A missing cross-origin-isolation policy affects clangd only; Monaco remains usable. |
| 48 | + |
| 49 | +The returned component API is: |
| 50 | + |
| 51 | +```js |
| 52 | +editor.getValue(); |
| 53 | +editor.setValue(source); |
| 54 | +editor.focus(); |
| 55 | +editor.layout(); |
| 56 | +editor.dispose(); |
| 57 | +const unsubscribe = editor.onDidChange((source) => {}); |
| 58 | +await editor.clangdReady; |
| 59 | +``` |
| 60 | + |
| 61 | +### Lightweight fallback |
| 62 | + |
| 63 | +The fallback editor is a separate, small public module and does not load Monaco: |
| 64 | + |
| 65 | +```js |
| 66 | +const { createCppFallbackEditor } = |
| 67 | + await import("https://clangd.example.com/fallback.js"); |
| 68 | + |
| 69 | +const fallback = createCppFallbackEditor({ |
| 70 | + element: document.querySelector(".fallback-host"), |
| 71 | + value: "int main() { return 0; }", |
| 72 | +}); |
| 73 | + |
| 74 | +fallback.getValue(); |
| 75 | +fallback.setValue("int main() { return 1; }"); |
| 76 | +fallback.focus(); |
| 77 | +const unsubscribe = fallback.onDidChange((source) => {}); |
| 78 | +fallback.dispose(); |
| 79 | +``` |
| 80 | + |
| 81 | +`fallback.js` contains its scoped styles, native textarea overlay, line numbers, scrolling synchronization, and lightweight C++ syntax highlighter. It can also enhance an existing textarea inside the supplied element, preserving source included in the initial HTML. |
| 82 | + |
| 83 | +### ANSI output |
| 84 | + |
| 85 | +ANSI SGR rendering is available from the main entry and as a lightweight standalone module: |
| 86 | + |
| 87 | +```js |
| 88 | +const { appendAnsi, ansiToFragment, stripAnsi } = |
| 89 | + await import("https://clangd.example.com/ansi.js"); |
| 90 | + |
| 91 | +appendAnsi(outputElement, compilerOutput); |
| 92 | +appendAnsi(outputElement, stderr, { className: "stderr" }); |
| 93 | +``` |
| 94 | + |
| 95 | +The renderer creates text nodes and styled spans rather than injecting HTML. The same functions are also exported by `editor.js`. |
| 96 | + |
| 97 | +Status events have a `type` of `monaco-loading`, `monaco-ready`, `clangd-downloading`, `clangd-starting`, `clangd-ready`, or `clangd-error`. Download events also contain `loaded` and, when the server provides it, `total` bytes. |
| 98 | + |
| 99 | +See [demo/index.html](demo/index.html), [demo/demo.js](demo/demo.js), and [demo/demo.css](demo/demo.css) for a complete framework-free highlighted fallback editor, seamless handoff, ANSI-aware Run output, and Compiler Explorer link. The demo's **Show fallback / Show Monaco** toggle recreates the fallback on demand for visual comparison; the normal one-way startup handoff still removes and cleans up the fallback. |
| 100 | + |
| 101 | +## Headers |
| 102 | + |
| 103 | +The host document must be cross-origin isolated for clangd's SharedArrayBuffer/pthread build: |
| 104 | + |
| 105 | +```http |
| 106 | +Cross-Origin-Opener-Policy: same-origin |
| 107 | +Cross-Origin-Embedder-Policy: require-corp |
| 108 | +``` |
| 109 | + |
| 110 | +The runtime asset server must make its public modules, workers, CSS, and WASM available cross-origin: |
| 111 | + |
| 112 | +```http |
| 113 | +Access-Control-Allow-Origin: * |
| 114 | +Cross-Origin-Resource-Policy: cross-origin |
| 115 | +``` |
| 116 | + |
| 117 | +Serve `.wasm` as `application/wasm`. [public/_headers](public/_headers) is an example configuration for static hosts that support Netlify-style header files. |
| 118 | + |
| 119 | +The cross-origin worker bootstrap uses Blob module workers. A restrictive host Content Security Policy therefore needs at least: |
| 120 | + |
| 121 | +```http |
| 122 | +Content-Security-Policy: worker-src blob:; script-src 'self' https://clangd.example.com blob: |
| 123 | +``` |
| 124 | + |
| 125 | +Adjust the runtime origin and the rest of the policy for the site. The runtime never registers a service worker or attempts to change the host's COOP/COEP policy. GitHub Pages users need a fronting service capable of setting these headers, or a COI service-worker solution managed by the host site itself. |
| 126 | + |
| 127 | +## Self-host the same package |
| 128 | + |
| 129 | +Extract the archive without rebuilding: |
| 130 | + |
| 131 | +```bash |
| 132 | +mkdir -p assets/clangd |
| 133 | +tar xf clangd-browser-runtime.tar.gz -C assets/clangd |
| 134 | +``` |
| 135 | + |
| 136 | +Then import the exact same distribution from the host origin: |
| 137 | + |
| 138 | +```js |
| 139 | +const { createCppEditor } = await import("/assets/clangd/editor.js"); |
| 140 | +``` |
| 141 | + |
| 142 | +All resource URLs are relative to emitted modules, so the distribution works at the origin root or any directory such as `/assets/clangd/`. |
| 143 | + |
| 144 | +## Build and package |
| 145 | + |
| 146 | +Normal frontend builds use prebuilt artifacts and never clone or compile LLVM: |
| 147 | + |
| 148 | +```bash |
| 149 | +pnpm install |
| 150 | +pnpm build |
| 151 | +pnpm pack:runtime |
| 152 | +``` |
| 153 | + |
| 154 | +The build produces stable `dist/editor.js`, `dist/fallback.js`, `dist/ansi.js`, `dist/editor.css`, internal chunks in `dist/assets/`, and the complete Emscripten artifact set in `dist/wasm/`. Packaging creates `clangd-browser-runtime.tar.gz`. |
| 155 | + |
| 156 | +Install an existing artifact directory or archive with: |
| 157 | + |
| 158 | +```bash |
| 159 | +./scripts/install-clangd-artifacts path/to/clangd-artifacts.tar.gz |
| 160 | +``` |
| 161 | + |
| 162 | +The input must contain `clangd.js`, `clangd.wasm`, and any other `clangd*` files produced by Emscripten. `build.sh` remains a separate manual reference workflow and is not invoked by `pnpm build` or CI. |
| 163 | + |
| 164 | +## Cross-origin demo and tests |
| 165 | + |
| 166 | +Build first, then start all acceptance origins: |
| 167 | + |
| 168 | +```bash |
| 169 | +pnpm build |
| 170 | +pnpm demo:origins |
| 171 | +``` |
| 172 | + |
| 173 | +- `http://localhost:4173` serves the demo host with COOP/COEP. |
| 174 | +- `http://localhost:4174` serves runtime assets with CORS/CORP. |
| 175 | +- `http://localhost:4175` serves the same runtime at `/assets/clangd/` for same-origin testing. |
| 176 | +- `http://localhost:4176` serves a non-isolated host for graceful-degradation testing. |
| 177 | + |
| 178 | +Run the Chromium acceptance suite with: |
| 179 | + |
| 180 | +```bash |
| 181 | +pnpm exec playwright install chromium |
| 182 | +pnpm test:browser |
| 183 | +``` |
| 184 | + |
| 185 | +The tests cover immediate fallback editing and Run, delayed runtime/WASM, edit-preserving handoff, DOM ownership, cross-origin isolation and worker startup, real clangd diagnostics, failure behavior, and same-origin extraction layout. |
| 186 | + |
| 187 | +## Compiler Explorer Run UI |
| 188 | + |
| 189 | +Compilation is intentionally a host concern. The demo keeps an `activeEditor.getValue()` abstraction that initially reads the textarea and later points to Monaco, then sends that source directly to the Compiler Explorer API. This makes Run independent of both Monaco and clangd readiness. |
0 commit comments