| Copyright | (C) 2016-2026 David M. Johnson |
|---|---|
| License | BSD3-style (see the file LICENSE) |
| Maintainer | David M. Johnson <code@dmj.io> |
| Stability | experimental |
| Portability | non-portable |
| Safe Haskell | None |
| Language | Haskell2010 |
Miso.Native
Description
miso native 📱
Miso.Native targets native mobile devices by driving the
Lynx runtime instead of the browser DOM. The same
MVU programming model, Component API, event
delegation and virtual-DOM diffing you use on the web (Miso) carry over
unchanged — only the element vocabulary differs (view_,
text_, … instead of div_ /
span_) and rendering is performed by Lynx's
element PAPI rather than by
mutating a browser DOM.
This module is the native analog of the miso / startApp
entrypoints: native (and nativeWithContext) boot a root Component onto
the Lynx runtime.
Enabling native
The native backend is gated behind the native cabal flag. It must be
enabled to bring Miso.Native and the Miso.Native.* element / event / FFI
modules into scope (build with -fnative). Web / WASM builds are unaffected —
all cross-thread machinery lives behind the 1 CPP guard.
The dual-thread architecture
Lynx runs your application across two threads, and miso maps onto both:
- BTS — the background thread ("background thread script"). This is
where your application logic lives. Everything runs here by default:
the
updatefunction, event handling,Effectscheduling and all virtual-DOM diffing. - MTS — the main thread ("main thread script"). This thread owns the actual element tree and rendering. It is where the pixels land. It is also available as a low-latency escape hatch for performance-critical event handling (see Main-thread events below).
The same Haskell bundle runs on both threads; the native runtime
(ts/miso-native.ts) selects the BTS or MTS drawing context per-thread from a
global flag, so there is no renderer to register — native starts the app
directly.
The guiding principle: everything originates on the BTS. The MTS is a rendering surface that the BTS drives across the thread boundary.
Knowing which thread you are on
Lynx builds the bundle with rspeedy — its Rust-based
tooling — which compiles the sources twice, once per thread, inlining a
compile-time constant (BACKGROUND) that distinguishes the two. That
constant surfaces in Haskell as three top-level Bools in Miso.Runtime:
mts—Truewhen this execution context is the Lynx main thread.bts—Truewhen this context is the Lynx background thread.web—Truefor a plain web / WASM build (neither Lynx thread).
Exactly one is True, and the value is invariant for the lifetime of a JS
context, so the runtime computes it once and caches it. Runtime code branches
on mts / bts to decide where work runs (e.g. the scheduler suppresses the
paint step on the MTS, which keeps only a read-only model replica).
What crosses the thread boundary, and how
Because logic (BTS) and rendering (MTS) live on different threads, miso synchronizes them by shipping messages across the boundary. This is largely invisible, but understanding it explains the API constraints below.
- Initial draw — The very first
Drawhappens on the MTS itself, and it does not rely on the BTS diffing a tree and transferring patches across the boundary. The rootComponentis booted from aStaticPtr(vianative/nativeWithContext), so the MTS reconstructs it from the pointer'sStaticKeyalone and renders the first frame locally (Lynx's instant first frame). Only after this initial draw does the cross-thread patch protocol take over: __every subsequent diff runs on the BTS and ships patches to the MTS__ to apply. - Subsequent component mounts — When the BTS
viewmounts a childComponent, that mount is synchronized to the MTS asynchronously using static mounting: the child is wrapped in astaticpointer (-XStaticPointers) so only itsStaticKey— not a closure — needs to cross the boundary. The MTS dereferences the key to rebuild the component locally. Seevcomp/mountStatic_. - State synchronization — The BTS owns the shared
modeland ships it to the MTS as it changes (JSON-serialized, hence theToJSON/FromJSONconstraints on native mounting combinators), so main-thread*MainWithhandlers observe an eventually-consistent copy. A child's initialpropsride the static mount payload — thestaticpointer carries the constructor and thepropsvalue is shipped separately, so it may depend on the parentmodel. Butpropsand the globalcontextare not re-synced on later changes: after the first frame they stay background-thread-only (matching ReactLynx — see Main-thread events). - Events — Events raised on the MTS are, by default, forwarded to the BTS
where
updateruns (see below). Cross-thread handlers are carried as anEventHandler, embedded withevent. static (…)so the peer thread can rebuild the handler from itsStaticKey.
First-frame rendering (instant first frame)
The MTS painting frame one itself (the Initial draw above) is Lynx's
instant first frame: the user sees UI without waiting for a background render
and patch round-trip. Meanwhile the BTS boots the same root and builds the
identical virtual-DOM tree in lockstep — with __deterministic nodeId
parity, so both threads address the same elements — but suppresses its own
create-patches__ for that first frame, since the MTS already painted them. A
single global initialDraw latch governs this on both threads; native /
nativeWithContext clears it once the whole root mount has finished.
After that handover the responsibilities are fixed, mirroring ReactLynx: the
BTS is the sole diff / paint authority — it runs update, diffs, and ships
patches — while the MTS only applies those patches (and runs main-thread
scripts / handlers). The MTS never diffs or repaints from the scheduler again;
this is why the shared model is BTS-owned and why nothing you do on the MTS
should try to redraw declaratively.
Static mounting
Because component constructors, event handlers and effects may need to be
reconstructed on the other thread, native miso threads them across the
boundary as static pointers rather than closures. This requires the
-XStaticPointers language extension.
The root component is mounted with mountStatic_ wrapped in
static:
{-# LANGUAGE StaticPointers #-}
-----------------------------------------------------------------------------
module Main where
-----------------------------------------------------------------------------
import Miso
import Miso.Native
-----------------------------------------------------------------------------
main :: IO ()
main = native nativeEvents (static (mountStatic_ app))
Child components are embedded in a view the same way, with vcomp:
view _ _ _ = view_ [] [vcomp() (static (mountStatic_childComponent)) ]
Static-pointer limitation. A static form may only close over
top-level, closed bindings — it cannot capture local variables. This is why
component constructors and main-thread handlers are supplied as references to
top-level definitions, with any runtime data (props, decoded event payloads)
shipped separately as serialized values rather than captured in a closure.
Effects: choosing a thread
Because an IO closure can't cross the thread boundary (only JSON-serialized
actions can), cross-thread work is expressed as dispatching an action to
the thread that should handle it. Two combinators do this:
runOnBGaction— runaction'supdateon the background thread (BTS). Used by a main-thread event handler that needs to change shared state, since the BTS solely owns themodel.runOnMainaction— runaction'supdateon the main thread (MTS). Used by a BTS effect that needs an imperative main-thread operation (see Miso.Native.MainThread).
Each ships only the given action to the target thread (or dispatches it
locally when already there), where its update runs exactly once. Sibling
effects in the current update are unaffected, and nothing is
double-executed. Off the native runtime both are an ordinary local dispatch,
equivalent to issue.
Subscriptions and threads
A Sub is dynamic — it is just a run in a forked thread — and a component's subs are started on __every
thread it mounts on. So a Sink action ->
IO ()Sub runs on both the BTS and the
MTS__ (once each), and each copy dispatches into its own thread's scheduler.
Because a Sub is ordinary runtime IO — unlike a static event
handler, whose thread is fixed at compile time — it selects its own thread at
runtime with the mts / bts Bools. This is the dynamic analog of a
handler's *Main variant:
-- background-only: open the socket once, feed the model
wsSub sink = when bts (websocketConnect "wss://…" sink)
-- main-thread-only: drive an imperative animation
animSub _ = when mts (eachFrame step)
Guard anything that must be single-owned. Without a bts / mts gate a
stateful sub double-runs — two websocket connections, a timer ticking on both
threads — so pin such subs to one thread. The no-op fork on the other thread
returns immediately.
Main-thread events
Thread affinity is per-handler, not per-event-name. Any given event can be
handled on either thread; the choice is made at each handler, so the same
event (say tap) may run on the BTS for one element and the MTS for another.
The default is the BTS — a plain onTap
handler runs on the background thread. Opting a handler into the MTS is
explicit (the *Main variants below); nothing runs on the main thread unless
you ask for it.
By default an event handler runs on the BTS: the event is forwarded from
the MTS, update runs on the BTS, the model changes, and the resulting diff is
shipped back to the MTS to paint. That round-trip is fine for most
interactions but adds latency for gesture- and scroll-linked animation.
For those cases, handlers have *Main-suffixed variants (e.g.
onTapMain,
onTouchMoveMain) that run __synchronously on
the MTS__ — no VDOM diff, no patches, no BTS round-trip. Such a handler is
imperative: it mutates the target element directly through the helpers in
Miso.Native.MainThread (e.g. setStyleProperty). The
*MainWith variants additionally hand the handler the current model and the
target DOMRef (\event model domRef -> action).
Because a main-thread handler must be reconstructed on the MTS, it is an
EventHandler embedded with event . static — so
main-thread event handlers require -XStaticPointers (the static keyword
is how the handler crosses to the MTS by StaticKey):
{-# LANGUAGE StaticPointers #-}
view _ _ _ =
view_ [ event (static (onTapMain HandleTap)) ] []
The same static capture limitation applies: an onTapMain handler refers to
a top-level action / function; runtime data reaches the handler via the
decoded event payload, not a captured closure.
The generic primitives (on / onMain). The
per-element on* / on*Main helpers are sugar over two combinators, and the
same (eventName, decoder, toAction) works with either — that is how one
event is captured on whichever thread you choose, per handler:
onname decoder toAction→ a plainAttributethat runs on the BTS. Nostatic: a background handler is reconstructed nowhere else, so it may close over the enclosingview.onMainname decoder toAction→ anEventHandlerthat runs on the MTS, embedded withevent. static.
-- sametapevent, one handler per thread: view_ [on"tap" emptyDecoder (\_ _ _ -> Grow) ] children -- BTS view_ [event(static (onMain"tap" emptyDecoder onTapMain)) ] children -- MTS
The Attribute-versus-EventHandler+static split is the mechanism: only
the main-thread handler has to cross to the MTS by StaticKey,
which is why onMain (and every *Main helper) needs
-XStaticPointers while on does not. (onMainWithOptions
exposes Phase / Options for the MTS
variant, mirroring onWithOptions.)
Reaching the model (and why it is passed, not captured). A static
main-thread handler cannot close over the model, props or context from
the enclosing view — those are local bindings, which static forbids. So
rather than capture them, the *MainWith variants __pass the model as an
argument__ to the handler, giving imperative MTS code the state it needs to
integrate without a BTS round-trip. Note this is the main-thread's own copy
of the model: it is populated on the MTS eventually consistently from the
BTS (the authoritative model still lives on the background thread), so a
handler may observe a value slightly behind the latest BTS state.
Props and context are not on the main thread. Unlike the model, a
component's props and the app-global context are not mirrored to the
MTS at all (matching ReactLynx, where React state — and therefore props and
context — is background-thread-only). They live solely on the BTS; the MTS
keeps only its boot values, so getProps /
getContext inside a main-thread handler would read stale data.
Only the model is hydrated to the MTS (eventually consistently, as above).
If a main-thread handler needs a prop or context value, fold it into the
model or carry it in the dispatched action payload — do not read props or
context on the main thread. This also means less cross-thread traffic: the
BTS ships a props/context change to the MTS only via the initial MOUNT
(for props), never on every subsequent change.
Ownership caveat. A property you drive imperatively from the MTS must not
also be written declaratively by the BTS view for the same element: both
threads write the shared element tree through the same PAPI with no
arbitration, so one will clobber the other. Keep a single owner per
(element, property) — typically compositor properties like transform /
opacity that the view leaves alone.
Main-thread-local state: MainThreadRef
A main-thread handler is imperative and must not write the BTS-owned model:
shared state changes belong on the background thread, so dispatch them with
runOnBG. But gestures and scroll-linked animation often need
mutable state that lives only on the MTS — the current drag offset, a fling
velocity, whether a follow loop is active. For that, use a
MainThreadRef, a thin IORef wrapper for
main-thread-only state (the analog of ReactLynx's MainThreadRef):
dragRef ::MainThreadRefDouble dragRef =mainThreadRef0 {-# NOINLINE dragRef #-}
mainThreadRef allocates the underlying cell as a CAF
via unsafePerformIO, so __every top-level binding needs its
own {-# NOINLINE #-} pragma__ — otherwise GHC may inline the CAF and split
the state into independent copies. Reads and writes
(readMainThreadRef /
writeMainThreadRef /
modifyMainThreadRef) are ordinary IORef
operations — safe without atomics because the MTS is single-threaded —
and modifyMainThreadRef_ takes a
so you can drive updates with the
Miso.Lens operators (State a ().=, %=, +=, …).
It pairs with eachFrame for a vsync-coalesced
animation loop: read the latest gesture state from the ref, imperatively paint
at most once per frame (via setStyleProperty /
setStylePropertyTransform), and stop by returning
False when the gesture ends.
Platform APIs and thread restrictions
Mirroring Lynx ("not all APIs exist on both threads"), miso's native APIs
are split by thread, and calling one from the wrong thread fails at runtime —
the type system does not catch it, so guard with mts / bts when code may
run on either thread. Neither module is re-exported here; import it directly.
Native modules (BTS-only) — Miso.Native.Module wraps Lynx's global
NativeModules(platform capabilities: storage, clipboard, device info, …).callNativeModuleinvokes a void-returning method andcallNativeModuleWitha callback method whose result is decoded viaFromJSON.NativeModulesexists only on the BTS:callNativeModule"NativeLocalStorageModule" "setStorageItem" [String"key",String"value" ]
update runs on the BTS by default, so this just works there; from a
main-thread handler, hop to the BTS first with runOnBG. On the
MTS the module is undefined and the call logs a consoleError.
- Main-thread element ops (MTS-only) — the imperative helpers in
Miso.Native.MainThread (
setStylePropertyetc.) and the element PAPI they call exist only on the MTS; on the BTS they no-op. Drive them from a*Mainhandler or viarunOnMain.
A minimal native component
-----------------------------------------------------------------------------
{-# LANGUAGE StaticPointers #-}
-----------------------------------------------------------------------------
import Miso
import Miso.Native
-----------------------------------------------------------------------------
view :: context -> props -> Model -> View context Action
view _ _ m =
vfrag_
[ view_ [ onTap Increment ] [ text_ [] [ "+" ] ]
, text_ [] [ text $ ms (show m) ]
, view_ [ onTap Decrement ] [ text_ [] [ "-" ] ]
]
More information on how to use miso is available on GitHub
Synopsis
- native :: Events -> StaticPtr (SomeStaticComponent () ()) -> IO ()
- nativeWithContext :: (ToJSON context, FromJSON context, Eq context) => Events -> context -> StaticPtr (SomeStaticComponent () context) -> IO ()
- mountStatic_ :: (Eq context, Eq model, FromJSON model, ToJSON model, FromJSON action, ToJSON action, FromJSON context, ToJSON context) => Component context () model action -> SomeStaticComponent () context
- mountStaticWithProps :: (Eq context, Eq props, Eq model, FromJSON model, ToJSON model, FromJSON action, ToJSON action, FromJSON props, ToJSON props, FromJSON context, ToJSON context) => Component context props model action -> SomeStaticComponent props context
- mountStaticUseContext :: (Eq context, Eq model, FromJSON model, ToJSON model, FromJSON action, ToJSON action, FromJSON context, ToJSON context) => Component context () model action -> SomeStaticComponent () context
- module Miso.Native.Element
- module Miso.Native.FFI
- module Miso.Native.Event
Entrypoint
native :: Events -> StaticPtr (SomeStaticComponent () ()) -> IO () Source #
The native drawing context is already selected per-thread by the runtime
(ts/miso-native.ts picks bts or mts from BACKGROUND), so there
is no renderer to register — we start the app directly.
{-# LANGUAGE StaticPointers #-}
import Miso
import Miso.Native
main :: IO ()
main = native nativeEvents (static (mountStatic_ app))
nativeWithContext :: (ToJSON context, FromJSON context, Eq context) => Events -> context -> StaticPtr (SomeStaticComponent () context) -> IO () Source #
Like native, but the user can specify a global context object.
{-# LANGUAGE StaticPointers #-}
import Miso
import Miso.Native
main :: IO ()
main = nativeWithContext nativeEvents () (static (mountStatic_ app))
Component mounting
Arguments
| :: (Eq context, Eq model, FromJSON model, ToJSON model, FromJSON action, ToJSON action, FromJSON context, ToJSON context) | |
| => Component context () model action |
|
| -> SomeStaticComponent () context |
Component mounting combinator.
Note: only use this if you're certain you won't be diffing two Component
against each other. Otherwise, you will need a key to distinguish between
the two Component, to ensure unmounting and mounting occurs.
mountStatic_ $ component model noop $ \m -> div_ [ id_ "foo" ] [ text (ms m) ]
Since: 1.9.0.0
Arguments
| :: (Eq context, Eq props, Eq model, FromJSON model, ToJSON model, FromJSON action, ToJSON action, FromJSON props, ToJSON props, FromJSON context, ToJSON context) | |
| => Component context props model action |
|
| -> SomeStaticComponent props context |
Component mounting combinator.
Note: only use this if you're certain you won't be diffing two Component
against each other. Otherwise, you will need a key to distinguish between
the two Component, to ensure unmounting and mounting occurs.
It takes only the component and yields a closed props -> component
constructor (SomeStaticComponent) suitable for static — the props value
is not supplied here, but later at the vcomp site, so a parent can pass
runtime props (e.g. derived from its own model) without an explicit lambda:
Static mounting automatically provides the key_ at compile time (via staticKey).
So the user doesn't need to use the +> combinators.
vcomp (model ^. field) (static (mountStaticWithProps child))
Since: 1.11.0.0
mountStaticUseContext Source #
Arguments
| :: (Eq context, Eq model, FromJSON model, ToJSON model, FromJSON action, ToJSON action, FromJSON context, ToJSON context) | |
| => Component context () model action |
|
| -> SomeStaticComponent () context |
Static Component mounting combinator that opts the child
into app-global React-style context updates.
Equivalent to mountStatic_, but sets useContext = True on the mounted
Component so it re-renders whenever the context changes
(see modifyContext). The static-key counterpart of
mountUseContext — use this (with vcomp_) instead of mountUseContext
under the Lynx dual-thread (1) backend.
vcomp_ (static (mountStaticUseContext myComp))
Since: 1.12.0.0
Element
module Miso.Native.Element
FFI
module Miso.Native.FFI
Event
module Miso.Native.Event