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"
]
Package Repositories

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
Export Keyword

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;
}
System Dependencies

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...
}
kit_ffi.zig Module

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 allocations
  • args - 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"
Static Analysis

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

  1. Ensure your kit.toml is complete with all metadata
  2. Add an exclude list to reduce package size
  3. Write a README.md with usage examples
  4. Add examples in an examples/ directory
  5. 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 Result for operations that can fail
  • Use Option types - Return Option for 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