Package Development
Kit supports three types of packages, allowing you to choose the right approach for your needs. Whether you're building a pure Kit library, wrapping a C library, or leveraging Zig's power, Kit provides the tools you need.
Overview
Kit packages are reusable libraries that can be shared and installed via kit add.
Every package must have a kit.toml manifest file that describes its metadata,
dependencies, and configuration.
Package Types
Kit supports three types of packages:
| Type | Use Case | FFI Keyword |
|---|---|---|
| Pure Kit | Libraries written entirely in Kit | None |
| C FFI | Wrapping existing C libraries | extern-c |
| Zig FFI | High-performance native code with Zig | extern-zig |
Pure Kit Packages
Pure Kit packages are written entirely in Kit. They're the simplest to create and are portable across all platforms Kit supports.
Example: Web Framework
Here's an example of a pure Kit package structure:
kit-mylib
├── kit.toml
├── src
│ ├── main.kit # Entry point
│ ├── request.kit
│ ├── response.kit
│ └── router.kit
├── examples
│ └── basic.kit
└── README.md
kit.toml
# Package metadata
[package]
name = "mylib"
version = "2026.1.13"
# kind = "kit" # Optional, "kit" is the default for pure Kit packages
authors = ["Your Name <you@example.com>"]
description = "A useful Kit library"
keywords = ["web", "framework"]
categories = ["web"]
license = "MIT"
repository = "https://github.com/your-name/kit-mylib"
entry-point = "src/main.kit"
# Dependencies
[dependencies]
logging = { git = "https://gitlab.com/kit-lang/packages/kit-logging" }
# Files to exclude when published
exclude = [
"examples/",
"tests/",
"*.test.kit",
"README.md"
]
First-party packages maintained by the Kit Language Team are hosted at
gitlab.com/kit-lang/packages/. Community packages can be hosted
on any Git provider (GitLab, GitHub, Bitbucket, etc.) using your own repositories.
src/main.kit
# Import internal modules
import "./request.kit" as Request
import "./response.kit" as Response
import "./router.kit" as Router
## Create a new application instance.
export app = fn() =>
{
routes: [],
middleware: []
}
## Register a GET route handler.
export get = fn(app, pattern, handler) =>
Router.add-route app "GET" pattern handler
## Register a POST route handler.
export post = fn(app, pattern, handler) =>
Router.add-route app "POST" pattern handler
## Start the server on the specified port.
export listen = fn(app, port, callback) =>
callback
Router.serve app port
Use the export keyword to make functions and values available to package users.
Non-exported bindings remain internal to the module.
C FFI Packages
C FFI packages wrap existing C libraries, allowing Kit code to call C functions directly. This is useful for leveraging the vast ecosystem of existing C libraries.
Syntax
extern-c c_function_name(param: Type, ...) -> ReturnType as Kit.alias from "header.h" link "library"
| Component | Description |
|---|---|
c_function_name |
The name of the C function to call |
as Kit.alias |
Optional Kit-friendly name for the function |
from "header.h" |
C header file containing the function declaration |
link "library" |
Library to link against (e.g., "sodium" links libsodium) |
Example: Crypto Library
Here's an example wrapping libsodium:
Package Structure
kit-crypto
├── kit.toml
├── src
│ ├── crypto.kit # Kit API with extern-c declarations
│ ├── crypto_wrapper.c # C wrapper code
│ └── crypto_wrapper.h # C header file
├── examples
│ └── hash.kit
└── README.md
kit.toml
# Package metadata
[package]
name = "crypto"
version = "2026.1.13"
kind = "ffi-c"
authors = ["Kit Language Team <support@kit-lang.org>"]
description = "Cryptographic library using libsodium"
keywords = ["crypto", "security", "hashing"]
categories = ["cryptography"]
license = "MIT"
repository = "https://gitlab.com/kit-lang/packages/kit-crypto"
entry-point = "src/crypto.kit"
# C source files to compile and link
csources = ["src/crypto_wrapper.c"]
# Files to exclude when published
exclude = [
"examples/",
"tests/",
"*.test.kit",
"README.md"
]
src/crypto.kit
# Low-level C API bindings
# Initialization
extern-c kit_crypto_init() -> Int as Crypto.init-raw from "crypto_wrapper.h" link "sodium"
# Hashing
extern-c kit_crypto_hash_sha256(message: String) -> String as Crypto.sha256-raw from "crypto_wrapper.h" link "sodium"
extern-c kit_crypto_hash_sha512(message: String) -> String as Crypto.sha512-raw from "crypto_wrapper.h" link "sodium"
# Password hashing
extern-c kit_crypto_password_hash(password: String) -> String as Crypto.password-hash-raw from "crypto_wrapper.h" link "sodium"
extern-c kit_crypto_password_verify(hash: String, password: String) -> Int as Crypto.password-verify-raw from "crypto_wrapper.h" link "sodium"
# High-level Kit API
## Initialize the crypto library. Must be called once before using other functions.
export init = fn =>
match Crypto.init-raw
| 0 -> Ok
| _ -> Err "Failed to initialize crypto library"
## Hash a string using SHA-256.
export sha256 = fn(message) =>
Crypto.sha256-raw message
## Hash a password using Argon2id.
export hash-password = fn(password) =>
Crypto.password-hash-raw password
## Verify a password against a hash.
export verify-password = fn(hash, password) =>
match Crypto.password-verify-raw hash password
| 0 -> true
| _ -> false
src/crypto_wrapper.c
/**
* libsodium wrapper for Kit
*/
#include <sodium.h>
#include <stdlib.h>
#include <string.h>
/**
* Initialize libsodium
* Returns 0 on success, -1 on failure
*/
int kit_crypto_init(void) {
return sodium_init();
}
/**
* SHA-256 hash
* Returns hex string (caller must free)
*/
char* kit_crypto_hash_sha256(const char* message) {
if (message == NULL) return NULL;
unsigned char hash[crypto_hash_sha256_BYTES];
crypto_hash_sha256(hash, (const unsigned char*)message, strlen(message));
char* hex = (char*)malloc(crypto_hash_sha256_BYTES * 2 + 1);
if (hex == NULL) return NULL;
sodium_bin2hex(hex, crypto_hash_sha256_BYTES * 2 + 1, hash, crypto_hash_sha256_BYTES);
return hex;
}
C FFI packages require users to have the linked libraries installed on their system. Document these requirements in your README.
Zig FFI Packages
Zig FFI packages use Kit's extern-zig keyword to call Zig code directly.
This provides high-performance native code without requiring users to have external libraries installed.
Syntax
extern-zig function-name(param: Type, ...) as zig_name from "path/to/file.zig"
| Component | Description |
|---|---|
function-name |
The Kit function name (with dashes) |
as zig_name |
Optional Zig function name if different (uses underscores) |
from "path.zig" |
Path to the Zig source file relative to the Kit file |
Example: Redis Client
Here's an example of a Zig FFI package:
Package Structure
kit-redis
├── kit.toml
├── src
│ └── main.kit # Kit API with extern-zig declarations
├── zig
│ ├── redis.zig # Zig implementation
│ └── kit_ffi.zig # Kit FFI types and helpers
├── examples
│ └── basic.kit
└── README.md
kit.toml
# Package metadata
[package]
name = "redis"
version = "2026.1.13"
kind = "ffi-zig"
authors = ["Kit Language Team <support@kit-lang.org>"]
description = "Redis client using native Zig FFI"
keywords = ["redis", "database", "cache"]
categories = ["database"]
license = "MIT"
repository = "https://gitlab.com/kit-lang/packages/kit-redis"
entry-point = "src/main.kit"
# Files to exclude when published
exclude = [
"examples/",
"tests/",
"*.test.kit",
"README.md"
]
src/main.kit
# Redis client using Zig FFI
# Connection Management
## Connect to a Redis server.
## Returns a Result with connection handle on success.
extern-zig connect(host: String, port: Int) from "../zig/redis.zig"
## Connect using a URL (e.g., "redis://localhost:6379").
extern-zig connect-url(url: String) as connect_url from "../zig/redis.zig"
## Close a connection.
extern-zig close(conn: Int) from "../zig/redis.zig"
## Ping the server.
extern-zig ping(conn: Int) from "../zig/redis.zig"
# String Operations
## Get the value of a key.
extern-zig get(conn: Int, key: String) from "../zig/redis.zig"
## Set the value of a key.
extern-zig set(conn: Int, key: String, value: String) from "../zig/redis.zig"
## Delete a key.
extern-zig del(conn: Int, key: String) from "../zig/redis.zig"
## Check if a key exists.
extern-zig exists?(conn: Int, key: String) as exists from "../zig/redis.zig"
zig/redis.zig
//! Redis client implementation for Kit FFI
const std = @import("std");
const ffi = @import("kit_ffi.zig");
const KitValue = ffi.KitValue;
// Public functions callable from Kit
pub fn connect(allocator: std.mem.Allocator, args: []const KitValue) KitValue {
if (args.len < 2) return ffi.makeErr(allocator, "connect requires host and port");
const host = args[0].getString() orelse {
return ffi.makeErr(allocator, "host must be a string");
};
const port_val = args[1].getInt() orelse {
return ffi.makeErr(allocator, "port must be an integer");
};
const port: u16 = @intCast(port_val);
// Connect to Redis server...
const address = std.net.Address.parseIp4(host, port) catch {
return ffi.makeErr(allocator, "Invalid address");
};
const sock = std.posix.socket(address.any.family, std.posix.SOCK.STREAM, 0) catch {
return ffi.makeErr(allocator, "Failed to create socket");
};
std.posix.connect(sock, &address.any, @sizeOf(std.posix.sockaddr.in)) catch {
return ffi.makeErr(allocator, "Connection failed");
};
return ffi.makeOk(allocator, ffi.makeInt(@intCast(sock)));
}
pub fn get(allocator: std.mem.Allocator, args: []const KitValue) KitValue {
if (args.len < 2) return ffi.makeErr(allocator, "get requires connection and key");
// Implementation...
}
The kit_ffi.zig module provides the KitValue extern struct and helper functions
for Zig FFI packages. Use accessor methods like getString(), getInt(), and
getList() to extract values. Use helpers like ffi.makeOk(), ffi.makeErr(),
ffi.makeString(), and ffi.makeInt() to construct return values.
Zig FFI Function Signature
All Zig FFI functions must follow this signature:
pub fn function_name(allocator: std.mem.Allocator, args: []const KitValue) KitValue
allocator- Memory allocator for any allocationsargs- Slice of Kit values passed as arguments- Returns a
KitValue
Package Configuration
The kit.toml file is the manifest for your package:
Required Fields
| Field | Description |
|---|---|
name |
Package name (lowercase, hyphens allowed) |
version |
Semantic version (e.g., "2026.1.13") |
entry-point |
Path to the main Kit file |
Optional Fields
| Field | Description |
|---|---|
kind |
Package type: "kit" (default), "ffi-c", or "ffi-zig" |
authors |
List of author names |
description |
Short package description |
license |
License identifier (e.g., "MIT") |
repository |
URL to the source repository |
keywords |
List of keywords for discovery |
categories |
List of categories |
csources |
C source files to compile (C FFI only) |
exclude |
Files to exclude when installed as dependency |
Resource Management
FFI packages often work with resources that need cleanup (connections, file handles, memory).
Kit provides the @defer-required attribute to annotate functions that acquire
resources, helping ensure proper cleanup.
The @defer-required Attribute
Use @defer-required("cleanup-function") before FFI declarations that return
resources. The attribute specifies which function must be called to release the resource.
Syntax
@defer-required("cleanup-function-name")
extern-zig acquire-resource(...) -> Resource from "file.zig"
Database Connection Example
# Acquire a connection - must call "close" when done
@defer-required("close")
extern-zig connect(host: String, port: Int) from "../zig/db.zig"
# Release the connection
extern-zig close(conn: Int) from "../zig/db.zig"
# Usage
conn = connect "localhost" 5432
defer close conn # Ensure cleanup happens
# ... use connection ...
Audio/Graphics Example
# Initialize audio system - must call "shutdown" when done
@defer-required("Audio.shutdown")
extern-zig Audio.setup(config: AudioConfig) from "../zig/audio.zig"
# Shutdown audio system
extern-zig Audio.shutdown() from "../zig/audio.zig"
# Usage
Audio.setup default-config
defer Audio.shutdown()
# ... play sounds ...
Module-Qualified Cleanup Functions
When the cleanup function is namespaced (e.g., Metal.release), use the
full qualified name:
@defer-required("Metal.release")
extern-zig Metal.create-buffer(size: Int) from "../zig/metal.zig"
Kit's linter uses @defer-required annotations to detect when resources
are acquired but not properly cleaned up. This helps catch resource leaks at
compile time rather than runtime.
When to Use @defer-required
Add @defer-required to functions that:
- Open connections (database, network, websocket)
- Allocate native memory or buffers
- Initialize subsystems (audio, graphics, windowing)
- Create handles that need explicit release
- Load resources that must be freed (images, fonts, files)
Publishing Packages
Packages can be published to Git repositories and installed via kit add.
Preparing for Publication
- Ensure your
kit.tomlis complete with all metadata - Add an
excludelist to reduce package size - Write a README.md with usage examples
- Add examples in an
examples/directory - Push to a Git repository (GitLab, GitHub, etc.)
Installing Published Packages
# Install from Git
kit add --git https://gitlab.com/your-name/your-package
# Install a specific version
kit add --git https://gitlab.com/your-name/your-package --tag v1.0.0
Best Practices
API Design
- Use Result types - Return
Resultfor operations that can fail - Use Option types - Return
Optionfor optional values - Document functions - Use
##doc comments on exported functions - Follow naming conventions - Use kebab-case for Kit functions
Error Handling
# Good: Return Result type
export connect = fn(host, port) =>
match connect-raw host port
| -1 -> Err "Connection failed"
| handle -> Ok handle
# Good: Return Option type
export get = fn(conn, key) =>
match get-raw conn key
| "" -> None
| value -> Some value
Testing
- Create test files in a
tests/directory - Add example files in an
examples/directory - Test both the interpreter (
kit run) and compiler (kit build)
Documentation
- Write a comprehensive README.md
- Document system requirements for C FFI packages
- Include usage examples for all major features