Skip to content

Commit 2935fda

Browse files
committed
Optimize WASM loading and improve playground examples
Performance improvements: - Pre-load WASM binary once in main thread (60MB download) - Share pre-loaded binary with all workers via ArrayBuffer transfer - Eliminates redundant downloads (was 120MB+ per compile, now 0MB) - Fresh worker per compilation still avoids signal handler buildup UI improvements: - Remove stats display from compiler output header for cleaner look - Update version display to show "clang v22.0.0git" format - Replace stdlib.h examples with self-contained code examples New examples: - Null Check Example: Basic flow-sensitive null checking - Function Invalidates Check: Shows narrowing invalidation (Phase 4) - _Nonnull Annotations: Type-level null safety enforcement Result: Single 60MB download on first load, unlimited compilations with zero additional downloads.
1 parent caf608d commit 2935fda

File tree

3 files changed

+75
-150
lines changed

3 files changed

+75
-150
lines changed

README.md

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,40 +39,31 @@ Null pointer dereferences are just one category of memory safety bugs. Here's ho
3939
4040
### What Gets Fixed
4141
42-
| Safety Issue | Standard C | **Null-Safe Clang** (null checking) | Clang `-fbounds-safety` | Rust |
43-
|-------------|------------|:-----------------------------------:|-------------------------|------|
44-
| Null pointer dereferences | ❌ Unsafe | **✅ Fixed** | ❌ Unsafe | ✅ Fixed |
45-
| Buffer overflows | ❌ Unsafe | ❌ Unsafe | ✅ Fixed | ✅ Fixed |
46-
| Use-after-free | ❌ Unsafe | ❌ Unsafe | ❌ Unsafe | ✅ Fixed |
47-
| Double-free | ❌ Unsafe | ❌ Unsafe | ❌ Unsafe | ✅ Fixed |
48-
| Uninitialized memory | ❌ Unsafe | ❌ Unsafe | ⚠️ Partial | ✅ Fixed |
42+
| Safety Issue | Standard C | **Null-Safe Clang** (null checking) |
43+
|-------------------------|------------|:-----------------------------------:|
44+
| Null pointer dereferences | ❌ Unsafe | **✅ Fixed** |
45+
| Buffer overflows | ❌ Unsafe | ❌ Unsafe |
46+
| Use-after-free | ❌ Unsafe | ❌ Unsafe |
47+
| Double-free | ❌ Unsafe | ❌ Unsafe |
48+
| Uninitialized memory | ❌ Unsafe | ❌ Unsafe |
4949
5050
51-
### Why This Still Matters
51+
### Why you still might want to try this
5252
5353
While Null-Safe Clang doesn't solve all memory safety issues, null pointer dereferences are a significant problem:
5454
55-
- One in four memory safety bugs involve null pointer dereferences ([Microsoft Security Response Center](https://github.com/microsoft/MSRC-Security-Research/blob/master/papers/2019/The%20Memory%20Safety%20Story.pdf))
56-
- Zero runtime overhead - nullability checks are compile-time only, no runtime checks inserted
55+
- Many memory safety bugs involve null pointer dereferences
5756
- Easier to adopt than rewriting in Rust (100% compatible with existing C code)
58-
- Incremental deployment (warnings by default, can enable per-file)
5957
- Complements other efforts (combine with `-fbounds-safety` for buffer safety)
58+
- Incremental deployment (warnings by default, can enable per-file)
6059
6160
6261
## Usage
6362
6463
Basic usage (warnings enabled by default):
6564
```bash
6665
clang mycode.c # Warnings for nullable dereferences
67-
```
68-
69-
Promote warnings to errors:
70-
```bash
7166
clang -Werror=nullability mycode.c # Treat nullability issues as errors
72-
```
73-
74-
Disable strict nullability:
75-
```bash
7667
clang -fno-strict-nullability mycode.c # Turn off nullability checking
7768
```
7869

@@ -92,15 +83,13 @@ void legacy_function(int* p) { ... }
9283
9384
- Nullable-by-default: All pointers are `_Nullable` unless marked `_Nonnull`
9485
- Flow-sensitive narrowing: `if (p)` proves `p` is non-null in that scope
95-
- Smart invalidation: Assumes functions have side effects; use `__attribute__((pure))` or `__attribute__((const))` to preserve narrowing
9686
- Early-exit patterns: Understands `return`, `goto`, `break`, `continue`
97-
- Multi-level pointers: Works with `int**`, `int***`, etc.
9887
- Pointer arithmetic: `q = p + 1` preserves narrowing from `p`
99-
- Type checking: Through function calls, returns, and assignments
100-
- Typedef support: Nullability annotations work seamlessly with typedefs
88+
- Type checking through function calls, returns, and assignments
89+
- Works with Typedefs
90+
- Assumes functions have side effects (use `__attribute__((pure))` or `__attribute__((const))` to preserve narrowing)
10191
- Null-safe headers: Annotated C standard library in `clang/nullsafe-headers/`
102-
- IDE integration: Enhanced `clangd` with real-time nullability diagnostics
103-
- Real-world tested: Validated on cJSON, SQLite
92+
- IDE integration: `clangd` built from this fork has the same logic and warnings as clang
10493
10594
## How It Works
10695

nullsafe-playground/compiler-worker.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ self.onmessage = function(e) {
1010

1111
if (type === 'load') {
1212
// Configure Module BEFORE loading clang.js
13-
// Emscripten will check if Module exists and merge with it
13+
// The main thread pre-loaded the WASM binary and is sharing it
1414
self.Module = {
1515
print: function(text) {
1616
self.postMessage({ type: 'stdout', text });
@@ -19,10 +19,10 @@ self.onmessage = function(e) {
1919
self.postMessage({ type: 'stderr', text });
2020
},
2121
noInitialRun: true,
22+
wasmBinary: data.wasmBinary, // Use the shared WASM binary
2223
postRun: [function() {
2324
// postRun happens AFTER runtime initialization
2425
console.log('Worker: postRun called');
25-
console.log('Worker: Checking what exists on Module:', Object.keys(self.Module).filter(k => k.startsWith('FS') || k === 'FS'));
2626

2727
// Try different FS locations
2828
const FS = self.Module.FS || self.FS || (typeof FS !== 'undefined' ? FS : null);
@@ -38,7 +38,6 @@ self.onmessage = function(e) {
3838
const checkFS = setInterval(() => {
3939
attempts++;
4040
const fsNow = self.Module.FS || self.FS || (typeof FS !== 'undefined' ? FS : null);
41-
console.log(`Worker: Polling for FS (attempt ${attempts}):`, !!fsNow);
4241

4342
if (fsNow) {
4443
clearInterval(checkFS);

nullsafe-playground/index.html

Lines changed: 59 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -528,8 +528,8 @@ <h1>Null-Safe Clang Playground</h1>
528528

529529
<select id="examplesSelect" class="examples-select">
530530
<option value="null-check" selected>Null Check Example</option>
531-
<option value="array">Array Safety</option>
532-
<option value="function">Function Parameters</option>
531+
<option value="function-invalidates">Function Invalidates Check</option>
532+
<option value="nonnull-annotation">_Nonnull Annotations</option>
533533
</select>
534534

535535
<button id="shareBtn" class="btn btn-secondary">
@@ -564,9 +564,6 @@ <h1>Null-Safe Clang Playground</h1>
564564
<div class="pane">
565565
<div class="pane-header">
566566
<span>Compiler Output</span>
567-
<div class="pane-actions">
568-
<span id="outputStats" class="icon-btn" style="cursor: default;"></span>
569-
</div>
570567
</div>
571568
<div class="output-container">
572569
<div class="output-section">
@@ -594,7 +591,6 @@ <h1>Null-Safe Clang Playground</h1>
594591
const shareBtn = document.getElementById('shareBtn');
595592
const loadingBar = document.getElementById('loadingBar');
596593
const toast = document.getElementById('toast');
597-
const outputStats = document.getElementById('outputStats');
598594
const divider = document.getElementById('divider');
599595
const outputDivider = document.getElementById('outputDivider');
600596

@@ -608,40 +604,29 @@ <h1>Null-Safe Clang Playground</h1>
608604
void unsafe(int* data) {
609605
*data = 42; // warning - data might be null!
610606
}`,
611-
'array': `#include <stdlib.h>
607+
'function-invalidates': `void modify(int** ptr); // external function
612608
613-
int main(void) {
614-
int *arr = malloc(10 * sizeof(int));
609+
void example(int* data) {
610+
if (data) {
611+
*data = 1; // OK - checked
615612
616-
arr[0] = 42; // warning - malloc can return NULL
613+
modify(&data); // might modify data!
617614
618-
if (arr) {
619-
arr[0] = 42; // OK - arr is non-null here
620-
free(arr);
615+
*data = 2; // warning - data could be null now!
621616
}
622-
623-
return 0;
624617
}`,
625-
'function': `void process(int *data) {
626-
*data = 42; // warning - data might be null
627-
}
628-
629-
void process_safe(int *data) {
630-
if (data) {
631-
*data = 42; // OK - checked for null
632-
}
633-
}
634-
635-
void guaranteed(int * _Nonnull data) {
618+
'nonnull-annotation': `// Use _Nonnull to require non-null arguments
619+
void process_nonnull(int * _Nonnull data) {
636620
*data = 42; // OK - caller must pass non-null
637621
}
638622
639-
int main(void) {
640-
int x = 0;
641-
process(&x);
642-
process_safe(&x);
643-
guaranteed(&x);
644-
return 0;
623+
void caller(void) {
624+
int* nullable = 0;
625+
int x = 10;
626+
627+
process_nonnull(0); // error!
628+
process_nonnull(nullable); // error!
629+
process_nonnull(&x); // OK
645630
}`
646631
};
647632

@@ -652,6 +637,7 @@ <h1>Null-Safe Clang Playground</h1>
652637
let isCompiling = false;
653638
let isDragging = false;
654639
let scriptUrl = null; // URL for worker to load clang.js
640+
let wasmBinary = null; // Pre-loaded WASM binary to share with workers
655641

656642
// Toast notification
657643
function showToast(message, duration = 2000) {
@@ -743,79 +729,40 @@ <h1>Null-Safe Clang Playground</h1>
743729
status.className = 'status';
744730
loadingBar.classList.add('active');
745731

746-
let capturedStdout = '';
747-
let capturedStderr = '';
748-
749732
try {
750733
// Determine script URL
751734
const isDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
752735
scriptUrl = isDev
753736
? './clang.js'
754737
: 'https://github.com/cs01/llvm-project/releases/latest/download/clang-nullsafe.js';
755738

756-
// Create a temporary worker to get the version
757-
const worker = new Worker('compiler-worker.js');
758-
759-
let versionResolved = false;
760-
let loadTimeout = setTimeout(() => {
761-
if (!versionResolved) {
762-
worker.terminate();
763-
throw new Error('Compiler initialization timeout');
764-
}
765-
}, 60000); // 60 second timeout
766-
767-
worker.onmessage = function(e) {
768-
const { type, text, error } = e.data;
769-
770-
if (type === 'ready') {
771-
capturedStdout = '';
772-
capturedStderr = '';
773-
worker.postMessage({
774-
type: 'compile',
775-
code: 'int main() { return 0; }',
776-
extraFlags: ['--version']
777-
});
778-
} else if (type === 'stdout') {
779-
capturedStdout += text + '\n';
780-
} else if (type === 'stderr') {
781-
capturedStderr += text + '\n';
782-
} else if (type === 'complete') {
783-
if (!versionResolved) {
784-
versionResolved = true;
785-
clearTimeout(loadTimeout);
786-
787-
const versionOutput = capturedStdout + capturedStderr;
788-
const versionMatch = versionOutput.match(/clang version ([^\s]+)/);
789-
if (versionMatch) {
790-
clangVersion = versionMatch[1];
791-
document.querySelector('.output-section-header').textContent =
792-
`Null-Safe Clang ${clangVersion}`;
793-
}
794-
795-
worker.terminate();
796-
797-
status.textContent = 'Ready to compile';
798-
status.className = 'status ready';
799-
compileBtn.disabled = false;
800-
loadingBar.classList.remove('active');
801-
showToast('Compiler loaded successfully!');
802-
803-
setTimeout(() => compile(), 500);
804-
}
805-
} else if (type === 'error') {
806-
clearTimeout(loadTimeout);
807-
worker.terminate();
808-
throw new Error(error);
809-
}
810-
};
739+
const wasmUrl = isDev
740+
? './clang.wasm'
741+
: 'https://github.com/cs01/llvm-project/releases/latest/download/clang-nullsafe.wasm';
742+
743+
// Pre-load the WASM binary ONCE
744+
console.log('Downloading WASM binary...');
745+
const wasmResponse = await fetch(wasmUrl);
746+
wasmBinary = await wasmResponse.arrayBuffer();
747+
console.log('WASM binary loaded:', wasmBinary.byteLength, 'bytes');
748+
749+
// Now test with a worker to get the version
750+
const versionResult = await compileCode('int main() { return 0; }', ['--version']);
751+
const versionOutput = versionResult.stdout + versionResult.stderr;
752+
const versionMatch = versionOutput.match(/clang version ([^\s]+)/);
753+
if (versionMatch) {
754+
clangVersion = versionMatch[1];
755+
document.querySelector('.output-section-header').textContent =
756+
`Null-Safe Clang ${clangVersion}`;
757+
}
811758

812-
worker.onerror = function(error) {
813-
clearTimeout(loadTimeout);
814-
worker.terminate();
815-
throw error;
816-
};
759+
status.textContent = 'Ready to compile';
760+
status.className = 'status ready';
761+
compileBtn.disabled = false;
762+
loadingBar.classList.remove('active');
763+
showToast('Compiler loaded successfully!');
817764

818-
worker.postMessage({ type: 'load', scriptUrl });
765+
setTimeout(() => compile(), 500);
819766

820767
} catch (error) {
821768
status.textContent = 'Failed to load compiler';
@@ -840,33 +787,30 @@ <h1>Null-Safe Clang Playground</h1>
840787
status.className = 'status compiling';
841788
outputNullsafe.innerHTML = '';
842789
outputMainline.innerHTML = '';
843-
outputStats.textContent = '';
844790
loadingBar.classList.add('active');
845791

846792
const startTime = performance.now();
847793

848794
try {
849795
const code = editor.value;
850796

851-
// Compile with null-safety enabled (default)
852-
const nullsafeFlags = [];
853-
const nullsafeResult = await compileCode(code, nullsafeFlags);
854-
855-
// Compile with nullability warnings suppressed (simulates mainline)
856-
const mainlineFlags = ['-Wno-nullability'];
857-
const mainlineResult = await compileCode(code, mainlineFlags);
797+
// Compile both versions in parallel
798+
const [nullsafeResult, mainlineResult] = await Promise.all([
799+
compileCode(code, []),
800+
compileCode(code, ['-Wno-nullability'])
801+
]);
858802

859803
const duration = (performance.now() - startTime).toFixed(0);
860804

861805
// Build command strings
862806
const baseArgs = '-fsyntax-only --target=wasm32-unknown-emscripten';
863-
const nullsafeCmd = `$ clang ${baseArgs}${nullsafeFlags.length ? ' ' + nullsafeFlags.join(' ') : ''} input.c`;
864-
const mainlineCmd = `$ clang ${baseArgs}${mainlineFlags.length ? ' ' + mainlineFlags.join(' ') : ''} input.c`;
807+
const nullsafeCmd = `$ clang ${baseArgs} input.c`;
808+
const mainlineCmd = `$ clang ${baseArgs} -Wno-nullability input.c`;
865809

866810
// Update headers with clearer titles
867811
const headers = document.querySelectorAll('.output-section-header');
868-
headers[0].textContent = `With Null Warnings (v${clangVersion})`;
869-
headers[1].textContent = `Without Null Warnings (v${clangVersion})`;
812+
headers[0].textContent = `With Null Warnings (clang v${clangVersion})`;
813+
headers[1].textContent = `Without Null Warnings (clang v${clangVersion})`;
870814

871815
// Count nullability warnings to detect missed bugs
872816
const nullWarningCount = (nullsafeResult.stderr.match(/\[-Wnullability\]/g) || []).length;
@@ -890,18 +834,6 @@ <h1>Null-Safe Clang Playground</h1>
890834
}
891835
}
892836

893-
// Calculate stats from null-safe results
894-
const errorCount = (nullsafeResult.stderr.match(/error:/g) || []).length;
895-
const warningCount = (nullsafeResult.stderr.match(/warning:/g) || []).length;
896-
897-
let stats = [];
898-
if (errorCount > 0) stats.push(`${errorCount} error${errorCount !== 1 ? 's' : ''}`);
899-
if (warningCount > 0) stats.push(`${warningCount} warning${warningCount !== 1 ? 's' : ''}`);
900-
if (nullWarningCount > 0) stats.push(`(${nullWarningCount} null bug${nullWarningCount !== 1 ? 's' : ''})`);
901-
stats.push(`${duration}ms`);
902-
903-
outputStats.textContent = stats.join(' • ');
904-
905837
status.textContent = 'Ready to compile';
906838
status.className = 'status ready';
907839
} catch (error) {
@@ -910,7 +842,6 @@ <h1>Null-Safe Clang Playground</h1>
910842
outputMainline.innerHTML = `<span class="error">Compilation failed: ${errorMsg}\n\nCheck console for details.</span>`;
911843
status.textContent = 'Compilation error';
912844
status.className = 'status error';
913-
outputStats.textContent = '✗ Failed';
914845
console.error('Compilation error:', error);
915846
} finally {
916847
isCompiling = false;
@@ -938,6 +869,7 @@ <h1>Null-Safe Clang Playground</h1>
938869
const { type, text, error, exitCode } = e.data;
939870

940871
if (type === 'ready') {
872+
// Worker is ready, send compile request
941873
worker.postMessage({
942874
type: 'compile',
943875
code,
@@ -967,7 +899,12 @@ <h1>Null-Safe Clang Playground</h1>
967899
reject(error);
968900
};
969901

970-
worker.postMessage({ type: 'load', scriptUrl });
902+
// Send the pre-loaded WASM binary to the worker
903+
worker.postMessage({
904+
type: 'load',
905+
scriptUrl,
906+
wasmBinary // Share the pre-loaded WASM
907+
});
971908
});
972909
}
973910

0 commit comments

Comments
 (0)