Supplying application modules from Java (`app:`)
Canonical source:
docs/guide/embedding/application-modules.mdat locked revisione99b712ecf658a527b1fe82c8beba2c1286d8dc9. This rendered page is non-normative; the applicable Protos specification remains authoritative.
Status: Non-normative HOWTO
Normative owners:
spec/semantics/MODULES.md,spec/concurrency/ACTORS.md(§8Actor.spawn), andspec/io/PROCESS_IO.md(Standard Polyglot embedding bootstrap and authority)
Purpose
Section titled “Purpose”A Java host often needs to ship Protos modules of its own: code that the main evaluation imports, or that a spawned Actor runs. In a standard Polyglot Context there is no filesystem or classpath search for such modules. The host supplies them explicitly through:
com.guillermomolina.protos.execution.ProtosEmbeddedModules .install(Context context, Map<String, String> modules)Each entry maps an exact app: specifier to the Protos source characters of
that module. The Protos program then reaches it through ordinary
import("app:...") or Actor.spawn("app:...", ...).
This page extends Embedding Protos in a Java application; read that first for the Context lifecycle, class path, and authority model.
Availability
Section titled “Availability”| Capability | State |
|---|---|
ProtosEmbeddedModules.install with import("app:...") |
Implemented and accepted (I087-2 / PLAT055) on the portable JVM runtime |
Actor.spawn("app:...", ...) |
Implemented and accepted (same slice) |
Filesystem/classpath lookup, relative app: imports, aliases |
Not available by design |
| Native Image embedding hosts / Native CLI | Not supported |
app: in the CLI, Test Tool, or other Protos-owned drivers |
Not available: installation is rejected on driver-owned Contexts |
Verified scope: this repository’s 0.3.312-SNAPSHOT development revision; no
release containing it is implied.
Example
Section titled “Example”import com.guillermomolina.protos.execution.ProtosEmbeddedModules;import java.util.Map;import org.graalvm.polyglot.Context;
public final class AppModules { private static final String WORKER = """ greeting: "hello" start: (initial) => { count: initial { increment: () => { count = count + 1 count } } } """;
public static void main(String[] args) { try (Context context = Context.newBuilder("protos").allowCreateThread(true).build()) { // 1. Install once, after build and before the first evaluation. ProtosEmbeddedModules.install(context, Map.of("app:worker", WORKER));
// 2. Ordinary import. String greeting = context.eval("protos", "import(\"app:worker\").greeting").asString();
// 3. The same module as the initial module of a new Actor. int count = context.eval("protos", "w: Actor.spawn(\"app:worker\", \"start\", 41)\n" + "w.request(\"increment\").value()") .asInt();
System.out.println(greeting + " " + count); } }}Build and run it with the class path from the embedding page.
Expected output: hello 42.
This is the same scenario that the portable smoke runs from an external
application against the distributed JAR (application-modules mode of
dist/Plat054EmbeddingProbe.java).
allowCreateThread(true) is needed only for the Actor.spawn part; plain
imports work without it.
Rules of the catalog
Section titled “Rules of the catalog”Exact specifiers. A key must be app: followed by a non-empty name. The
key is used verbatim as the canonical ModuleKey: no normalization, no extension
inference, no case folding, no relative resolution. import("app:worker")
finds the entry; import("app:worker.protos"), import("worker"), and
import("APP:worker") do not.
Ordinary module semantics. Catalog modules follow chapter 6 unchanged:
- within one Actor, every
import("app:x")returns the same cached instance and the body runs once; - two entries with identical source are still two modules
(
import("app:a") !== import("app:b")); - cyclic imports observe the real, partially initialized module;
- a failed initialization is an ordinary Protos Error, the cache entry is removed, and a later import retries with a fresh instance;
- each Actor has its own instances: a spawned Actor initializes its own copy
of
app:worker, and the RootActor’s copy is independent of it.
Lazy loading. A module is compiled only when first imported. An entry with a syntax error has no effect until something imports it.
Host evaluations never become catalog modules. Calling
context.eval(Source.newBuilder("protos", code, "app:worker").build()) does
not evaluate or replace the catalog module named app:worker: Source names,
URIs, and content never create a ModuleKey. Every host evaluation stays a
standalone entry.
Other domains are unaffected. std: modules still resolve from the
Standard Library alongside the catalog. A specifier that is not in the catalog,
including an unknown app: one, fails as an ordinary resolution Error.
Installation lifecycle
Section titled “Installation lifecycle”Install exactly once per Context, after building it and before its first evaluation:
Context.newBuilder("protos").build() context.initialize("protos") allowed before install context.getBindings("protos") allowed before install context.parse("protos", ...) allowed before installProtosEmbeddedModules.install(...) oncecontext.eval(...) first evaluation starts the Process- The
modulesmap is validated and copied. Later changes to your map have no effect, and an invalid entry rejects the whole installation: nothing is published. - An empty map is a valid installation (and still counts as the one installation).
- Installation does not create the Process.
- Installation and the first evaluation are atomic with respect to each other:
if they race on two threads, either the bootstrap sees the whole catalog or
installfails withIllegalStateException. - Each Context has its own catalog, even when several Contexts share one
Engine. A Context without a catalog cannot import another Context’sapp:modules.
Errors from install
Section titled “Errors from install”| Exception | When |
|---|---|
NullPointerException |
context, modules, a key, or a source is null |
IllegalArgumentException |
A key is not app: + non-empty name (for example app:, std:..., java:x, app:x), or the Context does not support Protos |
IllegalStateException |
The Context is closed, already has a catalog, has started (or failed) its Process bootstrap, or is owned by a Protos host driver |
Failures inside Protos (unknown specifier, failing module body) are ordinary
Protos Errors seen by the guest, not Java exceptions from install. If such an
Error escapes a host entry unhandled, it terminates the Process as described in
the embedding page.
Authority
Section titled “Authority”Supplying code grants no authority. A catalog module:
- does not receive
process,filesystem, ornetworkslots; only the initial module of the first host evaluation receives the bootstrap-local slots; - cannot reach Filesystem, Network, host access, or thread creation beyond what the Context itself allows;
- receives capabilities only when Protos code passes them explicitly, for example as an argument to a function the module exports.
References
Section titled “References”Implementation and evidence:
ProtosEmbeddedModules.java(public API and Javadoc);ProtosApplicationModuleResolver.java(exact catalog lookup in front of the Standard Library resolver);ProtosEmbeddedModulesTest.java(identity, cycles, retry, Actor-local instances, installation order, atomicity, shared Engines, authority);dist/Plat054EmbeddingProbe.java(external application,application-modulesmode).
Normative specification:
spec/semantics/MODULES.mdfor ModuleKey identity, Actor-local caching, cycles, failure, and host-initiated evaluations;spec/concurrency/ACTORS.md§8 forActor.spawnand the initial module of an Actor;spec/io/PROCESS_IO.mdfor embedding bootstrap and authority.
The app: spelling itself is host resolver policy of this embedding API, not a
Core language institution (see
chapter 6).