Creating a capsule
Create a small program and run it on your development node. Start with a greeting, then try a word counter or shipping calculator. Each accepts typed input and returns either an answer or a helpful application error.
Complete Get the developer tools and
Set up your workspace first. Keep its terminal
open: it defines dev, your selected $Language, $Workspace, $Project and
other tool paths. The steps below use those same variables on both operating systems.
Select Rust, C, TypeScript, Go, Java or C# / .NET above a code example. Your selection is shared across the examples. The build, deploy and test commands work the same way for every language; use the language you installed during setup.
1. A greeting capsule
The first program accepts a name and returns a greeting. An empty name produces a helpful error instead. This is the complete implementation in your selected language:
- Rust
- TypeScript
- Go
- C
- Java
- C#/.NET
pub fn greet(name: String) -> Result<String, String> {
let name = name.trim();
if name.is_empty() {
return Err("Please enter a name.".into());
}
if name.len() > 100 {
return Err("Use a name of at most 100 bytes.".into());
}
Ok(format!("Hello, {name}!"))
}
#[cfg(target_arch = "wasm32")]
mod component {
wit_bindgen::generate!({path: "examples/tutorial_greeting", world: "service"});
struct Capsule;
impl exports::examples::greeting::api::Guest for Capsule {
fn greet(name: String) -> Result<String, String> {
super::greet(name)
}
}
export!(Capsule);
}
export const api: typeof Contract = {
greet(name) {
const clean = trim(name);
if (!clean) throw 'Please enter a name.';
if (utf8Length(clean) > 100) throw 'Use a name of at most 100 bytes.';
return `Hello, ${clean}!`;
},
};
package export_examples_greeting_api
import (
wit "go.bytecodealliance.org/pkg/wit/types"
"strings"
)
func Greet(name string) wit.Result[string, string] {
name = strings.TrimSpace(name)
if len(name) == 0 {
return wit.Err[string, string]("Please enter a name.")
}
if len(name) > 100 {
return wit.Err[string, string]("Use a name of at most 100 bytes.")
}
return wit.Ok[string, string]("Hello, " + name + "!")
}
#include "lsf/text.h"
bool exports_examples_greeting_api_greet(probe_string_t *name, probe_string_t *value,
probe_string_t *error) {
size_t first = name->len, end = 0, offset = 0;
while (offset < name->len) {
size_t start = offset;
if (!lsf_text_whitespace(lsf_text_next(name, &offset))) {
if (first == name->len) first = start;
end = offset;
}
}
const char *failure = first == name->len ? "Please enter a name." :
end - first > 100 ? "Use a name of at most 100 bytes." : NULL;
if (failure) {
probe_string_free(name);
lsf_text_error(error, failure);
return false;
}
lsf_scope_t scope;
lsf_scope_init(&scope, 128);
size_t length = end - first;
uint8_t *bytes = lsf_scope_alloc(&scope, length + 8, 1);
lsf_require(bytes != NULL);
memcpy(bytes, "Hello, ", 7);
memcpy(bytes + 7, name->ptr + first, length);
bytes[length + 7] = '!';
probe_string_free(name);
*value = (probe_string_t){bytes, length + 8};
lsf_string_return(&scope, value);
lsf_scope_close(&scope);
return true;
}
public final class Capsule implements Bindings.Exports {
public Result<String, String> greet(String name) {
String trimmed = Text.trim(name);
if (trimmed.isEmpty()) return Result.err("Please enter a name.");
if (Text.utf8Length(trimmed) > 100) return Result.err("Use a name of at most 100 bytes.");
return Result.ok("Hello, " + trimmed + "!");
}
}
public class ApiExportsImpl : IApiExports
{
public static Result<string, string> Greet(string name)
{
string clean = Text.Trim(name);
if (clean.Length == 0) return Result<string, string>.Err("Please enter a name.");
if (Text.Utf8Length(clean) > 100) return Result<string, string>.Err("Use a name of at most 100 bytes.");
return Result<string, string>.Ok($"Hello, {clean}!");
}
}
The WIT contract describes the input and the two possible kinds of answer:
greet: func(name: string) -> result<string, string>;
You will find this contract under app/wit in your project. Generated bindings
connect it to your language's function. For now, keep the contract unchanged.
2. Create the project
Choose the greeting template. Setup has already selected its verified bundle
and template settings. Run this same command on Windows or Linux:
dev init "$Project" --bundle "$Bundle" --template "$Language/greeting" --template-sha256 "$GreetingTemplate"
Open the project in your editor:
| File or folder | What you do with it |
|---|---|
app | Edit the source and its WIT contract |
latent.project.json | Review the compiler, build recipe and source files it can use |
tests/scenarios.json | Read the named test cases |
tests/*-input.json, tests/*-expected.json | See each input and its expected answer |
3. Build and prepare the node
After reviewing the project, run these commands in the same terminal on either OS:
dev trust --workspace "$Workspace" --project "$Project"
dev build --workspace "$Workspace" --project "$Project"
dev prepare-test --workspace "$Workspace" --consent-test-fixtures --admission "$Admission"
Expect code: success from each command. The compiler comes from the selected
tool bundle. prepare-test prepares the stopped disposable node to accept this
application. Setup selected trusted-local for Rust/C or signed-fixture for the
other languages, including their required runtime permissions.
A signed fixture lasts 30 minutes and binds one build. If you edit that build or the fixture expires, create a fresh disposable workspace before testing it. Use the edit guide for the supported watch loop.
4. Start, deploy and test
Open a second terminal and start the node. Keep this command running. For the default paths from setup, use the following command; substitute your workspace name if you chose another one.
Windows (PowerShell):
& (Join-Path $env:USERPROFILE 'LSF-inputs-alpha5/frontend/bin/latent-dev.exe') `
--state-root (Join-Path $env:LOCALAPPDATA 'LatentDev-tutorial') dev up --workspace test-my-greeting
Linux (Bash):
"$HOME/LSF-inputs-alpha5/frontend/bin/latent-dev" --state-root "$HOME/.latent-dev-tutorial" \
dev up --workspace test-my-greeting
Wait for the ready event. In your first terminal, publish the accepted build
and run its tests with the same commands on either OS:
dev deploy --workspace "$Workspace"
dev test --workspace "$Workspace" --environment node
Expect passed: true and every required case to report status: passed.
Ada returns Hello, Ada!; an empty name produces the expected application error.
Some language profiles include additional runtime cases, so their case counts differ.
5. Send a request yourself
The greeting's first input file contains ["Ada"]. Call it directly:
dev invoke --workspace "$Workspace" --service examples/my-greeting --contract examples:greeting/api@1.0.0 --function greet --input "$Project/tests/0-input.json"
The response includes category: success, the selected deployment and an encoded
payload. To display the typed answer, on Windows:
$Reply = (dev invoke --workspace $Workspace --service examples/my-greeting `
--contract examples:greeting/api@1.0.0 --function greet `
--input (Join-Path $Project 'tests/0-input.json') | ConvertFrom-Json).result
[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Reply.data.payload.data))
Or on Linux:
dev invoke --workspace "$Workspace" --service examples/my-greeting --contract examples:greeting/api@1.0.0 --function greet --input "$Project/tests/0-input.json" \
| python3 -c 'import base64,json,sys; print(base64.b64decode(json.load(sys.stdin)["result"]["data"]["payload"]["data"]).decode())'
Expected: [{"ok":"Hello, Ada!"}]. These are independent example calls. If a
response is lost, use dev recover --workspace "$Workspace" to inspect that
original call before making another one.
6. Try a word counter
This program counts groups separated by spaces, tabs or newlines. An empty document has zero words. Very long input produces a readable error.
- Rust
- TypeScript
- Go
- C
- Java
- C#/.NET
pub fn count(text: &str) -> Result<u32, String> {
if text.len() > 4096 {
return Err("Use text of at most 4096 bytes.".into());
}
u32::try_from(text.split_whitespace().count()).map_err(|_| "Too many words.".into())
}
#[cfg(target_arch = "wasm32")]
mod component {
wit_bindgen::generate!({path: "examples/tutorial_word_count", world: "service"});
struct Capsule;
impl exports::examples::word_count::api::Guest for Capsule {
fn count(text: String) -> Result<u32, String> {
super::count(&text)
}
}
export!(Capsule);
}
export const api: typeof Contract = {
count(text) {
if (utf8Length(text) > 4096) throw 'Use text of at most 4096 bytes.';
let count = 0, inWord = false;
for (const scalar of text) {
const word = !whitespace(scalar);
if (word && !inWord) count++;
inWord = word;
}
return count;
},
};
package export_examples_word_count_api
import (
wit "go.bytecodealliance.org/pkg/wit/types"
"strings"
)
func Count(text string) wit.Result[uint32, string] {
if len(text) > 4096 {
return wit.Err[uint32, string]("Use text of at most 4096 bytes.")
}
return wit.Ok[uint32, string](uint32(len(strings.Fields(text))))
}
#include "lsf/text.h"
bool exports_examples_word_count_api_count(probe_string_t *text, uint32_t *value,
probe_string_t *error) {
if (text->len > 4096) {
probe_string_free(text);
lsf_text_error(error, "Use text of at most 4096 bytes.");
return false;
}
uint32_t count = 0;
bool in_word = false;
size_t offset = 0;
while (offset < text->len) {
bool word = !lsf_text_whitespace(lsf_text_next(text, &offset));
if (word && !in_word) ++count;
in_word = word;
}
probe_string_free(text);
*value = count;
return true;
}
public final class Capsule implements Bindings.Exports {
public Result<Long, String> count(String text) {
if (Text.utf8Length(text) > 4096) return Result.err("Use text of at most 4096 bytes.");
long count = 0;
boolean inside = false;
for (int offset = 0; offset < text.length();) {
int character = text.codePointAt(offset);
boolean word = !Text.whitespace(character);
if (word && !inside) count++;
inside = word;
offset += Character.charCount(character);
}
return Result.ok(count);
}
}
public class ApiExportsImpl : IApiExports
{
public static Result<uint, string> Count(string text)
{
if (Text.Utf8Length(text) > 4096) return Result<uint, string>.Err("Use text of at most 4096 bytes.");
uint count = 0;
bool inWord = false;
foreach (char scalar in text) {
bool word = !Text.Whitespace(scalar);
if (word && !inWord) ++count;
inWord = word;
}
return Result<uint, string>.Ok(count);
}
}
count: func(text: string) -> result<u32, string>;
Stop the greeting with dev down --workspace "$Workspace". Create another
workspace and project using the workspace setup,
for example test-my-words and My words. Reuse your downloads and existing WSL
distro. Create the project with this command, then follow the same build, start,
deploy and test steps with that workspace name:
dev init "$Project" --bundle "$Bundle" --template "$Language/word-count" --template-sha256 "$WordCountTemplate"
The tests send LSF runs small programs and expect 4. Look in
tests/scenarios.json for the service, contract and function to use in a direct call.
7. Try a shipping calculator
This program accepts an item count and whether delivery is express. It returns a price in cents: 500 for standard delivery or 1200 for express, plus 75 per item.
- Rust
- TypeScript
- Go
- C
- Java
- C#/.NET
pub fn quote(items: u32, express: bool) -> Result<u32, String> {
if !(1..=100).contains(&items) {
return Err("Choose between 1 and 100 items.".into());
}
let base_cents = if express { 1200 } else { 500 };
Ok(base_cents + items * 75)
}
#[cfg(target_arch = "wasm32")]
mod component {
wit_bindgen::generate!({path: "examples/tutorial_shipping", world: "service"});
struct Capsule;
impl exports::examples::shipping::api::Guest for Capsule {
fn quote(items: u32, express: bool) -> Result<u32, String> {
super::quote(items, express)
}
}
export!(Capsule);
}
export const api: typeof Contract = {
quote(items, express) {
if (items < 1 || items > 100) throw 'Choose between 1 and 100 items.';
return (express ? 1200 : 500) + items * 75;
},
};
package export_examples_shipping_api
import wit "go.bytecodealliance.org/pkg/wit/types"
func Quote(items uint32, express bool) wit.Result[uint32, string] {
if items < 1 || items > 100 {
return wit.Err[uint32, string]("Choose between 1 and 100 items.")
}
base := uint32(500)
if express {
base = 1200
}
return wit.Ok[uint32, string](base + items*75)
}
#include "lsf/text.h"
bool exports_examples_shipping_api_quote(uint32_t items, bool express, uint32_t *value,
probe_string_t *error) {
if (items < 1 || items > 100) {
lsf_text_error(error, "Choose between 1 and 100 items.");
return false;
}
*value = (express ? 1200u : 500u) + items * 75u;
return true;
}
public final class Capsule implements Bindings.Exports {
public Result<Long, String> quote(Long items, Boolean express) {
if (items < 1 || items > 100) return Result.err("Choose between 1 and 100 items.");
return Result.ok((express ? 1200L : 500L) + items * 75L);
}
}
public class ApiExportsImpl : IApiExports
{
public static Result<uint, string> Quote(uint items, bool express)
{
if (items < 1 || items > 100) return Result<uint, string>.Err("Choose between 1 and 100 items.");
return Result<uint, string>.Ok((express ? 1200U : 500U) + items * 75U);
}
}
quote: func(items: u32, express: bool) -> result<u32, string>;
Use another project and workspace, then select the shipping template:
dev init "$Project" --bundle "$Bundle" --template "$Language/shipping" --template-sha256 "$ShippingTemplate"
Run the same build, start, deploy and test commands. Two items with standard delivery produce
650; express delivery produces 1350. Zero items produce
Choose between 1 and 100 items. as a declared application error.
The three programs need no network, files or secrets. Their compilers can still require explicitly granted runtime clocks or entropy. To add application access to an outside service, continue with capabilities.
8. Clean up
Stop the selected node and inspect its state:
dev down --workspace "$Workspace"
dev status --workspace "$Workspace"
Expect state: stopped. Starting up again retains the deployment without another
deploy command. Keep this workspace if you are continuing with editing and recovery.
When you no longer need its node data, remove that workspace explicitly:
dev purge --workspace "$Workspace" --confirm-workspace "$Workspace"
Your project source remains. Repeat cleanup for each workspace you created. The developer command guide also explains when to remove the shared LSF-owned WSL distro.