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:
kcl_plugin_init– Registers the function dispatcher, a pointer to a function that receivesmethod,args_json, andkwargs_jsonparameters.kcl_plugin_invoke_json– Called by the KCL runtime to forward method calls to the dispatcher, implemented incrates/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:
- Direct registration check – The evaluator looks up
my_funcin the runtime context'splugin_functionsmap (seecrates/api/src/kcl.rs, lines 370-371). If present, the function is called directly without JSON marshalling overhead. - 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 (seecrates/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_initandkcl_plugin_invoke_jsonentry points, enabling high-performance integration with system resources. - KCL functions provide lightweight reusability within
.kfiles without requiring external compilation steps. - The
kcl_plugin.module prefix andload_pluginsflag serve as the gatekeepers that allow the parser and evaluator to recognize and resolve external dependencies. - Runtime resolution occurs through the
plugin_functionsmap incrates/api/src/kcl.rsor via JSON marshalling through the handler incrates/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →