In this section

This guide shows how to add a language to lungo generate with a plugin, the way protoc-gen-<language> programs add languages to protoc.

Write the program

A plugin reads a GenerateRequest (JSON) on standard input and writes a GenerateResponse (JSON) on standard output. The protocol reference lists every field. In Rust, the lungo-build crate provides the types:

use lungo_build::codegen::plugin::{GenerateRequest, GenerateResponse};
use std::io::Read;

fn main() {
    let mut input = String::new();
    std::io::stdin().read_to_string(&mut input).unwrap();
    let request: GenerateRequest = serde_json::from_str(&input).unwrap();
    let mut response = GenerateResponse::default();
    for key in request.options.keys() {
        response.errors.push(format!("unknown option `{key}`"));
    }
    if response.errors.is_empty() {
        let mut api = String::new();
        for f in &request.boundary.functions {
            api.push_str(&format!("// {} : {}\n// entry point: {}\n", f.lean_name, f.lean_type, f.symbol));
        }
        response.files.insert("api.txt".into(), api);
        // The program's C, which the package compiles and links against the runtime.
        response.files.extend(request.program_files.clone());
    }
    println!("{}", serde_json::to_string(&response).unwrap());
}

A generator for a real language:

  1. copies program_files: the package compiles every .c file with program/ on its include path, and links the runtime of request.runtime (by release URL and SHA-256, or from the local distribution);
  2. emits the language’s types for boundary.table and boundary.types, and a function per entry of boundary.functions that encodes its arguments in the wire format, calls the entry point symbol, and decodes the result per returns;
  3. installs itself as the process’s host (lungo_set_host) to receive calls of host functions, gives each facility of boundary.facilities an installer that registers the implementation of each of its operations with set_host_extern (by the operation’s index), and refuses a call before every facility is installed, naming the facility and an operation;
  4. for a function whose returns is async, calls the entry point for the program’s first step and drives it: asks the caller’s handler to perform each operation (of the async facility in boundary.async_facilities with that operation type), resumes the program with the answer (lungo_async_resume), and gives it up (lungo_async_cancel) when the handler fails or the call is cancelled (see the wire format);
  5. if the language can expose it, embeds the program’s assurance document (request.assurance) exactly as lungo serializes it. lungo writes assurance.json into every output itself; a plugin whose response contains that file fails.

The support libraries of the built-in languages (lungo-go, lungo-py, lungo-ts and lungo-swift) are complete examples of the runtime side, and compiler-tests/wire/vectors.json in lungo-rs tests a codec.

Install and run it

Put the program on PATH as lungo-gen-<language> (.exe on Windows):

cargo install --path my-plugin   # installs lungo-gen-kotlin
lungo generate --kotlin_out=gen/kotlin --kotlin_opt=package=org.example

or configure it in lungo.toml:

[plugins.kotlin]
out = "gen/kotlin"
options = { package = "org.example" }

The output directory is replaced by the response’s files, with assurance.json added. A plugin that fails, writes a malformed response or reports errors fails the command with LNG0504, showing its standard error or its errors; nothing is written.