Language Binding Specification

Pkl can be embedded within any host application. The host application has access to low level controls. It is able to manage the lifecycle of evaluators, as well as provide custom modules and resources to Pkl.

Currently, Pkl must be embedded as a child process, by shelling out to the CLI using the pkl server command. In the future, a C library will also be provided.

When embedded, communication between a host application and Pkl happens via message passing. The message passing specification can be found in Message Passing API.

For examples of language bindings in practice, review the pkl-go, or the pkl-swift codebases.

Pkl’s Java and Kotlin libraries binds to Pkl directly, and do not use message passing.

Features of a language binding

A language binding for Pkl should generally have the following components:

  1. A client that spawns pkl server, and talks to it using message passing.

  2. A deserializer that turns pkl binary encoding into a structure in the host language.

  3. A code generator that transforms Pkl schemas into schemas written in the host language. The code generator is mostly written in Pkl, with a lightweight executable that acts as the glue layer. For examples of code generators, consult pkl-go and pkl-swift.

Sample flow

Here is a sample flow for evaluating a module with the following contents:

file:///path/to/myModule.pkl
module MyModule

theModules = import*("customfs:/*.pkl")
  1. Client sends Create Evaluator Request, including customfs as a custom module reader.

    [
      0x20,
      {
        "requestId": 135,
        "allowedModules": ["pkl:", "repl:", "file:", "customfs:"],
        "clientModuleReaders": [
          {
            "scheme": "customfs",
            "hasHierarchicalUris": true,
            "isGlobbable": true,
            "isLocal": true
          }
        ]
      }
    ]
  2. Server sends Create Evaluator Response, with an evaluator id.

    [
      0x21,
      {
        "requestId": 135,
        "evaluatorId": -135901
      }
    ]
  3. Client sends Evaluate Request, providing the module’s URI.

    [
      0x23,
      {
        "requestId": 9805131,
        "evaluatorId": -13901,
        "moduleUri": "file:///path/to/myModule.pkl"
      }
    ]
  4. During evaluation, server evaluates import*("customfs:/*.pkl"), and sends List Modules Request.

    [
      0x2c,
      {
        "requestId": -6478924,
        "evaluatorId": -13901,
        "uri": "customfs:/"
      }
    ]
  5. Client responds with List Modules Response. In our pretend scenario, there is only one file; foo.pkl.

    [
      0x2d,
      {
        "requestId": -6478924,
        "evaluatorId": -13901,
        "pathElements": [
          {
            "name": "foo.pkl",
            "isDirectory": false
          }
        ]
      }
    ]
  6. Server sends Read Module Request to read foo.pkl.

    [
      0x28,
      {
        "requestId": 36408291,
        "evaluatorId": -13901,
        "uri": "customfs:/foo.pkl"
      }
    ]
  7. Client responds with the module’s contents

    [
      0x29,
      {
        "requestId": 36408291,
        "evaluatorId": -13901,
        "contents": "foo = 1"
      }
    ]
  8. Server finishes evaluation, and responds with the Evaluate Response.

    [
      0x24,
      {
        "requestId": 9805131,
        "evaluatorId": -13901,
        "result": <pkl binary value>
      }
    ]
  9. Client sends Close Evaluator.

    [
      0x22,
      {
        "evaluatorId": -13901
      }
    ]

Debug logs

Set the env var PKL_DEBUG=1 to enable more verbose logging from Pkl. It is recommended that clients also use this environment variable to enable debug logs of their own.