Contents

cleancocoa/swift-janet

Janet embedded in Swift via SwiftPM.

Installation

Add the package to your Package.swift:

dependencies: [
    .package(url: "https://github.com/CleanCocoa/swift-janet", from: "0.1.0"),
],
targets: [
    .target(name: "MyApp", dependencies: [.product(name: "Janet", package: "swift-janet")]),
]

Or in Xcode, File > Add Package Dependencies and paste the repository URL.

Usage

The Janet module offers two runtimes. Pick by where the call happens:

| | JanetRuntime | JanetRuntime.main | |---|---|---| | Runs on | its own dedicated thread | the main thread | | Call style | try await from anywhere | try from @MainActor code | | Instances | as many as you like, each an isolated VM | exactly one per process | | Good for | background scripts, parallel evaluation | UI that scripts each keypress | | Cost | a thread hop per call, about 5 to 7 µs | none, but the main thread blocks |

Both share the same API: eval runs source and returns the last value, define binds a Swift value under a name for later evaluations.

Background: JanetRuntime

import Janet

let janet = JanetRuntime()
try await janet.define("greeting", .string("hello"))
let result = try await janet.eval("(string greeting \", world\")")
// result == .string("hello, world")

Each instance owns one VM on one thread and is Sendable. Drop the last reference to tear both down.

Main thread: JanetRuntime.main

@MainActor func run() throws {
    try JanetRuntime.main.define("greeting", .string("hello"))
    let result = try JanetRuntime.main.eval("(string greeting \", world\")")
    // result == .string("hello, world")
}

Calls are synchronous, so nothing else on the main actor can interleave between a call and its result. State lives for the process; JanetRuntime.main.reset() discards every definition.

Values and errors

JanetValue is a Swift-owned copy of a Janet value: nil, booleans, numbers, strings, keywords, symbols, tuples, arrays, structs and tables. Anything else (functions, fibers, buffers, abstracts) arrives as .unsupported(typeName:).

JanetError carries Janet's message and the phase that failed: .parse, .compile, .runtime, or .copy when a value could not cross the boundary.

Caveats

  • At most one VM may be live per thread: creating a second one there, or touching a VM

from another thread, traps. JanetRuntime.main owns the main thread's VM, so do not put another one there.

  • Janet prints error diagnostics to stderr before eval throws.
  • Values are deep-copied across the boundary in both directions. A result nested deeper

than 256 levels, or containing a cycle through an array or table, makes eval throw with phase .copy instead of overflowing the stack of whichever thread releases it.

Learning Janet

Benchmarking

Benchmark/run.sh measures what the Swift layers add over the raw C API; see Benchmark/README.md for the method and reference numbers.

Updating Janet

Scripts/vendor-janet.sh v1.42.1

This clones the tag, builds the amalgamation with Janet's own Makefile, and copies janet.c, janet.h, LICENSE and a VERSION marker into Sources/CJanet.

Checking for leaks

Scripts/leaks.sh [--filter <test-or-suite>] [--graph <path.memgraph>] runs the test bundle under macOS leaks -atExit and reports whether released JanetRuntime instances left the Janet VM or executor thread behind.

License

MIT; see LICENSE. The vendored Janet sources are also MIT licensed; see Sources/CJanet/LICENSE.

Package Metadata

Repository: cleancocoa/swift-janet

Default branch: main

README: README.md