Hi everyone! I just released v0.1.0 of Falco — a browser engine I've been building in Rust on nights and weekends. No WebKit, no Gecko, no Chromium — every module is written from scratch in ~36,000 lines.
🔗 GitHub: https://github.com/poxk/Falco
🔗 Releases (prebuilt binaries for Linux/macOS/Windows): https://github.com/poxk/Falco/releases/tag/v0.1.0
What's inside
All modules are from scratch, with no dependency on existing browser engines:
HTML5 (html5/ + html.rs)
- Tokenizer — all 80 states of WHATWG §13.2.5, including script-data escape/double-escape state machine, attribute parsing with duplicate detection, named + numeric character references with Windows-1252 quirks table
- Tree builder — all 22 insertion modes of WHATWG §13.2.6, stack of open elements with scope algorithms (default/button/table/select/list-item), active formatting elements list, adoption agency algorithm (8-iteration outer loop with full inner loop and bookmark tracking), foster parenting for table content
- XML parser — strict, with namespace bindings, CDATA, PIs
- Encoding detection — BOM, HTTP Content-Type charset,
<meta charset>,<meta http-equiv>, heuristic UTF-8/UTF-16 detection, decoders for UTF-8 / UTF-16LE / UTF-16BE / Windows-1252 - innerHTML/outerHTML serialization — void elements,
<template>contents fragment, raw text elements, full attribute value escaping
DOM (dom2/)
-
NodeRef = Rc<RefCell<Node>>with parent/firstChild/lastChild/previousSibling/nextSibling pointers per spec - MutationObserver with observe()/disconnect()/take_records(), subtree ancestor matching, attributeFilter
- Shadow DOM — attachShadow() with open/closed modes, host validation, named + default slots, fallback content, slot distribution (flatten tree algorithm)
- Custom elements — customElements.define() with name validation, observedAttributes, lifecycle callbacks (connected/disconnected/adopted/attributeChanged/form-associated), pending upgrades, customized built-in elements (is="...")
- Accessibility tree — parallel tree with role/name/description/state/actions, implicit ARIA roles for ~50 HTML tags
CSS (css/ + css2/)
- Selectors Level 4 — type, class, id, universal, descendant, child, adjacent sibling, general sibling, attribute (=, ~=, |=, ^=, $=, *=)
- Pseudo-classes — :hover, :focus, :focus-visible, :focus-within, :active, :visited, :checked, :disabled, :enabled, :empty, :root, :first-child, :last-child, :only-child, :first-of-type, :last-of-type, :only-of-type, :nth-child(an+b), :nth-last-child, :nth-of-type, :nth-last-of-type, :nth-child(an+b of S), :is(), :where(), :not(), :has() (with relative selectors), :lang(), :dir()
- Cascade & specificity — (a, b, c) tuple, :where() zero specificity, :is()/:not()/:has() most specific arg, CascadeOrigin (UA/User/Author) with reversed order for !important, CascadeLayers
- @-rules — @media (parsed), @keyframes with cubic-bezier & steps timing, @font-face, @layer (cascade layers), @container queries with evaluate_container_query()
- Logical properties — margin-inline-start etc. resolved based on writing-mode
JavaScript VM (tjs/ + tjs_ext/)
- Custom bytecode VM with generational GC, inline caching, hidden classes, JIT tier-up (x86_64 only)
- ES2015+ syntax — let/const, arrow functions, template literals, destructuring, default + rest params, spread, for...of, computed property names, optional chaining, nullish coalescing
- Types — Symbol with 13 well-known symbols, BigInt (arbitrary precision, u32 limbs, signed), Promise with then/catch/finally + state machine, Iterator protocol, Generator as state machine
- Built-ins — Math, JSON, Array (map/filter/reduce/forEach/find/findIndex/includes/slice/splice/flat/flatMap), String (split/replace/match/padStart/padEnd/trim/startsWith/endsWith/includes/repeat), Object (keys/values/entries/assign/freeze/fromEntries), Reflect (get/set/has/deleteProperty/ownKeys), WeakMap, WeakSet, Map, Set, Proxy
- Microtask queue — Promise reactions drained as microtasks
- DOM bindings — document.getElementById, querySelector/querySelectorAll, console.log, alert, addEventListener (basic)
- Event loop integration — setTimeout, setInterval, requestAnimationFrame, fetch() (returns Promise), XMLHttpRequest
Layout (layout/)
- Block flow, inline flow (horizontal text wrapping with baselines)
- Flexbox — flex-direction, justify-content, align-items, flex-wrap, gap, flex-grow/shrink/basis
- CSS Grid — grid-template-columns/rows (with fr, auto, minmax(), repeat()), grid-column/grid-row placement, gap, auto-flow
- Table layout —
<table>,<tr>,<td>,<th>,<thead>,<tbody>,<tfoot>,<caption>, column width distribution, border collapse - Float, inline-block, position (static/relative/absolute/fixed)
- Units — px, em, rem, pt, %, vw, vh
- Writing modes — horizontal-tb, vertical-rl/lr, sideways-rl/lr
Painting (paint/ + svg/)
- Backgrounds (solid + linear/radial gradients), borders, border-radius, box-shadow, opacity (alpha blending)
- TrueType font rasterization via ab_glyph, bold and italic synthesis
- SVG — paths, basic shapes (rect/circle/ellipse/line/polyline/polygon), gradients, stroke + fill
- Hand-written PNG encoder (no flate2 dependency)
Security (security/) — implemented, not yet wired into renderer
- Origin / SOP — check_same_origin, check_cors (with credentials/wildcard handling), check_navigation, registrable_domain (with 2-part TLD list)
- Multi-process / site isolation — Process kinds (Browser/Renderer/GPU/Utility/Plugin), ProcessPerSite/ProcessPerTab policies, crash recovery with max-restarts/sad-tab/fatal modes
- Seccomp-bpf sandbox — renderer allowlist (~30 syscalls), blocks execve/fork/ptrace/open/socket/connect/mount, PR_SET_NO_NEW_PRIVS, drop_capabilities
- CSP — directive map, default-src fallback, source expression matching ('self'/'none'/'unsafe-inline'/'unsafe-eval'/data:/blob:/host/*.wildcard/scheme:), nonce/hash support, allows_inline_script, allows_eval, allows_javascript_url, is_safe_attribute
- TLS certificates — Certificate struct, validity, hostname matching (with wildcards), TrustStore with Mozilla defaults, validate_chain (chain building, signature check, hostname, EKU, path length), OCSP stub, HPKP pinning, Certificate Transparency
- Permissions — 20 permission types, per-(origin, permission) state, pluggable prompt handler, iframe allow parsing
- Extensions — Manifest V3, content scripts, match patterns (/://.host/*), glob matching, permissions, ChromeApi enum
- DevTools protocol — JSON value type, Request/Response/Event/RpcError, Inspector/Page/Runtime/DOM/Network/Console methods, event subscribers, console message buffering
Networking (net/)
- HTTP/1.1 fetch (ureq), HTTP/2 parser, cookie jar with proper domain/path matching, redirect handling with loop detection, HTTP cache with conditional requests, WebSocket (frame parser, masking, ping/pong)
Web runtime (web_runtime/)
- fetch(), XMLHttpRequest (sync + async), event loop (task queues, microtasks, RAF), Promise, WebGL (headless stubs), video, MSE, EME, NDSD, HTTP/2
Interactive --window mode
- Click links, address bar (F6 to focus), reload (r), back/forward (Alt+←/→)
- Form inputs (text/email/password/checkbox/submit/button/textarea), Tab/Shift+Tab cycling, Enter to submit
- Scrolling — mouse wheel, arrow keys, Page Up/Down, Home/End, vim-style g/G
- Focus ring, blinking caret (500ms), hover hint in status bar
- Headless fallback to PNG output if no display available
For comparison
| Engine | LOC | Language |
|---|---|---|
| Chrome/Blink | ~30M | C++ |
| Firefox/Gecko | ~20M | C++/Rust |
| Safari/WebKit | ~15M | C++ |
| Servo | ~1M | Rust |
| Ladybird | ~500k | C++ |
| Falco | ~36k | Rust |
The whole Falco codebase fits in a weekend of reading.
Quick start
cargo build --release
./target/release/falco https://example.com --out example.png
./target/release/falco page.html --window
328 unit tests pass, 30 ignored (mostly platform-specific JIT tests that fail on macOS runners due to MAP_JIT quirks).
What's NOT done yet (being honest)
The spec-compliant HTML5/DOM/CSS modules (html5/, dom2/, css2/) are structurally complete and pass their own unit tests, but not yet wired into the render pipeline — the legacy parser is still used for actual rendering. That's the v0.2.0 milestone.
The security module (security/) implements all algorithms (SOP, CSP, sandbox, cert validation, permissions, DevTools) but they're not enforced in the renderer yet — same situation.
What I'd love feedback on
DOM model — I'm using
Rc<RefCell<Node>>with parent/child/sibling pointers, matching the spec. For production I'd probably use a slotmap arena, but Rc is easier to read and matches the spec's pointer model. Thoughts?-
Architecture — I have both legacy (
html.rs/dom.rs/css.rs) and spec-compliant (html5//dom2//css2/) modules. The spec-compliant ones are not yet wired in. Should I:- (a) Wire them in before any other feature work, or
- (b) Focus on CSS animations/transitions first, or
- (c) Focus on JS DOM mutation (innerHTML/style changes)?
JS VM — I wrote my own bytecode VM (
tjs) instead of usingboa_engineor binding V8/SpiderMonkey. Reasoning: I wanted full control over the GC, the bytecode format, and the DOM integration. The VM is intentionally limited (no real regex engine, no Proxy trap completeness, no real async functions). Is this a reasonable tradeoff for a teaching engine?Sandbox — I implemented seccomp-bpf filters but they're Linux-only and not yet enforced. For cross-platform sandboxing, is the Windows Job Object approach + macOS sandbox-exec reasonable, or should I look at platform-agnostic alternatives?
Architecture overview
| Module | Lines | Description |
|---|---|---|
html5/ |
~4,900 | WHATWG HTML5 tokenizer + tree builder + serializer + XML + encoding |
html.rs |
~540 | Legacy HTML parser (still used in render pipeline) |
dom2/ |
~2,280 | Spec-compliant DOM, MutationObserver, Shadow DOM, custom elements, a11y |
dom.rs |
~140 | Legacy DOM (still used in render pipeline) |
css2/ |
~2,020 | Selectors L4, cascade specificity, @-rules, animations, containment |
css/ |
~1,660 | Legacy CSS parser + selector matching + color parsing |
style/ |
~1,720 | Style cascade + UA styles + inheritance + flex/grid properties |
layout/ |
~1,950 | Block / inline / flex / grid / table / float / absolute layout |
paint/ |
~470 | Canvas + font rasterizer + alpha compositing + gradients + shadows |
svg/ |
~1,130 | SVG parser + renderer (paths, shapes, gradients) |
tjs/ |
~4,340 | Custom JS VM: lexer, parser, interpreter, bytecode VM, JIT, value, builtins |
tjs_ext/ |
~780 | Symbol, BigInt, Promise, microtasks, Map/Set, WeakMap/WeakSet, Reflect |
web_runtime/ |
~4,300 | fetch, XHR, event loop, Promise, WebGL, video, MSE, EME, NDSD, HTTP/2 |
net/ |
~930 | HTTP fetch, cookies, cache, websocket, redirect |
security/ |
~3,590 | SOP, multi-process, sandbox, CSP, certs, permissions, extensions, DevTools |
window/ |
~1,110 | Interactive window: scrolling, forms, navigation, history, address bar |
image/ |
~200 | Image loader (HTTP, data: URLs, local files) + cache + scaling |
png/ |
~100 | Hand-written PNG encoder (no flate2 dependency) |
| Total | ~36,000 |
Links
- GitHub: https://github.com/poxk/Falco
- Releases: https://github.com/poxk/Falco/releases
Thanks for any feedback! I'm especially interested in architectural critique from anyone who has worked on Servo, Ladybird, or other browser engines.
Top comments (0)