feat: AGENTS.md and architecture guides for agents!

This commit is contained in:
Jonathan Kelley committed 2025-12-29 18:09:05 -05:00
1 parent 2ee8ecd477
commit 638c02e15a
12 files changed
+3324

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
# Dioxus Architecture Overview
This directory contains comprehensive architecture documentation for the Dioxus framework, designed to help future Claude agents (and human contributors) understand the codebase deeply.
## Document Index
| File | Description |
|------|-------------|
| `01-CORE.md` | VirtualDOM, rendering pipeline, component system, diffing algorithm |
| `02-CLI.md` | Build system, dev server, bundling, terminal UI, platform support |
| `03-RSX.md` | RSX macro parsing, AST types, code generation, autofmt |
| `04-SIGNALS.md` | Signals, generational-box, hooks, stores, state management |
| `05-FULLSTACK.md` | Server functions, SSR, hydration, client-server communication |
| `06-RENDERERS.md` | Web, desktop, native, liveview renderer implementations |
| `07-HOTRELOAD.md` | Subsecond hot-patching, RSX hot-reload, devtools protocol |
| `08-ASSETS.md` | Manganis asset system, const serialization, build-time processing |
| `09-ROUTER.md` | Routing system, Routable trait, navigation, history |
| `10-WASM-SPLIT.md` | WASM code splitting, lazy loading, chunk management |
## Crate Dependency Overview
```
dioxus (umbrella crate)
├── dioxus-core (VirtualDOM, runtime, scheduler)
│ └── generational-box (Copy semantics for references)
├── dioxus-rsx (macro parsing)
│ └── dioxus-autofmt (code formatting)
├── dioxus-signals (reactive state)
│ ├── generational-box
│ └── dioxus-stores (nested reactivity)
├── dioxus-hooks (use_signal, use_effect, etc.)
├── dioxus-html (HTML elements, events)
├── dioxus-router (client-side routing)
│
├── Renderers:
│ ├── dioxus-web (WASM/browser)
│ ├── dioxus-desktop (wry/tao webview)
│ ├── dioxus-native (blitz/vello GPU)
│ ├── dioxus-liveview (WebSocket streaming)
│ └── dioxus-ssr (server-side rendering)
│
├── Fullstack:
│ ├── dioxus-fullstack (client/server coordination)
│ ├── dioxus-fullstack-core (transport, types)
│ ├── dioxus-fullstack-macro (#[server] macro)
│ └── dioxus-server (Axum integration)
│
├── CLI (dx command):
│ ├── dioxus-cli (main binary)
│ ├── dioxus-cli-config (Dioxus.toml parsing)
│ └── dioxus-cli-opt (optimization passes)
│
├── Hot Reload:
│ ├── subsecond (hot-patching runtime)
│ ├── dioxus-rsx-hotreload (template diffing)
│ └── dioxus-devtools (WebSocket communication)
│
├── Assets:
│ ├── manganis (asset!() macro)
│ ├── manganis-core (asset types)
│ ├── manganis-macro (proc-macro)
│ └── const-serialize (compile-time CBOR)
│
└── Code Splitting:
├── wasm-splitter (CLI tool)
├── wasm-split-macro (#[wasm_split])
└── wasm-split-cli (binary processing)
```
## Key Architectural Patterns
### 1. Copy Semantics for Reactive State
Dioxus uses `generational-box` to provide `Copy` semantics for references. This enables signals and other reactive primitives to be freely passed around without explicit cloning.
### 2. WriteMutations Trait
All renderers implement `WriteMutations` which defines how VirtualDOM changes translate to real DOM operations. This abstraction enables the same component code to run on web, desktop, mobile, and server.
### 3. Template-Based Rendering
RSX macros generate static `Template` definitions at compile time. Only dynamic parts are diffed at runtime, making updates extremely efficient.
### 4. Hot-Reload Without Full Rebuild
Two complementary systems:
- **RSX Hot-Reload**: Changes to template literals are diffed and sent via WebSocket
- **Subsecond Hot-Patching**: Full Rust function replacement via jump table indirection
### 5. Compile-Time Asset Processing
The `asset!()` macro embeds asset metadata in binary link sections. The CLI extracts this at build time, processes assets, and patches the binary with final URLs.
### 6. Server Functions as RPC
The `#[server]` macro generates dual code paths - client serialization and server handler - enabling seamless RPC-style communication.
## Important Design Decisions
1. **Single VirtualDOM**: Even multi-window desktop apps share one VirtualDOM for simpler state management
2. **Scope-Based Cleanup**: All state (signals, tasks, contexts) is tied to component scope lifetime
3. **Platform Agnostic Events**: Events are converted through `HtmlEventConverter` trait for cross-platform support
4. **Binary Protocols**: Desktop and liveview use Sledgehammer binary format for compact mutation encoding
5. **Generational GC**: References are invalidated by generation counters, preventing use-after-free without runtime overhead per access
## Future Development Areas
These docs are designed to help with planned features:
- **Custom Bundlers**: Replacing tauri-bundler with native implementation
- **CLI Tunnels**: Remote device hot-reloading via SSH/SCP
- **Workspace Hot-Patching**: Patching library crates, not just the tip crate
- **Universal Android Builds**: Single APK for all architectures
- **Fullstack Extensions**: First-party database, auth, IAAC, secrets
+311
View File
@@ -0,0 +1,311 @@
# Dioxus Core - VirtualDOM Architecture
The `dioxus-core` crate is the heart of Dioxus, implementing the virtual DOM, component lifecycle, rendering pipeline, and reactive runtime.
## VirtualDom Structure
The `VirtualDom` struct orchestrates the entire reactive component tree:
```
VirtualDom
├── scopes: Slab<ScopeState> // Arena allocator for all component scopes
├── dirty_scopes: BTreeSet<ScopeOrder> // Scopes marked for re-render (height-ordered)
├── runtime: Rc<Runtime> // Shared async runtime
├── resolved_scopes: Vec<ScopeId> // Scopes resolved during suspense
└── rx: UnboundedReceiver<SchedulerMsg> // Event channel from scheduler
```
### Key Methods
- `VirtualDom::new(app)` - Create new DOM with root component
- `rebuild()` / `rebuild_in_place()` - Full tree reconstruction
- `render_immediate()` - Render all dirty scopes without suspense blocking
- `wait_for_work()` - Async poll for futures and scheduler events
- `wait_for_suspense()` - Block until all suspended futures complete
- `mark_dirty(scope_id)` - Schedule scope for re-render
- `with_root_context<T>()` - Inject dependency into root scope
## Rendering Pipeline
### Initial Render Flow
1. `rebuild()` calls `run_scope(ScopeId::ROOT)`
2. Result wraps in `LastRenderedNode` and calls `create_scope()`
3. `create_scope()` recursively creates all child scopes and DOM elements
4. Mutations written to `WriteMutations` sink
### Update Render Flow
1. `wait_for_work()` polls scheduler and pending futures
2. `process_events()` consumes `SchedulerMsg::Immediate(scope_id)` messages
3. `render_immediate()` pops dirty scopes in height order (top-to-bottom)
4. For each scope: `run_and_diff_scope()` executes component, then `diff_scope()`
5. Diff generates mutations via `WriteMutations` trait
## Component System
### Component Definition
- `Component<P>` type alias: `fn(P) -> Element`
- `ComponentFunction<P, M>` trait for component functions
- `fn_ptr(&self) -> usize` - Raw function pointer for identity
- `rebuild(&self, props: P) -> Element` - Execute component
### Props System
- `Properties` trait for all props:
- `type Builder` - For DSL prop construction
- `builder()` - Create props builder
- `memoize(&mut self, other: &Self) -> bool` - Check if props changed
- `into_vcomponent()` - Convert to VComponent
### Type Erasure
`VProps<F,P,M>` wraps strongly-typed props, erased to `BoxedAnyProps` via `AnyProps` trait:
- `render(&self) -> Element`
- `memoize(&mut self, other: &dyn Any) -> bool`
- `props()` / `props_mut()` - Access as `dyn Any`
- `duplicate()` - Clone into new box
## Scope System
### ScopeId
Small unique identifier (usize index into Slab):
- `ScopeId::ROOT` (0) - Root wrapper scope
- `ScopeId::ROOT_SUSPENSE_BOUNDARY` (1) - Default suspense
- `ScopeId::ROOT_ERROR_BOUNDARY` (2) - Default error boundary
- `ScopeId::APP` (3) - User's root component
### ScopeState (Public API)
```
ScopeState
├── context_id: ScopeId
├── last_rendered_node: Option<LastRenderedNode>
├── props: BoxedAnyProps
└── reactive_context: ReactiveContext
```
### Scope (Internal State)
```
Scope
├── name: &'static str // Component name for debugging
├── id: ScopeId
├── parent_id: Option<ScopeId>
├── height: u32 // Distance from root
├── hooks: RefCell<Vec<Box<dyn Any>>> // Hook state storage
├── hook_index: Cell<usize> // Current hook being accessed
├── shared_contexts: RefCell<Vec<Box<dyn Any>>>
├── spawned_tasks: RefCell<FxHashSet<Task>>
├── before_render / after_render // Callbacks
├── status: RefCell<ScopeStatus> // Mounted/Unmounted
└── suspense_boundary: SuspenseLocation
```
### Context Propagation
- `provide_context<T: Clone + 'static>(context: T)` - Store in scope
- `consume_context<T>()` - Retrieve from this scope or any parent
- Lookup walks parent chain via `parent_id`
## Event System
### Event Structure
```rust
pub struct Event<T: ?Sized> {
pub data: Rc<T>,
pub(crate) metadata: Rc<RefCell<EventMetadata>>,
}
pub struct EventMetadata {
pub propagates: bool,
pub prevent_default: bool,
}
```
### Event Flow
1. `Runtime::handle_event()` looks up element by `ElementId`
2. Gets parent `ElementRef` from slab
3. If bubbles: `handle_bubbling_event()` walks parent chain
4. Collects listeners in path order, calls in reverse (parents first)
5. Breaks on `stop_propagation()`
## Diffing Algorithm
### Entry Point
`diff_scope()` gets old/new VNodes and calls `old.diff_node(new, dom, mutations)`
### VNode Diffing
- **Template Check**: Different templates → replace entire subtree
- **Identity Check**: Same pointer → skip
- **Attribute Diffing**: `diff_attributes()` for dynamic attrs
- **Dynamic Node Diffing**: Recursively diff each dynamic node
### Dynamic Node Cases
- Text → Text: `diff_vtext()` updates content
- Placeholder → Placeholder: No-op
- Fragment → Fragment: Recursively diff children
- Component → Component: Check render fn, then memoize props
- Type Change: Remove old, create new
### Keyed List Diffing
1. **Prefix Pass**: Match keys from start
2. **Suffix Pass**: Match keys from end
3. **Middle Pass**: Handle insertions/deletions/moves
4. Uses `FxHashMap` for old→new key matching
## Mutations System
### WriteMutations Trait
Interface between VirtualDOM and real DOM:
- `append_children(id, count)` - Add N nodes to element
- `assign_node_id(path, id)` - Mark element at template path
- `create_placeholder(id)` - Create marker node
- `create_text_node(value, id)` - Create text node
- `load_template(template, index, id)` - Clone from template cache
- `replace_node_with(id, count)` - Replace element
- `set_attribute(name, ns, value, id)` - Update attribute
- `create_event_listener(name, id)` - Register listener
- `remove_node(id)` - Delete element
### Mutation Enum (for testing/serialization)
```rust
pub enum Mutation {
AppendChildren { id, m },
CreatePlaceholder { id },
CreateTextNode { value, id },
LoadTemplate { index, id },
ReplaceWith { id, m },
SetAttribute { name, ns, value, id },
// ... etc
}
```
## Scheduler
### Work Priority
1. **Dirty Scopes** (Highest): Re-render in height order
2. **Tasks**: Poll spawned futures
3. **Effects** (Lowest): Run after DOM changes
### ScopeOrder
Composite key: `(height: u32, id: ScopeId)` in `BTreeSet` ensures parent scopes run before children.
### Key Methods
- `queue_scope(order)` - Add to dirty_scopes
- `queue_task(task, order)` - Add to dirty_tasks
- `pop_work()` - Get next dirty scope or task
- `pop_effect()` - Get next pending effect
## Runtime
Manages async/scope/task coordination:
```
Runtime
├── scope_states: RefCell<Vec<Option<Scope>>>
├── scope_stack: RefCell<Vec<ScopeId>>
├── suspense_stack: RefCell<Vec<SuspenseLocation>>
├── tasks: RefCell<SlotMap<DefaultKey, Rc<LocalTask>>>
├── current_task: Cell<Option<Task>>
├── dirty_tasks: RefCell<BTreeSet<DirtyTasks>>
├── pending_effects: RefCell<BTreeSet<Effect>>
├── rendering: Cell<bool>
├── sender: UnboundedSender<SchedulerMsg>
├── elements: RefCell<Slab<Option<ElementRef>>>
└── mounts: RefCell<Slab<VNodeMount>>
```
## Tasks & Async
### Task Structure
- `id: TaskId` (slotmap key)
- `!Send + !Sync` marker
### Task Methods
- `Task::new(future)` - Spawn new task
- `cancel()` - Remove task
- `pause()` / `resume()` - Control polling
- `wake()` - Wake sleeping task
### LocalTask (Internal)
- Wraps `Pin<Box<dyn Future<Output = ()>>>`
- Custom waker that sends `SchedulerMsg::TaskNotified`
- Task dropped when owning scope drops
## Effects
Effects run AFTER mutations are applied to DOM:
1. Created via `use_effect()` in component
2. Queued in `Runtime::pending_effects`
3. After render, `finish_render()` sends `SchedulerMsg::EffectQueued`
4. `poll_tasks()` pops effects and calls `effect.run()`
## Suspense
### SuspenseContext
- Tracks suspended futures and placeholder nodes
- `suspended_tasks: RefCell<Vec<SuspendedFuture>>`
- `suspended_nodes: RefCell<Option<VNode>>`
- `frozen: Cell<bool>` for server-side locking
### Suspension Flow
1. Component calls `suspend()` → `Err(SuspendedFuture)`
2. `Element::Err(RenderError::Suspended)` propagates up
3. Nearest `SuspenseBoundary` catches it
4. Boundary renders placeholder
5. Future added to boundary's tasks
6. Boundary marked dirty when future completes
## VNodes & Templates
### VNode Structure
```rust
pub struct VNode {
vnode: Rc<VNodeInner>,
mount: Cell<MountId>,
}
pub struct VNodeInner {
pub key: Option<String>,
pub template: Template,
pub dynamic_nodes: Box<[DynamicNode]>,
pub dynamic_attrs: Box<[Box<[Attribute]>]>,
}
```
### Template (Static)
```rust
pub struct Template {
pub roots: &'static [TemplateNode],
pub node_paths: &'static [&'static [u8]],
pub attr_paths: &'static [&'static [u8]],
}
```
### TemplateNode Variants
- `Element { tag, namespace, attrs, children }` - Static element
- `Text { text }` - Static text
- `Dynamic { id }` - Index into dynamic_nodes
### DynamicNode Variants
- `Component(VComponent)` - Child component
- `Text(VText)` - Text node
- `Placeholder(VPlaceholder)` - Suspense/empty marker
- `Fragment(Vec<VNode>)` - Multiple children
## Error Boundaries
### ErrorContext
- `error: Rc<RefCell<Option<CapturedError>>>`
- `subscribers: Subscribers` for listening components
### Error Flow
1. Component returns `Element::Err(error)`
2. Nearest `ErrorBoundary` catches
3. `ErrorContext` stores error
4. Boundary re-renders with `error_context.error()` available
## Root Wrapper
Default scope hierarchy:
```
ScopeId(0): RootScopeWrapper
└── ScopeId(1): SuspenseBoundary
└── ScopeId(2): ErrorBoundary
└── ScopeId(3): User's Root App
```
Provides default suspense and error handling for the entire tree.
+264
View File
@@ -0,0 +1,264 @@
# Dioxus CLI Architecture
The `dx` CLI is a comprehensive build tool for Dioxus applications, supporting development, building, bundling, and cross-platform deployment.
## Command Structure
Entry point: `main.rs` with async tokio runtime and `TraceController` for error handling.
### Commands Enum
- `Serve` - Watch and serve with hot-reloading
- `Build` - Full production build
- `Bundle` - Package for distribution
- `Run` - Run without hot-reload
- `Create` / `Init` - Project scaffolding
- `Check` - Lint/validate RSX
- `Translate` - Convert HTML/JSX to RSX
- `Autoformat` - Format RSX code
- `Config` - Manage Dioxus.toml
- `Doctor` - Diagnose environment
- `Components` - Component registry
- `SelfUpdate` - Update CLI
- `Tools` - Internal tools (build-assets, hotpatch)
## Platform Support
### Platform Enum
- `Web` - wasm32-unknown-unknown
- `MacOS` / `Windows` / `Linux` - Desktop webview
- `Ios` / `Android` - Mobile webview
- `Server` - SSR/fullstack server
- `Liveview` - WebSocket streaming
- `Unknown` - Auto-select
### CommandWithPlatformOverrides
Allows separate client/server args for fullstack:
```bash
dx serve @client --features web @server --features server
```
Decomposes into client BuildArgs, server BuildArgs, and shared args.
## Build System
### BuildRequest
The "plan" for a single build:
```
BuildRequest
├── workspace: Workspace metadata
├── config: DioxusConfig
├── package: Package details
├── features: Vec<String>
├── target: Triple
├── bundle: BundleFormat
├── profile: Profile
├── rustflags: Vec<String>
└── custom_linker: Option<PathBuf>
```
Responsible for:
- Writing build artifacts
- Processing assets
- Writing platform-specific metadata (Info.plist, AndroidManifest.xml)
- Platform-specific linking (Android uses dx as opaque linker, hotpatching uses custom linker)
### BuildMode Enum
- `Base { run: bool }` - Normal cargo rustc build
- `Fat` - Full build with all symbols for hot-patching
- `Thin { rustc_args, changed_files, aslr_reference, cache }` - Fast rebuild
### BuildContext
Runtime configuration with progress channel:
- `ProgressTx` for status updates
- `BuildId` enum: PRIMARY (client), SECONDARY (server)
- Helper methods: `compiling()`, `linking()`, `bindgen()`, etc.
### AppBuilder
State machine managing ongoing builds:
- Build task handle and artifacts
- App child process (stdout/stderr)
- Hot-patching state (patches, module cache, ASLR offset)
- Build metadata: stage, compiled crates, timing
## Dev Server (Serve)
### AppServer (runner.rs)
Primary orchestration:
```
AppServer
├── workspace: Workspace metadata
├── client / server: AppBuilder instances
├── watcher: File system watcher
├── file_map: Cached RSX files for hot-reload diffing
├── devserver: WebServer
└── flags: use_hotpatch_engine, automatic_rebuilds, hot_reload, etc.
```
### WebServer (server.rs)
HTTP + WebSocket server providing:
- **Hot-reload socket** - RSX and asset updates
- **Build-status socket** - Compilation progress
- **HTTP server** - Static assets (public folder, WASM)
- **Proxy support** - Fullstack backend
- **DevTools** - Connected clients with build_id, ASLR reference, PID
### Output (output.rs)
Terminal UI with ratatui:
- Build progress with crate-by-crate counts
- Asset copying progress
- Colored output, throbber animations
- Keyboard input (r=rebuild, p=pause, v=verbose)
- 100ms tick-based rendering
### File Change Handling
```
File modified → detect type
├── Rust file → diff RSX using dioxus_rsx_hotreload::diff_rsx()
│ ├── Extract changed templates
│ ├── Build HotReloadMsg
│ └── Send via WebSocket
├── Asset file → copy to bundle, signal reload
└── If can't hot-reload → queue full rebuild
```
### serve_all() Main Loop
Uses `tokio::select!` to multiplex:
- Builder updates (compilation, process output)
- WebServer events (connections, messages)
- Output (TUI updates, input)
- TraceController (panic handling)
- File watcher (filesystem changes)
## Asset Processing
### Pipeline (build/assets.rs)
1. **Manganis Integration** - Extract `__ASSETS__` symbols from binary
2. **Asset Hashing** - Compute hashes for cache-busting
3. **Two Format Support** - Legacy (0.7.0-0.7.1) and New (0.7.2+) serialization
4. **Platform Paths** - Assets relative to executable; Android uses "asset root"
### Processing by Type
- **Images**: Resize, convert format (Avif, WebP), optimize
- **CSS/JS**: Minify, hash-suffix naming
- **Folders**: Recursive copy
- **Unknown**: Direct copy with optional hash
## Bundling
### Bundle System
Platform-specific packaging:
- **Web/Server**: No special bundling, outputs to web/ directory
- **Desktop**: Uses tauri-bundler (dmg, msi, appimage)
- **iOS**: App bundle format with codesigning warnings
- **Android**: Runs `gradle bundleRelease` for AAB
### BundleConfig
```
BundleConfig
├── identifier: String
├── publisher: String
├── icon: Vec<PathBuf>
├── resources: Vec<PathBuf>
├── deb / macos / windows / android: Platform-specific
```
## Hot-Patching
### HotpatchModuleCache
Caches compiled dependency symbols:
- Symbol table with function/data info
- Pre-computed for Fat builds
- Reused in Thin builds for fast linking
### Patch Creation Flow
1. Compile app with Fat mode (all symbols)
2. On change: compile Thin (small, fast)
3. Analyze binary diff → create JumpTable
4. Apply patches to running executable
5. If patch fails → fall back to full rebuild
### Platform Implementations
- `create_windows_jump_table()` - x86/x64 jump stubs
- `create_native_jump_table()` - macOS/Linux function overrides
- `create_wasm_jump_table()` - WASM indirect call table updates
## Configuration
### DioxusConfig (Dioxus.toml)
```toml
[application]
asset_dir = "assets"
out_dir = "dist"
tailwind_input/output = "..."
ios_info_plist = "Info.plist"
android_manifest = "AndroidManifest.xml"
[web]
app.title = "My App"
https.enabled = true
pre_compress = false
[bundle]
identifier = "com.example.app"
publisher = "Company"
```
### Resolution Order
1. Default values
2. Dioxus.toml in project root
3. CLI argument overrides
4. Environment variables
## Workspace
### Workspace Struct (Singleton)
```
Workspace
├── krates: Krates (cargo metadata, dependency graph)
├── settings: CliSettings (user settings ~/.config/dioxus/)
├── sysroot: Rust target sysroot
├── rustc_version: Version
├── wasm_opt: Optional path
├── cargo_toml: Parsed Cargo.toml
└── android_tools: Optional NDK/SDK
```
Lazy-loaded via `Workspace::current()` with tokio Mutex.
## Extension Points
### Adding New Bundlers
1. New variant in `BundleFormat` enum
2. New variant in `Renderer` enum
3. New config struct (e.g., `MyPlatformSettings`)
4. Implementation in `Bundle::bundle()`
5. Asset directory structure definition
6. Linking strategy if needed
### Adding New Commands
1. New variant in `Commands` enum
2. New command module in `cli/`
3. Register in `TraceController::main()` match
4. Optional `StructuredOutput` variant for results
### Customizing Build Hooks
- Pre/post-build scripts in Dioxus.toml
- Custom linker integration
- Asset transformation pipeline
## Key Patterns
### Channel-Based Progress
```
Build Task → BuilderUpdate Channel → AppBuilder → ServeUpdate → Output TUI
```
Decouples build process from UI for real-time updates.
### Lazy Workspace Loading
Single workspace load with tokio Mutex, shows spinner if cargo-metadata takes >1 second.
### Renderer/Bundle Inference
1. Explicit flags: `--web`, `--webview`, `--native`
2. Platform aliases: `--ios`, `--android`, `--desktop`
3. Feature detection from Cargo.toml
4. Default: Infer from target triple
+298
View File
@@ -0,0 +1,298 @@
# RSX and Autofmt Architecture
The `dioxus-rsx` crate parses JSX-like syntax into Rust code, while `dioxus-autofmt` provides formatting capabilities.
## RSX Parsing
### Entry Point: CallBody
Root struct for `rsx! {}` macro contents:
```
CallBody
├── TemplateBody (list of BodyNode roots)
├── template_idx: Cell<usize>
└── span: Option<Span>
```
`CallBody::new()` initializes template indices and cascades hotreload info through nested structures.
### BodyNode Enum
Six variants representing RSX content:
- `Element(Element)` - HTML elements (div, span)
- `Component(Component)` - User components
- `Text(TextNode)` - String literals with interpolation
- `RawExpr(ExprNode)` - Braced expressions `{expr}`
- `ForLoop(ForLoop)` - `for pat in expr { body }`
- `IfChain(IfChain)` - `if cond { } else { }`
### Parsing Priority
1. Peek for LitStr → TextNode
2. Peek for `for` → ForLoop
3. Peek for `if` → IfChain
4. Peek for `match` → RawExpr
5. Peek for Brace → RawExpr
6. Web components: Ident + `-` → Element
7. Lowercase ident → Element
8. Otherwise → Component (fallback)
## Key AST Types
### Element
```
Element
├── name: ElementName (Ident or Custom)
├── raw_attributes: Vec<Attribute>
├── merged_attributes: Vec<Attribute> // After combining duplicates
├── spreads: Vec<Spread>
├── children: Vec<BodyNode>
├── brace: Option<Brace>
└── diagnostics: Diagnostics
```
`merge_attributes()` collapses duplicate attribute names using `IfmtInput` with space delimiter.
### Attribute
```
Attribute
├── name: AttributeName (BuiltIn, Custom, or Spread)
├── value: AttributeValue
├── colon: Option<Token![:]>
├── dyn_idx: DynIdx
└── el_name: Option<ElementName>
```
### AttributeValue Variants
- `Shorthand(Ident)` - attribute without value
- `AttrLiteral(HotLiteral)` - hotreloadable literal
- `EventTokens(PartialClosure)` - event handlers
- `IfExpr(IfAttributeValue)` - conditional attributes
- `AttrExpr(PartialExpr)` - arbitrary expressions
### HotLiteral
```
HotLiteral
├── Fmted(HotReloadFormattedSegment) // "{expr}" interpolation
├── Float(LitFloat)
├── Int(LitInt)
└── Bool(LitBool)
```
### IfmtInput (Formatted Strings)
Parses string contents into segments:
- `Segment::Literal(String)` - plain text
- `Segment::Formatted(FormattedSegment)` - `{expr}` interpolation
Parsing rules:
- `{{` → literal `{`
- `}}` → literal `}`
- `{expr}` → formatted segment
- `{expr:format_args}` → formatted with format spec
### Component
```
Component
├── name: syn::Path
├── generics: Option<AngleBracketedGenericArguments>
├── fields: Vec<Attribute>
├── component_literal_dyn_idx: Vec<DynIdx>
├── spreads: Vec<Spread>
├── children: TemplateBody
├── dyn_idx: DynIdx
└── diagnostics: Diagnostics
```
### ForLoop
```
ForLoop
├── for_token, pat, in_token
├── expr: Box<Expr>
├── body: TemplateBody
└── dyn_idx: DynIdx
```
Generates: `(expr).into_iter().map(|pat| { body })`
### IfChain
```
IfChain
├── if_token, cond: Box<Expr>
├── then_branch: TemplateBody
├── else_if_branch: Option<Box<IfChain>>
├── else_branch: Option<TemplateBody>
└── dyn_idx: DynIdx
```
### DynIdx
`Cell<Option<usize>>` for tracking dynamic indices. Transparent in PartialEq/Eq/Hash. Used for hot-reload mapping.
## Code Generation
### TemplateBody → VNode::Template
Generated output structure:
1. `__TEMPLATE_ROOTS` - Static array of TemplateNode
2. **Dynamic nodes** - Components, interpolated text, loops, conditionals
3. **Dynamic attributes** - Non-static attribute values
4. **Dynamic literal pool** - In debug, vec of formatted values
5. **Dynamic value pool** - Maps literal indices to values
### Template Structure
```rust
dioxus_core::Element::Ok({
#[cfg(debug_assertions)]
fn __original_template() -> &'static HotReloadedTemplate { ... }
let __dynamic_nodes: [DynamicNode; N] = [ ... ];
let __dynamic_attributes: [Box<[Attribute]>; M] = [ ... ];
static __TEMPLATE_ROOTS: &[TemplateNode] = &[ ... ];
// Template reference and rendering
})
```
### Generated TemplateNode Types
- `Element { tag, namespace, attrs, children }` - Static element
- `Text { text }` - Static text
- `Dynamic { id }` - References dynamic node pool
## Template System
### TemplateBody Structure
```
TemplateBody
├── roots: Vec<BodyNode>
├── template_idx: DynIdx
├── node_paths: Vec<Vec<u8>> // Path to each dynamic node
├── attr_paths: Vec<(Vec<u8>, usize)> // Path and attribute index
├── dynamic_text_segments: Vec<FormattedSegment>
└── diagnostics: Diagnostics
```
### Template ID Assignment
- `CallBody::next_template_idx()` generates sequential IDs
- Each nested structure gets unique ID
- Combined with `file!()`, `line!()`, `column!()` for source location
### HotReloadFormattedSegment
Wraps `IfmtInput` with `dynamic_node_indexes: Vec<DynIdx>`:
- One DynIdx per `Segment::Formatted` entry
- Maps formatted segments to dynamic nodes during hot-reload
## Autofmt System
### Entry Points
- `try_fmt_file(contents, &syn::File, IndentOptions)` → Vec<FormattedBlock>
- `fmt_block(block_str, indent_level, IndentOptions)` → Option<String>
- `write_block_out(body)` → Option<String>
### FormattedBlock
```
FormattedBlock
├── formatted: String
├── start: usize (byte offset)
└── end: usize (byte offset)
```
### Writer State
```
Writer
├── raw_src: &str
├── src: Vec<&str> // Lines
├── out: Buffer
├── cached_formats: HashMap<LineColumn, String>
└── invalid_exprs: Vec<Span>
```
### Buffer
```
Buffer
├── buf: String
├── indent_level: usize
└── indent: IndentOptions
```
### Optimization Levels
1. **Empty**: `div {}` (no space inside)
2. **Oneliner**: `div { class: "x", child {} }` (single line)
3. **PropsOnTop**: Props multiline, children follow
4. **NoOpt**: Everything multiline
### Short-Circuit Optimization
```rust
if formatted.len() <= 80
&& !formatted.contains('\n')
&& !body_is_solo_expr
&& !formatted.trim().is_empty()
{
formatted = format!(" {formatted} "); // Collapse to single line
}
```
## Whitespace Handling
Whitespace is significant in RSX:
- Text nodes preserve exact whitespace
- Comments must be preserved
- Formatting must not change text node content
### Comment Preservation
- `write_comments()` accumulates full-line comments before spans
- `write_inline_comments()` preserves end-of-line comments
- Comments tracked via `LineColumn` from Span
## Expression Formatting
### write_partial_expr() Strategy
1. Use `prettier_please` to unparse expressions
2. Handle nested rsx! macros specially
3. `unparse_expr()` visits macro calls:
- Format nested rsx! blocks
- Replace macros with marker unicode
- Apply formatted blocks back
### Marker Replacement
Uses marker `"𝕣𝕤𝕩"` to replace macros during unparse, avoiding conflicts with actual code.
## Indentation System
### IndentOptions
```
IndentOptions
├── width: usize
├── indent_string: String ("\t" or spaces)
└── split_line_attributes: bool
```
### Functions
- `indent_str()` → returns indent string
- `count_indents(line)` → estimates indent level
- `line_length(line)` → estimates visible length
## Attribute Formatting
### write_attributes(props_same_line)
- `true`: attrs on one line with spaces
- `false`: each attr on new line, indented
### write_attribute_value()
- `Shorthand` → just ident
- `AttrLiteral` → uses Display impl
- `EventTokens` → write_partial_expr()
- `IfExpr` → write_attribute_if_chain()
- `AttrExpr` → write_partial_expr()
## Extension Points
### Adding New Node Types
1. Add variant to BodyNode enum
2. Implement Parse trait
3. Implement ToTokens for code gen
4. Add write_* method to Writer
### Adding New Attribute Types
1. Add variant to AttributeValue
2. Implement Parse for detection
3. Add merge handling if needed
4. Add write_attribute_value case
### Changing Formatting Heuristics
- Modify ShortOptimization logic
- Adjust threshold constants (80, 100 chars)
- Modify estimation functions
+328
View File
@@ -0,0 +1,328 @@
# Signals and State Management
Dioxus state management is built on a layered architecture: generational-box for memory, signals for reactivity, and stores for nested data.
## Generational Box
### Core Concept
Provides `Copy` semantics for references through generation-based validation:
```
GenerationalBox<T, S>
├── raw: GenerationalPointer<S>
│ ├── storage: &'static S
│ └── location: GenerationalLocation
│ ├── generation: NonZeroU64
│ └── created_at: &'static Location (debug)
└── _marker: PhantomData<T>
```
### Memory Management
**Allocation:**
```rust
pub trait Storage<Data> {
fn new(value: Data, caller: Location) -> GenerationalPointer<Self>;
fn new_rc(value: Data, caller: Location) -> GenerationalPointer<Self>;
}
```
**Storage Variants:**
- `UnsyncStorage` - Single-threaded, uses `RefCell` (fast, no locking)
- `SyncStorage` - Multi-threaded, uses `RwLock` (thread-safe)
**StorageEntry:**
```
StorageEntry<Data>
├── generation: NonZeroU64
├── refcount: u32
└── data: Data // Empty, Data, Rc, or Reference
```
### Generation Tracking
1. Caller provides pointer with generation X
2. Storage checks: `entry.generation == X`
3. If yes: data valid, proceed
4. If no: return `BorrowError::Dropped`
When dropped: generation incremented, data cleared, existing pointers invalidated.
### Owner Pattern
```rust
pub struct Owner<S> {
owned: Vec<GenerationalPointer<S>>,
}
impl<S> Drop for Owner<S> {
fn drop(&mut self) {
for location in self.owned.drain(..) {
location.recycle(); // Invalidates all pointers
}
}
}
```
## Signals
### Architecture
```
Signal<T, S>
└── inner: CopyValue<SignalData<T>, S>
└── SignalData<T>
├── value: T
└── subscribers: Arc<Mutex<HashSet<ReactiveContext>>>
```
### Signal Types
**Signal<T, S>** - Primary mutable reactive primitive:
- Implements `Readable` for subscribed reads
- Implements `Writable` for reactive updates
- `.read()` subscribes current scope
- `.peek()` reads without subscribing
**Memo<T>** - Derived reactive value:
- Wraps `Signal<T>` for value storage
- Contains `UpdateInformation` tracking dirty state
- Lazy evaluation: only recomputes when dependencies change
- Uses `PartialEq` to skip updates if unchanged
**CopyValue<T, S>** - Generic mutable wrapper:
- Lower-level than Signal
- No built-in reactivity
- Building block for Signal
**Global<T, R>** - Lazy singleton:
- Created once per app on first access
- Stored in `ScopeId::ROOT` context
- Uses `InitializeFromFunction` trait
**ReadSignal<T>** / **WriteSignal<T>** - Type-erased boxed signals:
- Store `Box<dyn DynReadable>`
- Flexible APIs accepting any readable type
**MappedSignal<O, V, F>** - Derived readonly:
- Maps inner signal through function F
- `signal.map(|x| &x.field)`
### Reactivity System
**Automatic Subscription:**
```
signal.read()
→ try_read_unchecked()
→ Check ReactiveContext::current()
→ If exists: reactive_context.subscribe(signal.subscribers)
→ Later writes call mark_dirty() on all subscribers
```
**Update Propagation:**
```
signal.write()
→ WriteLock created
→ User modifies value
→ WriteLock dropped → SignalSubscriberDrop::drop()
→ signal.update_subscribers():
→ Get subscriber snapshot (brief lock)
→ Call mark_dirty() on each
→ Re-extend subscriber list
```
### Global Signals
**GlobalLazyContext:**
```
GlobalLazyContext
└── map: Rc<RefCell<HashMap<GlobalKey, Box<dyn Any>>>>
```
**Resolution:**
1. Static const defines `GlobalSignal<T>`
2. First access calls `resolve()`:
- Check HashMap by key
- If found: return clone
- If not: run constructor in ROOT scope
- Store result
3. Subsequent accesses return same instance
**Key Types:**
- `GlobalKey::File { file, line, column, index }`
- `GlobalKey::Raw(&'static str)`
## Hooks
### Core Hook Pattern
```rust
#[track_caller]
pub fn use_hook<T>(f: impl FnOnce() -> T) -> T {
let component_id = current_scope_id();
let mut hooks = get_hooks(component_id);
if hooks.len() <= hook_index {
hooks.push(Box::new(f())); // First render
}
hooks[hook_index].clone() // Return existing
}
```
### Hook Types
**use_signal<T>() -> Signal<T>**
- Creates local signal owned by component
- State persists across renders
**use_memo<R>() -> Memo<R>**
- Creates memoized computation
- Reruns when read signals change
**use_effect(callback)**
- Runs side effects when dependencies change
- Creates `ReactiveContext` to track reads
- Queues effect rerun on dependency notification
**use_resource<T, F>(future_fn) -> Resource<T>**
- Async state management
- Watches dependencies, reruns future when they change
- Returns `Resource<T>` with value, state, task
**use_callback<I, O>() -> Callback<I, O>**
- Memoized callback
- Prevents unnecessary closures
**use_coroutine(init)**
- Long-lived task
- Spawned once at component creation
**use_context<T>() -> T**
- Retrieves ancestor context
### Hook Rules
1. Must call same hooks every render
2. Must call in same order
3. Order determines hook identity
4. Breaking rules causes panic or bugs
## Stores
### Purpose
Signals work for scalar state. Stores provide:
- Granular reactivity per field
- Lazy signal creation
- Ergonomic field access
### Store Architecture
```
Store<T, Lens>
└── selector: SelectorScope<Lens>
├── subscriptions: StoreSubscriptions
├── path: TinyVec<u16>
└── value: Lens
```
### Subscription Tree
```
StoreSubscriptions
└── inner: CopyValue<StoreSubscriptionsInner>
└── root: SelectorNode
├── subscribers: HashSet<ReactiveContext>
└── root: HashMap<PathKey, SelectorNode>
└── [0] → SelectorNode
└── [1] → SelectorNode
```
### Path Tracking
```rust
let store = Store::new(vec![a, b, c]);
let item_1 = store[1]; // path = [1]
let field = item_1.name; // path = [1, field_hash]
// Writing store[1].name only marks dirty:
// - subscribers at path [1]
// - subscribers at path [1, field_hash]
// - NOT path [0] or [2]
```
### Store Macro
```rust
#[derive(Store)]
struct TodoItem {
checked: bool,
contents: String,
}
// Generates:
pub trait TodoItemStoreExt<__Lens> {
fn checked(self) -> Store<bool, __Lens::MappedSignal>;
fn contents(self) -> Store<String, __Lens::MappedSignal>;
fn transpose(self) -> TodoItemStoreTransposed;
}
pub struct TodoItemStoreTransposed {
pub checked: Store<bool>,
pub contents: Store<String>,
}
```
### Enum Support
```rust
#[derive(Store)]
enum Status {
Loading,
Ready(String),
Error(String),
}
// Generates:
fn is_loading(self) -> bool;
fn ready(self) -> Option<Store<String, ...>>;
fn transpose(self) -> StatusStoreTransposed;
```
## Key Traits
### Readable
```rust
pub trait Readable {
type Target;
type Storage;
fn try_read_unchecked(&self) -> Result<ReadableRef<T>>;
fn try_peek_unchecked(&self) -> Result<ReadableRef<T>>;
fn subscribers(&self) -> Subscribers;
}
```
### Writable
```rust
pub trait Writable: Readable {
type WriteMetadata;
fn try_write_unchecked(&self) -> Result<WritableRef<T>>;
}
```
### Storage (AnyStorage)
```rust
pub trait AnyStorage {
type Ref<'a, T>: Deref<Target = T>;
type Mut<'a, T>: DerefMut<Target = T>;
fn map<T, U>(ref_: Ref<T>) -> Ref<U>;
fn map_mut<T, U>(mut_ref: Mut<T>) -> Mut<U>;
}
```
## Comparison
| Feature | Signal | Memo | Store |
|---------|--------|------|-------|
| Mutability | Mutable | Read-only | Mutable |
| Dependencies | None | Tracks reads | Implicit via paths |
| Granularity | Single value | Computation | Per-field |
| When to use | Direct state | Derived values | Nested structures |
| Subscriptions | Simple HashSet | Via context | Tree structure |
| Performance | O(1) update | O(deps) recompute | O(path) update |
## Memory Model
1. **Heap**: Values in storage singletons per-scope
2. **Stack**: Only pointers (GenerationalBox) in components
3. **Cleanup**: Owner drops when scope dies
4. **Validity**: Generation checking on each access
5. **Sharing**: Arc+Mutex for subscribers, Rc+RefCell for ownership
+297
View File
@@ -0,0 +1,297 @@
# Dioxus Fullstack Architecture
The fullstack ecosystem enables seamless client-server communication through server functions, SSR, and hydration.
## Server Functions (#[server])
### Macro Generation
The `#[server]` macro generates dual code paths:
**On Client:**
```rust
// Creates request encoding/decoding
let client = ClientRequest::new(Method::POST, endpoint, &query);
let response = ServerFnEncoder::fetch_client(client, args, unpack).await;
let result = ServerFnDecoder::decode_client_response(response).await;
```
**On Server:**
```rust
// Creates Axum handler
fn __inner__function__(state, request) -> Response {
// Extract arguments from request
// Call actual function
// Serialize response
}
// Register globally
inventory::submit! {
ServerFunction::new(Method::POST, path, handler)
}
```
### Configuration Options
- `endpoint` - Custom URL path prefix (default: `/api`)
- `input` - Request encoding (Json, Cbor, MessagePack)
- `output` - Response encoding
- `middleware` - Tower middleware layers
- Server-only extractors: `headers: HeaderMap`, `cookies: Cookies`
### Wire Protocol
**Request:**
- Method: POST
- URL: `/api/{function_name}?{query_params}`
- Content-Type: application/json
- Body: JSON-serialized struct with arguments
**Response:**
```rust
enum RestEndpointPayload<T, E> {
Success(T),
Error(ErrorPayload<E>),
}
struct ErrorPayload<E> {
message: String,
code: u16,
data: Option<E>,
}
```
### Handler Registration
1. `ServerFunction` struct holds metadata
2. `inventory::submit!` for compile-time registration
3. `ServerFunction::collect()` returns all handlers
4. `DioxusRouterExt::register_server_functions()` registers on Axum
## SSR (Server-Side Rendering)
### SsrRendererPool
- Thread-safe pool of `dioxus_ssr::Renderer` instances
- Manages concurrent rendering
- Optional incremental cache for static generation
### Rendering Pipeline
1. Create VirtualDom with component function
2. Render to HTML string
3. Inject hydration data and scripts
4. Stream chunks via futures channel
### Streaming Modes
**Disabled (default):**
- All server futures resolved before sending
- Simple, SEO-friendly
**OutOfOrder:**
- Initial chunk sent with placeholders
- Suspense boundaries stream independently
- Uses `StreamingRenderer`
**Streaming Chunk Structure:**
```html
<!-- Initial -->
<div>Header</div>
<div>Loading...</div>
<!-- Later -->
<script>window.dx_hydrate(mount_id, "data");</script>
<div hidden id="ds-1-r">Resolved content</div>
```
## Hydration
### Server-Side
1. `HydrationContext` created at render start
2. Hooks serialize data: `use_server_future`, `use_server_cached`, `use_loader`
3. Data stored with unique IDs
4. Serialized to base64-encoded CBOR
5. Injected as `window.__DIOXUS_HYDRATION_DATA__`
### Client-Side
1. JavaScript reads hydration data from HTML
2. Runtime provides `HydrationContext` to components
3. Components call same hooks, check for cached data
4. Data deserialized and populated
5. Event listeners attached without re-rendering
### Key Types
- `SerializeContextEntry<T>` - Entry in hydration context
- `SerializedHydrationData` - Base64-encoded CBOR
- `TakeDataError` - Why data couldn't be retrieved
## Server Architecture
### FullstackState
- Wraps `FullstackContext` and runtime handle
- Passed as State to Axum handlers
- Allows request context access
### DioxusRouterExt Trait
Extension methods on `axum::Router`:
- `serve_static_assets()` - Serves `/public`
- `serve_dioxus_application(cfg, component)` - Full SSR + server functions
- `register_server_functions()` - Registers collected handlers
- `serve_api_application(cfg, component)` - API-only
### Handler Wrapping
```
Handler wrapped in:
1. FullstackContext::scope() - provides context
2. LocalPool::spawn_pinned() - enables !Send futures
3. Middleware layers
4. Response header injection
```
### FullstackContext
Task-local context providing:
- Request headers via RwLock
- Streaming status (RenderingInitialChunk, Committed, Complete)
- Response headers
- HTTP status codes
- Reactive subscriptions
### ServeConfig
- `index` - Custom IndexHtml
- `incremental` - ISR cache
- `context_providers` - Injectable contexts
- `streaming_mode` - Disabled or OutOfOrder
## Client-Server Communication
### ClientRequest
```
ClientRequest
├── url: String
├── method: Method
├── headers: HeaderMap
└── extensions: Extensions
```
### Encoding Traits
- `EncodeRequest<In, Out, R>` - Serializes and makes request
- `IntoRequest<R>` - Converts to request
- `Encoding` trait - Defines format (Json, Cbor)
### Suspense Integration
- Server functions automatically suspend
- `use_server_future` - Waits on server, caches for client
- `use_loader` - Enhanced error handling
- `use_server_cached` - Caches computed values
### Error Handling
- Server errors serialized with message, code, details
- `ServerFnError` enum standardizes representation
- `AsStatusCode` trait maps to HTTP status
- Supports `anyhow::Error`, `StatusCode`, custom types
## Extension Points
### Adding Database Support
```rust
config.provide_context(|| {
Box::new(Database::new()) as Box<dyn Any>
});
#[server]
async fn get_user(id: i32) -> Result<User> {
let db = consume_context::<Database>();
db.query(...).await
}
```
### Adding Authentication
```rust
#[server(auth: AuthExtractor)]
async fn protected(data: String, auth: AuthExtractor) -> Result<String> {
// auth extracted from request
}
```
Or via middleware:
```rust
#[server]
#[middleware(AuthLayer::new())]
async fn protected_fn() -> Result<String> { }
```
### Custom Encoding
```rust
pub struct CustomEncoding;
impl Encoding for CustomEncoding {
fn content_type() -> &'static str { "application/custom" }
fn encode(data: impl Serialize, buf: &mut Vec<u8>) -> Option<usize> { }
fn decode<O: DeserializeOwned>(bytes: Bytes) -> Option<O> { }
}
#[server(input = CustomEncoding, output = CustomEncoding)]
async fn my_fn(arg: String) -> Result<String> { }
```
### Response Customization
```rust
#[server]
async fn custom_response() -> Result<String> {
let ctx = FullstackContext::current().unwrap();
ctx.set_response_headers(headers);
Ok("data".to_string())
}
```
## Data Flow
### Server Function Call
```
Client Component
→ Call #[server] fn
→ Create ClientRequest, serialize args
→ HTTP POST /api/function_name
→ Server Router
→ FullstackContext::scope()
→ Extract args, call function
→ Serialize Result
→ HTTP Response
→ Client deserialize
→ Component receives value
```
### SSR Hydration
```
Server:
Page Request → VirtualDom::new()
→ HydrationContext created
→ Render components
→ use_server_future serializes results
→ Inject __DIOXUS_HYDRATION__ script
→ HTTP 200 + HTML
Client:
Receive HTML
→ Parse hydration data
→ VirtualDom::new()
→ HydrationContext populated
→ Components render, hooks get cached data
→ Event listeners attached
→ Hydration complete
```
## Key Patterns
### Inventory System
Global registration via `inventory::submit!` at compile time. Enables dynamic discovery without explicit registration.
### Magic Trait Deref
Multiple `&` deref layers select correct trait impl:
- Prefers `FromRequest` over `DeserializeOwned`
- Prefers `FromResponse` over serialization
### Hash-based Routing
Routes hashed using xxhash64 of module path, preventing collisions with same-name functions in different modules.
### Error Type Tiering
Auto-deref tiers implementations:
1. Typed error struct
2. ServerFnError wrapper
3. anyhow::Error
+320
View File
@@ -0,0 +1,320 @@
# Dioxus Renderers Architecture
Dioxus supports multiple rendering backends through a common trait-based abstraction. Each renderer implements the same interfaces to enable cross-platform component reuse.
## Core Abstraction
### WriteMutations Trait
The bridge between VirtualDOM and real DOM implementations:
- `append_children(id, count)` - Add N nodes to element
- `assign_node_id(path, id)` - Mark element at template path
- `create_placeholder(id)` - Create marker node
- `create_text_node(value, id)` - Create text node
- `load_template(template, index, id)` - Clone from template cache
- `replace_node_with(id, count)` - Replace element
- `set_attribute(name, ns, value, id)` - Update attribute
- `create_event_listener(name, id)` - Register listener
- `remove_node(id)` - Delete element
### HtmlEventConverter Trait
Maps platform-specific events to standardized Dioxus types:
- Each renderer provides its own implementation
- Converts `PlatformEventData` (renderer-specific) to typed event data
- Enables event polymorphism across platforms
### Event Flow Pattern
```
Platform Event → Captured by renderer
→ Converted via HtmlEventConverter
→ runtime.handle_event(name, event, element_id)
→ VirtualDOM handlers invoked
→ State changes → Re-render
→ WriteMutations applied
```
## Web Renderer (dioxus-web)
### WebsysDom Structure
```
WebsysDom
├── interpreter: Sledgehammer JS interpreter
├── document: web_sys::Document reference
├── root: Root DOM node
├── templates: HashMap<Template, u16>
├── runtime: Rc<Runtime>
└── (hydration): skip_mutations, suspense_hydration_ids
```
### Mutation Implementation
- Directly delegates to JavaScript interpreter via wasm-bindgen
- Templates serialized once, stored in JS, instantiated by reference
- Sledgehammer interpreter maintains stack of nodes being constructed
- Binary protocol enables efficient mutation batching
### Event Handling
- Single delegated listener on root element
- Event.target walked up DOM tree for `data-dioxus-id` attribute
- `WebEventConverter` converts web_sys events to Dioxus types
- Supports all standard DOM events (mouse, keyboard, touch, etc.)
### Hydration System
1. SSR server renders HTML with `dio_el` data attributes
2. Client receives base64-encoded hydration context
3. VirtualDOM rebuilt with `skip_mutations = true`
4. Client traverses pre-rendered DOM assigning element IDs
5. Streaming hydration for suspense boundaries via `rehydrate_streaming()`
### Launch Flow
```
launch(root_component, contexts, config)
→ Create VirtualDom
→ Create WebsysDom wrapper
→ If hydrate: Deserialize data, rebuild with skip_mutations
→ Otherwise: vdom.rebuild(&mut websys_dom)
→ Main loop: wait_for_work() → render_immediate() → flush_edits()
```
## Desktop Renderer (dioxus-desktop)
### Wry/Tao Integration
Uses Wry webview library with Tao window management.
### App Structure
```
App
├── unmounted_dom: Cell<Option<VirtualDom>>
├── webviews: HashMap<WindowId, WebviewInstance>
├── shared: Rc<SharedContext>
│ ├── event_handlers: WindowEventHandlers
│ ├── pending_webviews: Vec<PendingWebview>
│ ├── shortcut_manager: ShortcutRegistry
│ └── websocket: EditWebsocket
└── control_flow: ControlFlow
```
### WebviewEdits
Implements WriteMutations for wry-based rendering:
- Delegates to `WryQueue` managing mutation batch
- WebSocket server on random port for mutation transmission
- Binary protocol via Sledgehammer interpreter
### IPC (Interprocess Communication)
```
Browser event → JavaScript → window.postMessage()
→ Wry intercepts request
→ Extract dioxus-data header (base64 JSON)
→ IpcMessage { method, params }
→ Handle: UserEvent, Query, BrowserOpen, Initialize
```
### Protocol Handler
- `dioxus://` custom protocol for asset serving
- Handles `__events` path for event processing
- `__file_dialog` for file selection
- Custom handler namespaces for user-provided handlers
- Checks `dioxus_asset_resolver` for bundled assets
### Native Features
- Menu integration via muda crate
- System tray via trayicon
- Global hotkeys via global_hotkey crate
- File dialogs and drag-drop support
- Headless mode for testing
### Configuration
```
Config
├── WindowBuilder customization
├── Custom event loop
├── Protocols and async protocols
├── Pre-rendered HTML template
├── Context menu disable flag
├── Background color (RGBA)
└── Devtools support toggle
```
## Native Renderer (dioxus-native)
### Blitz Integration
- Blitz layout engine for CSS layout
- Vello for GPU-accelerated vector rendering
- Winit for cross-platform window management
- Not a browser engine - custom native rendering pipeline
### DioxusNativeWindowRenderer
```
DioxusNativeWindowRenderer
├── anyrender-vello wrapper
├── WindowRenderer trait implementation
├── GPU features configurable
└── Custom paint support
```
### Rendering Pipeline
```
VirtualDOM components
→ DioxusNativeDOM
→ Blitz DOM tree with CSS
→ Blitz layout engine
→ Vello renderer
→ GPU rendering
```
### Layout and CSS
- CSS 2.1+ parser (not full CSS 3)
- Flexbox-based layout model
- Computed styles attached to element nodes
- Layout computed bottom-up during mutation
### Application Handler (Winit)
```
Event::NewEvents(StartCause::Init)
→ Create initial window
→ DioxusDocument with VirtualDOM
Resumed → Renderer.resume()
WindowEvent::RedrawRequested
→ VirtualDOM.render_immediate()
→ Mutations applied to Blitz DOM
→ Layout computed
→ Render frame
WindowEvent::Resized → Queue redraw
```
## LiveView Renderer (dioxus-liveview)
### Server-Rendered Architecture
```
Client (Browser with WebSocket)
↓ events
Server (VirtualDOM runs here)
↓ mutations (binary protocol)
Client receives mutations → Sledgehammer applies
```
### LiveViewPool
- Thread pool using `tokio_util::task::LocalPoolHandle`
- Each client spawns pinned task
- VirtualDOM runs on task's executor
- Pool handles multiple concurrent clients
### Per-Client Lifecycle
```
LiveViewPool::launch_virtualdom(ws, || VirtualDom::new())
→ Create MutationState, QueryEngine
→ vdom.rebuild() → Send initial HTML
→ Loop:
tokio::select! {
ws.next() → Handle event/query
vdom.wait_for_work() → Has work
query_rx.recv() → JS query
hot_reload_rx.recv() → Hot reload
}
render_immediate() → Send mutations
```
### Binary Protocol
```
MutationState::write_memory_into(&mut bytes)
→ Sledgehammer binary encoding
→ WebSocket transmission
→ Client: window.interpreter.handleEdits(bytes)
```
### Message Types
1. **Binary Frames** (mutations)
2. **Text Frames** (queries/metadata)
3. **Incoming Events** (user interactions)
### Mounted Element Queries
```
LiveviewElement methods:
scroll_offset() → JS: getScrollLeft/Top()
scroll_size() → JS: getScrollWidth/Height()
client_rect() → JS: getBoundingClientRect()
scroll_to(options) → JS: element.scrollTo()
```
## Interpreter Package (dioxus-interpreter-js)
### Sledgehammer Framework
Ultra-compact binary protocol for DOM mutations shared across renderers.
### Core Components
1. **INTERPRETER_JS** - Base interpreter class
2. **NATIVE_JS** - Platform-specific extensions
3. **SLEDGEHAMMER_JS** - Generated bindings from Rust
### MutationState
- Implements WriteMutations for binary serialization
- Channel-based mutation recording
- Template caching and deduplication
### Stack Machine Operations
```
create_text_node(value, id) → Push text node
create_placeholder(id) → Push comment node
append_children(parent_id, count) → Pop and append
replace_with(id, count) → Replace element
set_attribute(id, name, value, ns) → Set DOM attribute
new_event_listener(name, id, bubbles) → Register listener
```
## Common Patterns
### 1. WriteMutations Implementation
Every renderer implements the trait differently:
- **Web**: Delegates to Sledgehammer JS
- **Desktop**: Accumulates in MutationState, sends via WebSocket
- **Native**: Applies directly to Blitz DOM
- **Liveview**: Accumulates and sends via WebSocket
### 2. Configuration Pattern
Renderers use `Box<dyn Any>` for extensible config:
```rust
launch(app, configs: Vec<Box<dyn Any>>)
→ For each config: try downcast to expected type
```
### 3. Lazy Initialization
Most renderers delay context setup until first window/request.
### 4. Event Loop Integration
- **Web**: WASM bindgen + browser event loop
- **Desktop**: Tao event loop with UserWindowEvent
- **Native**: Winit ApplicationHandler
- **Liveview**: Tokio select! macro
### 5. Mutation Batching
All non-web renderers batch mutations for efficiency:
- Accumulate in temporary structure
- Periodically flush to transport
## Adding a New Renderer
1. **Implement WriteMutations**
- Define how platform applies DOM-like mutations
- Handle template system
- Manage stack machine state
2. **Implement HtmlEventConverter**
- Map platform events to Dioxus types
- Handle serialization if needed
3. **Create Launch Function**
- Create VirtualDOM
- Initialize renderer structures
- Enter platform event loop
- Call `vdom.render_immediate(&mut mutations)`
4. **Define Configuration**
- Create Config struct
- Support `Box<dyn Any>` downcasting
5. **Optional Enhancements**
- Custom mount event data
- Document integration
- History support
- Asset serving
+284
View File
@@ -0,0 +1,284 @@
# Hot-Reload and Hot-Patching Architecture
Dioxus supports two complementary hot-reload systems: RSX template hot-reload for UI changes, and Subsecond hot-patching for Rust code changes.
## Subsecond Hot-Patching
### Jump Table Architecture
Subsecond implements hot-patching through **jump table indirection**:
1. All hot-reloadable functions called through `subsecond::call()` or `HotFn::current()`
2. Runtime looks up function pointer in global `APP_JUMP_TABLE`
3. Jump table points to latest compiled version
4. When patch applied, only jump table is updated
5. Original executable remains untouched
**Advantage**: Decouples running binary from patched code. Safe memory model.
### Patch Application Flow
```
1. ThinLink compiles only modified functions → patch dylib
2. Patch sent via devtools WebSocket
3. subsecond::apply_patch() loads via libloading::Library
4. Base address calculated using main as anchor
5. Jump table updated with old→new address mappings
```
### ASLR Handling
Operating systems randomize memory addresses (ASLR), so compiled addresses don't match runtime.
**Solution**:
1. Running app captures real address of `main` via `dlsym()`/`GetProcAddress()`
2. ASLR reference sent to devserver
3. Devserver computes offset: `old_offset = aslr_reference - table.aslr_reference`
4. All jump table addresses adjusted by offset
5. Patch library base address calculated similarly
6. Final: `(old_address + old_offset) → (new_address + new_offset)`
### Thread-Local Storage (TLS)
TLS presents challenges for hot-patching:
- **Globals and statics**: Generally supported
- **Thread-locals in tip crate**: Reset to initial value on new patches
- **Thread-locals in dependencies**: Work correctly (not re-patched)
**Why**: New thread-local variables exist in patch library's TLS segment, separate from main executable's TLS. Subsecond doesn't rebind TLS accesses.
### Limitations
1. **Struct changes**: Not supported - size/alignment changes cause crashes
2. **Pointer versioning**: Not implemented - all function pointers considered "new"
3. **Workspace support**: Only tip crate patches, library crates ignored
4. **Static initializers**: Changes not observed
5. **Global destructors**: Newly added globals have destructors that never run
### Platform Support
- **Desktop**: Linux, macOS, Windows (x86_64, aarch64)
- **Mobile**: Android (arm64-v8a, armeabi-v7a), iOS Simulator
- **Web**: WASM32 (limited module reloading)
- **Not supported**: iOS device (code-signing)
## RSX Hot Reload
### Hot Literal System
RSX hot reload is **orthogonal** to subsecond. While subsecond reloads Rust functions, RSX hot reload handles template literal values:
**Hot-reloadable**:
- Formatted segments: `"{variable}"`
- Literal component properties: `Component { value: 123 }`
- Dynamic text nodes: `"{expression}"`
**Not hot-reloadable**:
- Rust code changes
- Component structure changes
- Control flow changes
### Template Diffing Algorithm
**Conservative approach**: If Rust code changes, not hot-reloadable.
```
1. Parse old and new files
2. Extract all rsx! macro invocations
3. Replace all rsx! bodies with empty rsx! {}
4. Remove doc comments
5. Compare modified files
6. If identical → Rust unchanged → proceed with template diff
7. If different → requires full rebuild
```
### Dynamic Pool System
Three pools of reusable items from last build:
1. **Dynamic text segments**: `"{class}"`, `"{id}"`
2. **Dynamic nodes**: Components, for loops, if chains
3. **Dynamic attributes**: Spread operators, dynamic values
Each item tracks usage with `Cell<bool>` for waste scoring.
### Greedy Matching Algorithm
For each new dynamic node:
1. Find all candidates from last build with compatible structure
2. Attempt to create hot-reloaded template using candidate's pool
3. Score: Count unused dynamic items remaining
4. Select candidate with least unused items
5. Greedy approach optimal because it maximizes reuse
### Change Detection
**NOT possible**:
- Number of `rsx!` macros changes
- Rust expressions change
- Component structure changes
- Control flow conditions change
**IS possible**:
- Component children content
- Reordering attributes
- Adding dynamic text segments from pool
- Changing literal values
- Shuffling template structure
## Devtools Protocol
### WebSocket Communication
Bidirectional WebSocket between app and devserver.
**Connection**:
- URL: `ws://localhost:3000/_dioxus`
- Query params: `build_id`, `pid`, `aslr_reference`
- Persistent during development
### Message Types
```rust
pub enum DevserverMsg {
HotReload(HotReloadMsg), // Templates + optional jump table
HotPatchStart, // Binary patching starting
FullReloadStart, // Rebuilding entire app
FullReloadFailed, // Build failed
FullReloadCommand, // Full page reload needed
Shutdown, // Devserver shutting down
}
```
### HotReloadMsg Structure
```rust
pub struct HotReloadMsg {
pub templates: Vec<HotReloadTemplateWithLocation>,
pub assets: Vec<PathBuf>,
pub ms_elapsed: u64,
pub jump_table: Option<JumpTable>,
pub for_build_id: Option<u64>,
pub for_pid: Option<u32>,
}
```
### Message Processing
1. `connect(callback)` - Generic connection
2. `connect_subsecond()` - Subsecond-specific with jump tables
3. `apply_changes(dom, msg)`:
- Updates signal-based template cache
- Applies jump table if PID matches
- Marks all components dirty
### WASM-Specific Handling
- Connection at `ws://host/_dioxus?build_id={build_id}`
- Supports playground mode (iframe via postMessage)
- Console logging sent to devserver
- Page reload on `FullReloadCommand`
- Asset cache invalidation via `dx_force_reload` query params
## Integration Flow
### Subsecond in Dioxus Apps
```rust
fn main() {
dioxus::launch(app);
// Devtools automatically connects during init
}
```
For non-Dioxus apps:
```rust
fn main() {
dioxus_devtools::connect_subsecond();
loop {
dioxus_devtools::subsecond::call(|| {
handle_request()
});
}
}
```
### Async Integration
```rust
#[tokio::main]
async fn main() {
dioxus_devtools::serve_subsecond_with_args(
state,
|state| async {
app_main(state).await
}
).await;
}
```
**Process**:
1. Catches patch message
2. Applies jump table
3. Drops current future
4. Creates new future with hot function
5. Continues execution
## Build System Integration
### Fat vs Thin Builds
**Fat Build** (initial):
- Full build with all symbols
- Required for initial symbol table
- Creates `HotpatchModuleCache`
**Thin Build** (patches):
- Compiles only modified functions
- Uses cached dependency symbols
- Produces minimal patch dylib
### JumpTable Structure
```rust
pub struct JumpTable {
pub lib: PathBuf, // Path to patch dylib
pub map: HashMap<u64, u64>, // old_addr → new_addr
pub aslr_reference: u64,
pub new_base_address: u64,
pub ifunc_count: u32,
}
```
### Platform-Specific Patching
- `create_windows_jump_table()` - x86/x64 jump stubs
- `create_native_jump_table()` - macOS/Linux function overrides
- `create_wasm_jump_table()` - WASM indirect call table updates
## Key Design Decisions
1. **Jump table indirection**: Safe, no memory corruption
2. **Conservative RSX diffing**: Rust changes trigger rebuild
3. **Greedy pool matching**: Optimal template reuse
4. **WebSocket protocol**: Real-time bidirectional updates
5. **PID filtering**: Correct process receives patches
6. **Dual system**: RSX for templates, Subsecond for logic
## Future Considerations
### Workspace Support
- Dependency graph analysis for affected crates
- Incremental library crate compilation
- Cross-crate function pointer resolution
### Remote Hot-Reloading
- SCP transport for bandwidth efficiency
- Binary diff for minimal transfers
- Cryptographic verification
### CLI Tunnels
- SSH/TCP protocol wrapping
- Connection persistence
- Latency-tolerant queueing
+347
View File
@@ -0,0 +1,347 @@
# Manganis Asset System Architecture
Manganis provides compile-time asset management through the `asset!()` macro, enabling type-safe asset references with automatic optimization and cache-busting.
## Asset Macro Architecture
### Compile-Time Expansion
```
asset!("/assets/image.png", AssetOptions::image())
↓
1. Path Resolution
- Resolves relative to CARGO_MANIFEST_DIR
- Validates path exists and stays within crate
2. File Hashing
- Creates DefaultHasher from path + options + span
- Produces 16-character hex hash
3. Generate Link Section
- Creates __ASSETS__{hash} symbol
- Creates __MANGANIS__{hash} for legacy CLI
- Both use CBOR serialization via const_serialize
4. Code Generation
- Emits BundledAsset const with PLACEHOLDER_HASH
- Creates Asset struct with function pointers
- Uses volatile reads to prevent optimization
```
### Link Section Generation
Two static link sections for backward compatibility:
1. **Legacy Format** (`__MANGANIS__{hash}`):
- Uses const_serialize_07
- For older CLIs (0.7.0-0.7.1)
2. **New Format** (`__ASSETS__{hash}`):
- Uses const_serialize_08 (CBOR)
- For dx >= 0.7.2
Both start with `PLACEHOLDER_HASH` sentinel, replaced by CLI during build.
## Asset Types and Options
### Type Hierarchy
```
AssetOptions
├── ImageAssetOptions
│ ├── format: ImageFormat (Png, Jpg, Webp, Avif)
│ ├── size: ImageSize (Manual | Automatic)
│ └── preload: bool
│
├── CssAssetOptions
│ ├── minify: bool (default: true)
│ ├── preload: bool
│ └── static_head: bool
│
├── JsAssetOptions
│ ├── minify: bool (default: true)
│ ├── preload: bool
│ └── static_head: bool
│
├── CssModuleAssetOptions
│ ├── minify: bool
│ └── preload: bool
│
├── FolderAssetOptions
│ └── (no custom options)
│
└── Unknown (generic binary)
```
All options serializable at const-time via `SerializeConst` derive.
### CSS Module Integration
```rust
css_module!(Styles = "/my.module.css", AssetOptions::css_module());
// Generates:
struct Styles {}
impl Styles {
pub const header: &str = "abc[hash]"; // Unique scoped class
pub const button: &str = "def[hash]";
}
```
**CSS Identifier Collection**:
- Scans for `.className` and `#idName` patterns
- Converts to snake_case
- Creates ConstStr values that auto-inject stylesheet on deref
## Const Serialization System
### CBOR Format (RFC 8949 Subset)
**Supported Major Types**:
- UnsignedInteger (0)
- NegativeInteger (1)
- Bytes (2)
- String (3)
- Array (4)
- Map (5)
**Not Supported**: Tagged values (6), floating point (7)
### Const Serialization Mechanism
Layout-based binary copying at compile time:
```rust
unsafe trait SerializeConst: Sized {
const MEMORY_LAYOUT: Layout;
}
enum Layout {
Enum(EnumLayout), // repr(C, u8) required
Struct(StructLayout), // Field offsets
Array(ArrayLayout), // Fixed-size
Primitive(PrimitiveLayout),
List(ListLayout), // Variable length
}
```
**Process**:
1. Calculate total size from MEMORY_LAYOUT
2. Copy bytes from source following layout
3. Apply transformations (endianness)
4. Append to ConstVec buffer
### ConstStr Implementation
```rust
pub struct ConstStr {
bytes: [MaybeUninit<u8>; 256], // Fixed 256-byte buffer
len: u32,
}
```
Used for asset paths and CSS identifiers. Serialized as List layout.
## Asset Resolution Pipeline
### Build-Time Processing (dx CLI)
**Phase 1: Binary Scanning**
```
1. Read compiled binary
2. Find __ASSETS__ symbols via objfile parser
3. Platform-specific methods:
- Native: object crate symbol tables
- Windows PE: PDB file parsing
- WASM: walrus data section parsing
- Android: NDK handling
```
**Phase 2: Asset Deserialization**
```
For each __ASSETS__{hash}:
1. Read serialized bytes from section
2. Deserialize BundledAsset via const_serialize_08
3. Fall back to const_serialize_07 if fails
4. Extract: absolute_source_path, bundled_path, options
```
**Phase 3: Unique Asset Collection**
- Deduplicate by (absolute_path, options) pair
- Create AssetManifest with unique set
**Phase 4: Asset Optimization**
| Type | Processing |
|------|------------|
| Image | Resize, convert format, optimize |
| CSS | Minify if enabled |
| JS | Minify if enabled |
| Folder | Recursive copy |
| Unknown | Direct copy with optional hash |
**Phase 5: Binary Patching**
```
For each processed asset:
1. Compute final hash (content + options + version)
2. Create new BundledAsset with:
- bundled_path: "/assets/{output-filename}"
3. Serialize to CBOR
4. Locate __ASSETS__{hash} in binary
5. Overwrite bytes at symbol offset
```
### Runtime Resolution
**Development Mode** (`!is_bundled_app()`):
```
Asset::resolve()
→ Returns bundled().absolute_source_path
→ Browser/native accesses original files
```
**Production Mode** (`is_bundled_app()`):
```
Asset::resolve()
→ Constructs path from:
* base_path() (e.g., "/app")
* bundled_path from BundledAsset
→ Returns "/app/assets/{output-filename}"
```
### Platform-Specific Resolution
**Web (WASM)**:
- `resolve_web_asset()`: Fetches via HTTP `fetch()` API
- Supports CORS headers
- Returns `Vec<u8>`
**Desktop**:
- `resolve_asset_path_from_filesystem()`
- Bundle structure varies:
- macOS: `../Resources/assets/`
- Linux: `../lib/{product}/assets/`
- Windows: `assets/` (same directory as exe)
**Android**:
- `to_java_load_asset()`: Uses NDK AssetManager
- Accesses APK's assets/ directory
- Debug fallback: `/data/local/tmp/dx/`
## Hash-Based Cache Busting
### Hash Computation Layers
**INPUT_HASH** (macro generation):
```
= DefaultHasher(span.debug_string + options.string + asset_path)
Used for: __ASSETS__{INPUT_HASH} symbol
```
**CONTENT_HASH** (build time):
```
= Hash(source_contents + applied_options + manganis_version)
Used for: Final output filename
Example: image.png → image-a1b2c3d4e5f6g7h8.webp
```
**CSS_MODULE_HASH**:
```
= Hash(css_module_options + content_hash)
Used for: Scoped CSS identifiers
```
### Filename Generation
```
IMAGE: /assets/photo.png → /assets/photo-{hash}.webp
CSS/JS: /assets/style.css → /assets/style-{hash}.css
FOLDER: /assets (folder) → /assets/ (unchanged)
```
## Key Architectural Patterns
### Const-Time Code Generation
- Uses proc_macro for compile-time expansion
- Leverages const fn exclusively (no allocators)
- Binary layout determined at compile time
### Volatile Reads for Correctness
- `std::ptr::read_volatile()` prevents optimizing away link section reads
- Critical because link section gets patched post-compilation
### Layout-Respecting Serialization
- Types must have well-defined memory layout
- `repr(C, u8)` for enums
- CBOR for variable-length fields
### Dual Path Resolution
- Dev: Returns absolute_source_path
- Prod: Returns bundled_path with base_path
- Same Asset instance works across configurations
### Symbol-Based Discovery
- No manifest file needed
- Binary symbols serve as asset registry
- Scales with multi-crate applications
## Extension Points
### Adding New Asset Types
1. **Create Options Struct**:
```rust
#[derive(SerializeConst, ...)]
pub struct VideoAssetOptions {
format: VideoFormat,
preload: bool,
}
```
2. **Add AssetVariant Case**:
```rust
pub enum AssetVariant {
Video(VideoAssetOptions),
}
```
3. **Implement Builder**:
```rust
pub const fn video() -> AssetOptionsBuilder<VideoAssetOptions> {
AssetOptionsBuilder::variant(VideoAssetOptions::default())
}
```
4. **Register in CLI** (`assets.rs`):
```rust
AssetVariant::Video(opts) => {
// Video-specific processing
}
```
### Future Concepts
**IAAC (Infrastructure as Code)**:
```rust
const DATABASE_CONFIG: Asset = asset!("/config/db.yaml");
// CLI extracts, validates, deploys
```
**secret!() Macro**:
```rust
const API_KEY: Secret = secret!("DIOXUS_API_KEY");
// Compile-time validation
// Runtime injection from secure store
```
## Version Compatibility
**Legacy (0.7.0-0.7.1)**:
- const_serialize_07
- Symbol: `__MANGANIS__{hash}`
**Current (0.7.2+)**:
- const_serialize_08 (CBOR)
- Symbol: `__ASSETS__{hash}`
**Migration**: CLI tries new format first, falls back to legacy if deserialization fails or PLACEHOLDER_HASH still present.
+363
View File
@@ -0,0 +1,363 @@
# Dioxus Router Architecture
The Dioxus router provides type-safe, declarative routing with support for nested layouts, dynamic parameters, and platform-agnostic navigation.
## Route Definition
### Routable Trait
Routes are defined using Rust enums with the `#[derive(Routable)]` macro:
```rust
#[derive(Routable, Clone, PartialEq, Debug)]
enum Route {
#[route("/")]
Home {},
#[route("/blog/:blog_id")]
Blog { blog_id: usize },
#[route("/edit?:blog_id")]
Edit { blog_id: usize },
#[route("/hashtag/#:hash")]
Hash { hash: String },
}
```
The `Routable` trait provides:
- **`const SITE_MAP`**: Static map of all route segments
- **`render(level: usize) -> Element`**: Renders component at nesting level
- **`FromStr` implementation**: Parses URLs into route enums
- **`Display` implementation**: Converts routes to URLs
- **Helper methods**: `is_child_of()`, `parent()`, `static_routes()`
### Route Matching Priority
1. Query routes (`/?:query`)
2. Static routes (`/route`)
3. Dynamic routes (`/:route`)
4. Catch-all routes (`/:..route`)
### Segment Types
- **Static**: Literal path segments like `/blog`
- **Dynamic**: Single parameters like `/:id` (implements `FromRouteSegment`)
- **CatchAll**: Multiple segments like `/:..segments` (implements `FromRouteSegments`)
### Query Parameters
- **Spread query**: `/?:..query` (entire query string, uses `FromQuery`)
- **Segmented query**: `/?:param1&:param2` (individual params, use `FromQueryArgument`)
### Hash Fragments
`/#:hash_segment` (uses `FromHashFragment`)
## Navigation
### RouterContext
Manages navigation state with these core methods:
```rust
impl RouterContext {
// Navigation
pub fn push(&self, target: impl Into<NavigationTarget>);
pub fn replace(&self, target: impl Into<NavigationTarget>);
// History
pub fn go_back(&self);
pub fn go_forward(&self);
pub fn can_go_back(&self) -> bool;
pub fn can_go_forward(&self) -> bool;
// Introspection
pub fn current<R: Routable>(&self) -> R;
pub fn full_route_string(&self) -> String;
pub fn prefix(&self) -> Option<String>;
// Site map
pub fn site_map(&self) -> &'static [SiteMapSegment];
}
```
The router context is:
1. Created in `Router` component with `RouterConfig`
2. Provided via Dioxus context to children
3. Uses **signals** for reactive updates
4. Tracks **subscribers** for re-rendering
5. Integrates with **history provider**
### NavigationTarget
```rust
pub enum NavigationTarget<R = String> {
Internal(R), // Route parsed by router
External(String), // External URL (browser handles)
}
```
## Link Component
Declarative navigation:
```rust
Link {
to: NavigationTarget,
active_class: Option<String>, // Applied when matches current
class: Option<String>,
new_tab: bool,
onclick: Option<EventHandler>,
children: Element,
}
```
**Behavior**:
1. Accesses router without subscribing
2. Checks if target matches current for `active_class`
3. On click (left button, no modifiers):
- Prevents default
- Routes via `router.push_any(target)`
- Executes optional onclick
4. Generates `<a>` tag with `href` for SEO
5. External links use default browser navigation
## Nested Routes
### Layouts and Outlets
```rust
#[derive(Routable)]
enum Route {
#[nest("/admin")]
#[layout(AdminLayout)]
#[route("/")]
AdminHome {},
#[route("/users")]
Users {},
#[end_layout]
#[end_nest]
#[route("/")]
Home {},
}
#[component]
fn AdminLayout() -> Element {
rsx! {
header { "Admin Panel" }
Outlet::<Route> {} // Children render here
}
}
```
### Outlet Component
- Renders child routes at correct nesting depth
- Uses `OutletContext` to track depth level
- Works with layout wrapping via macro
### VirtualDOM Integration
1. `Routable::render(level)` matches route at nesting level
2. Each layout increments nesting level
3. Router changes mark context-accessing components dirty
4. Outlet at correct depth renders matched component
## Route Parameters
### Parameter Traits
| Component | Trait | Example |
|-----------|-------|---------|
| Path segment | `FromRouteSegment` / `ToRouteSegment` | `/users/:id` |
| Multiple segments | `FromRouteSegments` / `ToRouteSegments` | `/files/:..path` |
| Query full | `FromQuery` | `/?:q=search` |
| Query arg | `FromQueryArgument` | `/?page=1&sort=name` |
| Hash | `FromHashFragment` | `/#section-id` |
### Auto-Implementation
Traits auto-implement for types with standard traits:
- `FromRouteSegment`: `FromStr + Default`
- `FromQueryArgument`: `FromStr + Default`
- `FromQuery`: `From<&str>`
- `FromHashFragment`: `FromStr + Default`
### URL Encoding
- Path segments: `PATH_ASCII_SET` (excludes `/`, `?`, `#`)
- Query strings: `QUERY_ASCII_SET` (excludes `&`, `=`)
- Hash fragments: `FRAGMENT_ASCII_SET`
- Auto encode/decode during parse/display
## Router Macro
### Code Generation
`#[derive(Routable)]` generates three implementations:
**1. FromStr Implementation**:
```rust
impl std::str::FromStr for Route {
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Split URL into path, query, hash
// Percent-decode segments
// Try matching against all routes (specificity order)
// Return error with attempted routes if no match
}
}
```
**2. Display Implementation**:
```rust
impl std::fmt::Display for Route {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Route::Blog { blog_id } => {
write!(f, "/blog/{}", blog_id)?;
// Percent-encode as needed
}
}
}
}
```
**3. Routable Trait Implementation**:
```rust
impl Routable for Route {
const SITE_MAP: &'static [SiteMapSegment] = &[...];
fn render(&self, level: usize) -> Element {
match (level, self) {
(0, Self::Blog { .. }) => rsx! { Layout { Outlet {} } }
(1, Self::Blog { blog_id }) => rsx! { Blog { blog_id: *blog_id } }
_ => VNode::empty()
}
}
}
```
### Error Type Generation
Each route generates detailed error enum:
```rust
pub enum RouteMatchError {
Home(HomeParseError),
Blog(BlogParseError),
BlogIdParseError(ParseIntError),
}
```
### Segment Parsing
```rust
pub enum RouteSegment {
Static(String), // "/blog" -> Static("blog")
Dynamic(Ident, Type), // "/:id" -> Dynamic(id, u32)
CatchAll(Ident, Type), // "/:..paths" -> CatchAll(paths, Vec<String>)
}
```
**Process**:
1. Split URL into path, query, hash
2. Remove trailing slashes
3. Percent-decode components
4. Split path by `/`
5. Classify segments (static/dynamic/catch-all)
6. Match field names to route parameters
7. Validate types exist in struct fields
## History Abstraction
### History Trait
Platform abstraction for navigation:
```rust
pub trait History {
fn current_route(&self) -> String;
fn current_prefix(&self) -> Option<String>;
fn can_go_back(&self) -> bool;
fn can_go_forward(&self) -> bool;
fn go_back(&self);
fn go_forward(&self);
fn push(&self, route: String);
fn replace(&self, path: String);
fn external(&self, url: String) -> bool;
fn updater(&self, callback: Arc<dyn Fn() + Send + Sync>);
fn include_prevent_default(&self) -> bool;
}
```
### MemoryHistory
In-memory fallback implementation:
```rust
pub struct MemoryHistory {
state: RefCell<MemoryHistoryState>,
base_path: Option<String>,
}
struct MemoryHistoryState {
current: String,
history: Vec<String>, // For back
future: Vec<String>, // For forward
}
```
**Behavior**:
- Default starts at `/`
- `push`: Adds current to history, sets new as current, clears future
- `replace`: Only changes current
- `go_back/forward`: Swaps between stacks
- Does not navigate external URLs
**Use Cases**:
- Testing/SSR without browser history
- In-memory fullstack navigation
- Development without browser
### Platform Implementations
**Web**: Browser History API, popstate events
**Desktop**: Platform-specific window/navigation APIs
**LiveView**: WebSocket with server-managed state
## VirtualDOM Integration
1. **Initialization**:
- `Router` creates `RouterContext` with config
- Provides to descendants via context
- Gets current route from history
- Syncs with history if needed
2. **URL Change**:
- `RouterContext::push_any(NavigationTarget)` called
- History provider's method invoked
- `change_route()` callback fires
- Subscribers marked dirty
3. **Re-render**:
- Components using `use_router()`/`use_navigator()` marked dirty
- `Routable::render(level)` generates new tree
- Layouts render at nesting levels
- `Outlet` renders final component
4. **Parameter Passing**:
- Route parameters part of enum variant
- Passed as component props
- Type-safe at compile time
## Key Design Decisions
1. **Trait-Based Extensibility**: Custom types implement parameter traits
2. **Type Safety**: Routes are enums - invalid routes are compile errors
3. **Macro-Driven**: Reduces boilerplate, efficient matching
4. **Platform Abstraction**: History trait enables different renderers
5. **Reactive Updates**: Signals and subscriptions for efficient re-renders
6. **Nested Layouts**: Outlets allow composition without complexity
7. **URL Encoding**: Automatic percent-encoding/decoding
8. **SEO-Friendly**: Links generate real `<a>` tags with hrefs
+324
View File
@@ -0,0 +1,324 @@
# WASM Code Splitting Architecture
The WASM-Split system enables code splitting for large WebAssembly applications, allowing developers to lazily load feature chunks on demand.
## Overview
### Problem Solved
Large WASM binaries can impact initial load times. WASM-Split produces:
- **main.wasm** - Core application
- **module_N_*.wasm** - Lazy-loaded feature chunks
- **chunk_N_*.wasm** - Shared code across modules
- **__wasm_split.js** - Runtime loader
### Output Structure
```
dist/
├── main.wasm # Core application
├── module_1_*.wasm # Feature chunk 1
├── module_2_*.wasm # Feature chunk 2
├── chunk_1_*.wasm # Shared code
└── __wasm_split.js # JavaScript loader
```
## Macro Usage
### `#[wasm_split]` Attribute Macro
Marks async functions as split boundaries:
```rust
#[wasm_split(my_feature)]
async fn load_feature() -> i32 {
// This code will be in module_my_feature.wasm
expensive_computation()
}
```
**Generated Names** (pattern: `__wasm_split_00<module>00_<type>_<hash>_<function>`):
- `__wasm_split_00my_feature00_import_<hash>_load_feature` - FFI import
- `__wasm_split_00my_feature00_export_<hash>_load_feature` - FFI export
**Transformation**:
1. Generates `extern "C"` export with implementation
2. Creates import declaration from `__wasm_split.js`
3. Creates thread-local `LazySplitLoader` with load function
4. Awaits loader before calling imported version
### `#[lazy_loader]` Macro
For libraries creating lazy-loadable wrappers:
```rust
#[lazy_loader(extern "auto")]
fn my_lazy_fn(x: i32) -> i32;
```
Returns `LazyLoader<Args, Ret>` with:
- `.load().await` - Async module loading
- `.call(args)` - Sync invocation after loading
**"auto" ABI**: Automatically combines all modules into one.
## Build Integration
### Input Requirements
CLI takes two binary inputs:
1. **original.wasm** - Pre-wasm-bindgen (with `--emit-relocs`)
2. **bindgened.wasm** - Post-wasm-bindgen
Compilation requirements:
- `--emit-relocs` - Relocation information
- Debug symbols - Function name resolution
- LTO - Symbol consistency
### Processing Pipeline
**Phase 1: Discovery and Graph Building**
```
1. Scan imports/exports for __wasm_split_00<module>00_* pattern
2. Parse relocations from original.wasm
3. Build call graph from:
- CODE section relocations
- DATA section relocations
- Direct function calls via IR walking
4. Build parent graph (inverse for reachability)
5. Compute reachability for each split point
```
**Phase 2: Chunk Identification**
- Identify functions used by multiple modules
- Extract into shared chunks
- Build chunk dependency graph
**Phase 3: Module Emission**
Three output types (parallel via rayon):
**A. Main Module**:
- Remove split point exports
- Replace element segments with dummy functions
- Create indirect function table
- Convert split imports to stub functions
- Re-export memories, globals, tables
- Run garbage collection
**B. Split Modules** (per split point):
- Identify unique vs shared symbols
- Convert chunk functions to imports
- Clear and reconstruct data segments
- Create element segment initializers
- Export main entry function
- Run GC
**C. Chunk Modules** (shared code):
- Similar to split modules
- No main export function
- Contains commonly-used functions
## Walrus Operations
The system uses **walrus** for WASM binary manipulation:
### Function Table Management
```
1. Ensure funcref table exists (__indirect_function_table)
2. Expand table for split modules + shared functions
3. Create passive element segments for initialization
```
### Stub Function Creation
Stubs perform indirect calls:
```
1. Push function arguments onto stack
2. Push table index (pointing to real function)
3. Call via CallIndirect with table
```
### Import/Export Manipulation
- Convert imports to local stub functions
- Re-export shared resources
- Use `__wasm_split` namespace for shared imports
### Graph Analysis
**Node Types**:
- `Node::Function(FunctionId)`
- `Node::DataSymbol(usize)`
**Graphs**:
- **Call Graph**: Function → callees
- **Parent Graph**: Inverse (what calls them)
- **Reachability Graph**: Per split point, all reachable symbols
- **Main Graph**: All reachable from main exports
## Runtime Loading
### LazyLoader Structure
```rust
pub struct LazyLoader<Args, Ret> {
// Generic loader for function (Args) -> Ret
}
impl LazyLoader {
pub async fn load(&self) -> bool; // Load module
pub fn call(&self, args: Args) -> Result<Ret, SplitLoaderError>;
}
```
### LazySplitLoader State Machine
Three states:
1. **Deferred** - Not initiated, holds load function
2. **Pending** - Load initiated, waiting for callback
3. **Completed** - Loaded (with success boolean)
**Async Interface**:
- `SplitLoaderFuture` implements `Future<Output = bool>`
- `poll()` handles state transitions
- Uses `Waker` to resume on callback
### JavaScript Runtime (`__wasm_split.js`)
**`makeLoad()` Function**:
```javascript
async function(callbackIndex, callbackData) {
// 1. Await chunk dependencies
// 2. Check if already loaded
// 3. Fetch module binary
// 4. Call initSync from main.wasm
// 5. Construct import object:
// - Memory from main module
// - Indirect function table
// - Stack pointers and TLS base
// - Main module exports as imports
// - Fused imports from other modules
// 6. WebAssembly.instantiateStreaming()
// 7. Add exports to fusedImports
// 8. Invoke callback with table index
}
```
**Callback Mechanism**:
```javascript
__indirect_function_table.get(callbackIndex)(callbackData, true)
```
Wakes up Rust Future waiting in loader.
## Integration Flow
```
Compile Phase: Rust code with #[wasm_split]
↓
Macro Expansion: FFI functions + LazyLoader
↓
Build Phase: wasm-split CLI processes binaries
↓
Parse Relocations → Build Call Graph → Identify Split Points
↓
Compute Reachability → Parallel Emission
↓
Output: main.wasm + module_*.wasm + chunk_*.wasm + __wasm_split.js
↓
Runtime: JavaScript fetch → Instantiate → Callback → Future wakes
↓
Lazy functions available synchronously
```
## Key Transformations
### Main Module
1. Remove split point exports
2. Replace element segments with dummies
3. Create indirect function table
4. Convert imports to stubs (call indirect table)
5. Re-export shared resources
6. Remove relocation/linking sections
7. GC unused code
### Split Module
1. Partition functions (unique vs shared)
2. Convert chunk functions to imports
3. Clear data segments, reconstruct needed data
4. Create element segment initializers
5. Create FFI imports from main
6. Export entry function
7. Remove start function
8. GC unreachable symbols
## Usage Example
```rust
// Define lazy-loadable feature
#[wasm_split(admin_panel)]
async fn load_admin_panel() -> AdminPanel {
AdminPanel::new()
}
// Use in application
async fn handle_route(route: Route) {
match route {
Route::Admin => {
let panel = load_admin_panel().await;
panel.render();
}
_ => // ...
}
}
```
## Design Considerations
### Split Point Granularity
- One `#[wasm_split]` per feature chunk
- Too fine-grained = many small fetches
- Too coarse = large chunks
### Shared Code Optimization
- Common functions extracted to chunks
- Prevents duplication across modules
- Chunks loaded before dependent modules
### Memory Sharing
- Single memory instance shared across all modules
- Indirect function table shared for cross-module calls
- TLS base pointer coordinated
### Error Handling
```rust
pub enum SplitLoaderError {
NotLoaded,
LoadFailed,
}
```
Loader returns `Result` for robust error handling.
## Limitations
1. **Async boundaries**: Split points must be async functions
2. **Call graph static**: Determined at build time
3. **No dynamic imports**: All split points known at compile time
4. **WASM-specific**: Only works with wasm32 target
## Future Considerations
- Route-based automatic splitting
- Size-based chunk optimization
- Prefetching hints
- Integration with service workers for caching