How to Extend KCL Configurations with Custom Functions or External Plugins

KCL configurations can be extended by importing built-in plugin modules that load native shared libraries via the kcl_plugin. prefix, or by writing reusable functions directly in .k files, with the runtime enabling plugins through the load_plugins flag and C-ABI entry points kcl_plugin_init and kcl_plugin_invoke_json.

The KCL programming language, maintained in the kcl-lang/kcl repository, provides a robust extension system that allows developers to enhance configurations with custom logic. Whether you need to integrate external cryptographic libraries or reuse validation logic across projects, understanding how to extend KCL configurations with custom functions or external plugins unlocks the full potential of the language.

Understanding KCL Extension Mechanisms

KCL supports two complementary approaches for extending configuration capabilities, each serving different use cases and implementation requirements.

Built-in Plugin Modules (Native Shared Libraries)

The plugin module mechanism enables KCL to load native shared libraries at runtime through a C-ABI interface. This approach is implemented in crates/runtime/src/stdlib/plugin.rs and allows developers to expose functions written in languages like Rust, C, or Go to KCL code. The system uses a global dispatcher stored in PLUGIN_HANDLER_FN_PTR, a Mutex<Option<...>> that holds the function pointer registered by kcl_plugin_init.

Custom Functions Written in KCL

For logic that does not require native performance or external system access, developers can define custom functions directly in .k files. These functions are parsed and evaluated by the KCL runtime like any other code, residing in the project's source tree rather than external shared libraries. The function evaluation logic is handled in crates/evaluator/src/func.rs, where the runtime manages function scopes and argument passing.

Architecture of the KCL Plugin System

The plugin architecture spans multiple crates in the KCL codebase, coordinating compile-time resolution with runtime execution.

Parser and Resolver Integration

When a KCL program is parsed, the load_plugins flag (defaulting to false) is stored in ParserOptions within crates/parser/src/lib.rs (lines 265-283). If a file imports a module whose name starts with kcl_plugin., the parser checks this flag; otherwise, it aborts with an error.

The semantic resolver treats any import beginning with kcl_plugin. as a plugin package, using the constant PLUGIN_MODULE_PREFIX defined in crates/sema/src/plugin/mod.rs. This prefix convention ensures that the compiler can distinguish between standard library modules and external plugin modules.

Runtime Plugin Handler

The shared library must expose two specific C-ABI entry points:

  1. kcl_plugin_init – Registers the function dispatcher, a pointer to a function that receives method, args_json, and kwargs_json parameters.
  2. kcl_plugin_invoke_json – Called by the KCL runtime to forward method calls to the dispatcher, implemented in crates/runtime/src/stdlib/plugin.rs (lines 11-20).

Invocation Flow

When KCL evaluates import kcl_plugin.my_pkg and subsequently calls kcl_plugin.my_pkg.my_func(args…), the evaluator performs a two-stage lookup:

  1. Direct registration check – The evaluator looks up my_func in the runtime context's plugin_functions map (see crates/api/src/kcl.rs, lines 370-371). If present, the function is called directly without JSON marshalling overhead.
  2. JSON fallback – If not found in the direct map, the evaluator falls back to kcl_plugin_invoke_json. The handler receives the fully-qualified method name, marshals arguments to JSON, and returns a JSON result that the runtime converts back to a KCL value (see crates/runtime/src/stdlib/plugin.rs, lines 68-84).

Creating a Custom KCL Plugin in Rust

Below is a complete workflow for building a native plugin that exposes a say_hello(name: str) -> str function to KCL configurations.

Step 1: Implement the Plugin Library

Create a Rust library that exposes the required C-ABI entry points and registers your custom logic:

use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use kcl_runtime::{PluginFunction, Value, Context};

/// The actual function implementation called from KCL.
fn say_hello(_ctx: &mut Context, args: &Value, _kwargs: &Value) -> kcl_runtime::Result<Value> {
    // Extract the first positional argument as a string.
    let name = args.list_get(0)?.str()?.to_owned();
    Ok(Value::str(&format!("Hello, {}!", name)))
}

/// C-ABI entry point: initializes the plugin and returns a function map.
#[no_mangle]
pub extern "C-unwind" fn plugin_init() -> *mut std::collections::HashMap<String, PluginFunction> {
    let mut map = std::collections::HashMap::new();
    // Register with the fully qualified short name.
    map.insert("my_pkg.say_hello".to_string(), 
               Box::new(say_hello) as PluginFunction);
    Box::into_raw(Box::new(map))
}

/// Required C-ABI entry point for the KCL runtime dispatcher.
#[no_mangle]
pub unsafe extern "C-unwind" fn kcl_plugin_init(
    fn_ptr: extern "C-unwind" fn(
        method: *const c_char,
        args_json: *const c_char,
        kwargs_json: *const c_char,
    ) -> *const c_char,
) {
    unsafe { kcl_plugin::kcl_plugin_init(fn_ptr) };
}

Step 2: Compile the Shared Library

Build the project as a dynamic library compatible with your target platform:

cargo build --release --lib --crate-type cdylib

This produces libmy_plugin.so on Linux, libmy_plugin.dylib on macOS, or my_plugin.dll on Windows.

Step 3: Load and Execute from the CLI

Use the --plugin-agent flag to provide a raw pointer to the plugin's initialization function. The kcl run command automatically sets opts.load_plugins = true when a non-zero agent pointer is supplied, as implemented in crates/runner/src/runner.rs (lines 165-170):


# Obtain the function pointer (example using Python ctypes for demonstration)

PLUGIN_PTR=$(python3 -c "
import ctypes
lib = ctypes.CDLL('./target/release/libmy_plugin.so')
print(ctypes.cast(lib.plugin_init, ctypes.c_void_p).value)
")

# Execute the KCL file with the plugin loaded

kcl run ./config.k --plugin-agent $PLUGIN_PTR

Step 4: Consume the Plugin in KCL

Create a KCL configuration file that imports the plugin module using the kcl_plugin. prefix and calls the exposed function:

import kcl_plugin.my_pkg

# Call the native function with a string argument

greeting = kcl_plugin.my_pkg.say_hello("Configuration World")

# Output the result

print(greeting)  # => "Hello, Configuration World!"

When the KCL evaluator processes this file, it resolves the kcl_plugin.my_pkg import through the runtime's plugin_functions map or falls back to the JSON-based invocation handler defined in crates/runtime/src/stdlib/plugin.rs.

Summary

Extending KCL configurations with custom functions or external plugins involves understanding the dual architecture of the extension system:

  • Native plugins require compiling shared libraries that expose kcl_plugin_init and kcl_plugin_invoke_json entry points, enabling high-performance integration with system resources.
  • KCL functions provide lightweight reusability within .k files without requiring external compilation steps.
  • The kcl_plugin. module prefix and load_plugins flag serve as the gatekeepers that allow the parser and evaluator to recognize and resolve external dependencies.
  • Runtime resolution occurs through the plugin_functions map in crates/api/src/kcl.rs or via JSON marshalling through the handler in crates/runtime/src/stdlib/plugin.rs.

Frequently Asked Questions

How do I enable plugin support when running KCL programmatically?

When invoking KCL through the Rust API or other language bindings, set the load_plugins field in ParserOptions to true and provide a valid plugin agent pointer. In crates/runner/src/runner.rs (lines 165-170), the runner automatically enables this flag when a non-zero plugin_agent argument is supplied via the CLI.

What is the difference between direct plugin registration and the JSON fallback mechanism?

Direct registration involves inserting function pointers into the plugin_functions HashMap in crates/api/src/kcl.rs (lines 370-371), allowing zero-overhead calls from KCL to native code. The JSON fallback, implemented in crates/runtime/src/stdlib/plugin.rs (lines 68-84), marshals arguments to JSON strings and invokes kcl_plugin_invoke_json when direct registration is unavailable, providing flexibility at the cost of serialization overhead.

Can I write KCL plugins in languages other than Rust?

Yes, any language capable of producing C-ABI compatible shared libraries can implement KCL plugins. The requirements are exposing kcl_plugin_init to register the dispatcher and kcl_plugin_invoke_json to handle method calls with JSON serialization. Languages like C, C++, Go, or Zig can be used, provided they can export the required unmangled symbols and handle the JSON-based argument passing defined in crates/runtime/src/stdlib/plugin.rs.

What naming convention must I follow for plugin modules?

All plugin imports must use the kcl_plugin. prefix, defined as PLUGIN_MODULE_PREFIX in crates/sema/src/plugin/mod.rs. When registering functions in the native plugin, use the format <package_name>.<function_name> (e.g., my_pkg.say_hello), which KCL resolves as kcl_plugin.my_pkg.say_hello in configuration code. The parser validates this prefix in crates/parser/src/lib.rs when the load_plugins flag is enabled.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →