Reference · Apps and the UI kit
Cryswol: every system verb in Crystal
The Crystal shard that gives an app windows, notifications, files, power, network, Search and the other system verbs, as typed objects.
On this page
What it is
Cryswol is the Crystal library behind every WolOS app written in Crystal. An app writes require "cryswol" and calls the system through Crystal objects, blocks and exceptions: windows, notifications, the clipboard, preferences, file panels, power, network, sound, Search, messages between apps, devices, sharing and background services.
Why
The system interface is a C library, and C asks you to manage memory and error codes by hand. Cryswol does that once, in one place: handles are released for you, a callback can never crash into the system, and a failed call raises an error that carries the system's own sentence. It adds no privilege: the system still checks every permission.
How
- Install the Developer module:
wol modules install dev(or tick Developer Tools in the installer). See Set up for development. You seecrystal --versionanswer in a terminal. - Write one file that begins with
require "cryswol". Your first app in Crystal walks through a seven-line program that reads the system and posts a notification. (wol app new hello --template cli, which writes a project for you, comes with the next update.) - Build it with
crystal build, with the system shards onCRYSTAL_PATHas shown below. - Blocking calls (file panels, sharing, messages) go through
Wol.background, so the window never freezes.
If something goes wrong
| What you see | What to do |
|---|---|
crystal: command not found | The base image has no compiler. Install the Developer module: wol modules install dev. |
Wol::PermissionDenied with a sentence | The person has not allowed it. Show the sentence; it says where in System Settings to allow it. |
Wol::UsageError | The program used something after it was released or outside its callback. It is a bug in the app, never shown to a person. |
| The window freezes on a file panel | Call it through Wol.background, which runs it on its own thread with its own context. |
| An app refuses to start on another computer | It was built against a newer system library. Its package must say depends=(libwol>=1.N) for the newest verb it calls. |
Related
Your first app in Crystal · Set up for development · Your first Wolkit app · libwol: the app API · Crystal shards · The Cryswol page
Reference
Cryswol is the Crystal shard that gives an app every WolOS verb: windows and Kit parts, notifications, menus, the clipboard, preferences, file panels, power, network, audio, Search, accounts, messages between apps, devices, sharing and background services. It lives in wolos-crystal/shards/cryswol/ (BSD-2-Clause), its namespace is Wol, and it is a thin, typed layer over libwol.so.1 and crates/wol-abi/include/wol.h (ADR 0020). An app writes require "cryswol" and gets Crystal objects, exceptions and blocks, never pointers.
Why
ADR 0036 makes Crystal the userland language, and it says every WolOS verb must be reachable from Crystal through generated declarations, never hand-written ones. The C ABI stays the contract: Cryswol adds no wire and holds no privilege, so wold still checks every permission (System Settings > Privacy & Security), and the same wol.h serves C, Swift and .NET. The high layer exists for three reasons. Memory: libwol's handles have a retain and release protocol that a garbage collector does not see. Callbacks: a Crystal block cannot be handed to C directly, and an exception must not unwind into C. Errors: a status code plus an out-parameter becomes one exception that carries libwol's sentence.
Layers
| Layer | Files | Written by | License |
|---|---|---|---|
Low: lib LibWol | src/wol/lib_wol.cr | generated from wol.h (crates/wol-abi/tests/common/bindgen.rs) | BSD-2-Clause |
Low, opt-in: lib LibWopkg | src/wol/pkg/lib_wopkg.cr | generated from crates/wopkg/include/wopkg.h | GPL-2.0-or-later |
Low, opt-in: lib LibRealtime | src/wol/realtime/lib_realtime.cr | generated from crates/realtime/include/wol_realtime.h, no @[Link] | GPL-2.0-or-later |
| Core | src/wol/{version,error,handle,callbacks,string,context,loop,background,info,event}.cr | by hand | BSD-2-Clause |
| Kit verbs | src/wol/{window,widget,notify,menu,toolbar}.cr | by hand | BSD-2-Clause |
| Service verbs | src/wol/{system,clipboard,prefs,file,power,audio,network,search,account,message,device,share,service}.cr | by hand | BSD-2-Clause |
| Packages, opt-in | src/wol/pkg/pkg.cr over LibWopkg | by hand | GPL-2.0-or-later |
require "cryswol" (src/cryswol.cr) loads lib_wol.cr and the BSD layer, every file directly in src/wol/. The GPL parts sit in subdirectories, so that glob never reaches them, and are opt-in: require "cryswol/pkg" and require "cryswol/realtime". They are GPL, and an app of any license must be able to link the default shard. The generator writes each lib_*.cr both to crates/wol-abi/bindings/crystal/ (where the C-ABI conformance programs hello.cr, parts.cr and services.cr use it) and to the shard; cargo test -p wol-abi --test bindings fails when either copy differs from the header.
Ownership rules
- Handles. Each opaque type has one wrapper class built on
Wol::Handle. It holds the pointer, calls the matching*_releasefromfinalize, and#closedrops the reference at once. A call on a released handle raisesWol::UsageError; libwol also refuses a stale handle, so this is a second guard. - Borrowed results. A
*_get_*result lives as long as its argument. Strings are copied out at once; aWol::Infochild keeps its parent alive, so it can never outlive the answer it came from. - Callbacks. A block is boxed, and the box is held by the Crystal wrapper object, which the context keeps registered until it is cancelled or released. The reason: libwol's heap is invisible to the collector, so a box held only as
user_datawould be freed. The function pointer handed to C is a class-level trampoline, not a closure (Crystal refuses to send a closure to C). It unboxesuser_data, runs the block insidebegin/rescue, and routes an exception toWol::Loop.on_error. Nothing unwinds into C. - Callback-scoped objects. A Search reply, a message and an event are valid only while their callback runs (
wol.hsays so per call). The wrapper marks them invalid when the block returns; later use raisesWol::UsageError.Wol::Message#keepretains a message to reply later. - Strings in. Every string goes as pointer plus byte length and libwol copies it; Crystal strings may hold a NUL byte, and libwol refuses one where
wol.hforbids it. An optional string (detail,icon) isnil. - Strings out.
String.new(ptr, len)at once, then release the owned reference. A pointer from libwol is never kept. - None is
nil. An out-parameter that is NULL on success means "nothing": an unset preference, a cancelled panel, a missing key. Those returnnil(String?,Wol::OpenFile?), not an exception. - File descriptors.
Wol::OpenFile#to_ioreturns a duplicate (close-on-exec), so it survives the release of the file. The loop reads a duplicate ofwol_context_get_fdtoo, and closes it before the context is released: wrapping libwol's own descriptor leaves a stale event-loop registration that hangs the next context given the same number.
The service verbs
Each group is one module or class in its own file under src/wol/. The context comes first, and the answer keys are those wol.h lists for the call. An Info answer reads like JSON: info["battery"]["percent"].as_i?, info["interfaces"].map(&.["kind"].as_s), and a missing key is nil.
| Group | Crystal | Permission | Spec |
|---|---|---|---|
| System | Wol::System.info(ctx) : SystemInfo (os_name, kernel, appearance, accent_hex) | none | answers_spec.cr |
| Clipboard | Wol::Clipboard.text(ctx), .set_text(ctx, text) | clipboard-read, clipboard-write | prefs_clipboard_spec.cr |
| Preferences | prefs = Wol::Prefs.new(ctx), then prefs["k"]?, prefs["k"] = v, prefs.delete("k"), prefs.keys | app-preferences | prefs_clipboard_spec.cr |
| Files | Wol::Panel.open(ctx, title) : OpenFile?, .save(ctx, name, title); OpenFile#name, #to_io, #read | file-panels | files_spec.cr |
| Power | Wol::Power.info(ctx), .keep_awake(ctx, reason) { ... } | power-info, keep-awake | answers_spec.cr |
| Network | Wol::Network.info(ctx), .online?(ctx) | network-info | answers_spec.cr |
| Audio | Wol::Audio.info(ctx), .set_volume(ctx, 0..100), .set_muted(ctx, bool) | audio-info, audio-control | answers_spec.cr |
| Search | Wol::Search.query(ctx, text); Wol::Search.provide(ctx, on_open: ->(key : String) { }) { |reply, text| reply.add(key, title, detail) } | search, search-provider | answers_spec.cr, search_spec.cr |
| Account | Wol::Account.info(ctx) | account-info | answers_spec.cr |
| Messages | Wol::Endpoint.new(ctx, name) { |m| m.reply(text) }, Message#keep, Wol::Message.send(ctx, peer, name, body), .publish(ctx, topic, body) | ipc, ipc:<peer> | messages_spec.cr |
| Devices | Wol::Device.list(ctx), .open(ctx, vendor, product) : OpenFile | devices, device:<vendor>:<product> | answers_spec.cr, files_spec.cr |
| Sharing | Wol::Share.text(ctx, title, text): "copied", "saved" or "cancelled" | share | prefs_clipboard_spec.cr |
| Services | Wol::Service.start(ctx, name), .stop, .status; Wol::Service.current (the name this process runs as, or nil) | background | services_spec.cr |
| Packages (opt-in, GPL) | Wol::Pkg::Context.new(config), then #search, #info, #index_update(req) { |event| }, #verify, #history_record, #rollback, ...; results are JSON text | wold's consent, for system changes | pkg_spec.cr |
Errors
Every verb goes through one private check: on a status other than WOL_OK it reads the code and the message of the wol_error, releases it, and raises. The message is libwol's sentence, word for word; it is written for a person and passes the copy rules.
wol_status | Value | Crystal |
|---|---|---|
WOL_ERR_INVALID_ARGUMENT | 1 | Wol::InvalidArgument |
WOL_ERR_PERMISSION_DENIED | 2 | Wol::PermissionDenied |
WOL_ERR_UNAVAILABLE | 3 | Wol::Unavailable |
WOL_ERR_INTERNAL | 4 | Wol::Internal |
WOPKG_ERR_UNTRUSTED (opt-in) | 5 | Wol::Pkg::Untrusted |
WOPKG_ERR_NOT_FOUND (opt-in) | 6 | Wol::Pkg::NotFound |
All of them are Wol::Error, which carries code : Int32. Wol::UsageError is separate: it means the program is wrong (a released handle, a callback-scoped object used too late, a second dispatch from inside a callback). Its text is for the developer and is never shown to a person. An exception raised inside a callback never reaches C: it goes to Wol::Loop.on_error, which by default writes it to standard error and keeps the loop running.
Threads and blocking calls
wol.h asks for one thread at a time per context. The default Crystal execution context is single-threaded, so an app that stays on it meets the rule without effort. Only the Wol::Loop fiber calls wol_context_dispatch, and callbacks run inside it, so a callback should be short and spawn its work.
Some verbs wait for the person or a peer: Wol::Panel.open and .save (up to ten minutes), Wol::Share.text (until the person chooses) and Wol::Message.send (up to 3 seconds). On the loop's fiber they freeze the window. Run them through Wol.background(ctx.app_id) { |bg| Wol::Panel.open(bg) } (src/wol/background.cr). It runs the block on its own thread with a separate context for the same app (wol.h allows one context per driving thread), and the calling fiber waits for the result without blocking the other fibers. An exception in the block is raised in the caller. Crystal 1.22 has execution contexts with no flag; never build with -Dpreview_mt alone. Every such method says Blocks: in its doc comment.
Needs: toolchain and advisories
A Cryswol program needs the pinned toolchain, crystal >= 1.22.0, and nothing outside the standard library unless the shard is counted. The pin lives in one file, tools/crystal/toolchain.json; the shard's shard.yml, the recipes, CI (tools/crystal/lint.sh) and this page are checked against it (RCR-046). Security advisories for crystal, gc, llvm-libs and for shards go into tools/crystal/advisories.json; a listed version in the snapshot record or in a shard.lock fails the rules until the snapshot moves and the recipes that compile Crystal bump pkgrel (RCR-045). What each standard-library module may be used for (typed decode, timeouts, Random::Secure, argv commands) is the table in docs/RULES-crystal.md, "Standard library and security policy"; the procedures are in tools/crystal/README.md.
Build
The toolchain is pinned: crystal 1.22 from the base distribution's repositories (shard.yml asks for >= 1.22.0, the only version tested). It is a build dependency only (ADR 0036): an app at run time needs gc pcre2 glibc and libwol.
On an installed WolOS, Cryswol needs the Developer module. The base image ships neither crystal, nor libwol.so.1, nor the shard. wol modules install dev (or ticking Developer Tools in the installer, or Settings > Modules) installs wolos-module-dev, which depends on crystal, pkgconf (crystal build asks pkg-config for the libraries it links), libwol and cryswol (wolos-pkgbuilds/wolos-module-dev/PKGBUILD, record files/usr/share/wolos/modules/dev.json; wolos-pkgbuilds/checks/tests/modules.rs keeps them there). After that crystal --version answers, /usr/lib/libwol.so.1 is there and the shard sits under /usr/lib/wolos/crystal/cryswol, so a program builds with CRYSTAL_PATH="/usr/lib/wolos/crystal:$(crystal env CRYSTAL_PATH)" crystal build app.cr. A packaged app built elsewhere needs only libwol (its own depends=), not the module. The VM journey wolos-iso/tests/wolvm/journeys/cryswol-hello.yaml installs the module, builds and runs examples/hello.cr against the live wold as the person, and requires both the notification and the theme event that wol theme light / dark sends (every theme change publishes it on wold, crates/controller/src/run.rs announce_theme).
- The library.
wolos-pkgbuilds/libwol/PKGBUILDbuildscrates/wol-abi(cargo build -p wol-abi --release --locked), linkslibwol.so.1withcrates/wol-abi/link.sh(onlyabi/wol.mapdecides the exports), and installs/usr/lib/libwol.so.1, the/usr/lib/libwol.solink for-lwoland/usr/include/wol.h. Its pkgver is the ABI version (WOL_VERSION_MAJOR.MINOR.PATCH, kept in step bytools/bump-wolos.sh), so an app's package saysdepends=(libwol>=1.14)for the newest verb it calls. An app built against a newer libwol refuses to start on an older one (symbol versions), which is why that dependency matters. The contract test iswolos-pkgbuilds/checks/tests/recipe_libwol.rs. - The shard package.
wolos-pkgbuilds/cryswol/PKGBUILDinstalls the shard with its ownmake installunder/usr/lib/wolos/crystal/cryswol(sorequire "cryswol"works on thatCRYSTAL_PATHentry), depends oncrystalandlibwol, and runsmake fmt-check(the specs need cargo and run in the wolos CI). Its pkgver isshard.yml's version (kept in step bytools/bump-wolos.sh); thewolkitpackage depends on it. The contract test iswolos-pkgbuilds/checks/tests/recipe_cryswol_shard.rs. - The shard in the tree.
make specinwolos-crystal/shards/cryswol/builds libwol intobuild/and runscrystal specagainst the fake wold and Kit host (crates/wol-abi/examples/wol_fake.rs, fed byspec/fixtures/<group>.json), so the specs need no running WolOS.make formatrunscrystal tool format --check. - File layout.
src/cryswol.cr(default entry),src/wol.cr, one BSD file per verb group directly insrc/wol/; the GPL parts in subdirectories:src/wol/pkg/(lib_wopkg.cr,ffi.cr,pkg.cr, viasrc/cryswol/pkg.cr) andsrc/wol/realtime/(lib_realtime.cr, viasrc/cryswol/realtime.cr). Alsospec/withspec/fixtures/, andexamples/hello.cr. - Acceptance.
WOL_BINDINGS_REQUIRED=crystal cargo test -p wol-abi --test bindings --test crystal_shard(everywol.hfunction has a wrapper or a reasoned EXEMPT row; the shard specs andexamples/hello.crrun), plusmake specandmake fmt-checkin the shard, andcargo test -p controller --test rules_crystal. - An app. In a package build:
export CRYSTAL_PATH="<shards dir>:$(crystal env CRYSTAL_PATH)", thencrystal build --release src/app.cr. Noshardsbinary is needed. Out-of-tree projects use apath:dependency on the shard directory.
How to add a verb
A new call in libwol costs its Crystal wrapper in the same change (ADR 0036).
- Add the declaration to
crates/wol-abi/include/wol.hand the symbol to a new node ofcrates/wol-abi/abi/wol.map(docs/modules/abi.md, ABI rules). - Run
WOL_BLESS=1 cargo test -p wol-abi --test bindings. It regenerates both copies oflib_wol.cr. - Write the wrapper in the shard file of its group (
src/wol/<group>.cr): a doc comment on every public method; strings in throughWol::Strings.ptr/.size; the status throughWol.call,Wol.outorWol::Info.copy; out-parameters to Crystal values ornil;# Blocks:when it waits. A verb file holds noPointer,pointerof,LibCorto_unsafe(RCR-010): the raw memory stays in the core files on the FFI allowlist (crates/controller/tests/rules_crystal.rs,FFI_ALLOW). - Add the answer the fake wold gives to
spec/fixtures/<group>.jsonand a spec tospec/<group>_spec.cr. Assert the call wold received (from the fake's log) as well as what the method returned. - Run
make spec formatin the shard, thencargo test -p wol-abi --test bindings. The coverage check fails while a function of the header has no wrapper. - Add the call to
servicesin each language (crates/wol-abi/tests/common/services.rschecks them all).
Related
- libwol: the app APIOne stable C ABI for apps in any language: windows, menus, notifications, files, power and more.
- 0036: Crystal is the userland languageOne language for apps, tools and integrations, with generated bindings over the stable C ABI.
- Crystal rules (RCR)The rules every Crystal file in WolOS follows, the standard library policy, the pinned toolchain and the advisories list.
- Plugin API referenceThe complete manifest, permissions and Lua API for shell plugins.
- Crystal shards and wol shardOne signed list of Crystal libraries, each pinned to one commit, and the commands that write the pins and the lock for you.
From the developer documentation, wonderlandos/wolos docs/modules/cryswol.md.