HTTP in Koru: Python Simplicity, C Performance

· 8 min read

The Promise

What if you could write this:

~import "$koru/curl"
~import "$std/io"

~koru.curl:get(url: "https://api.github.com/users/octocat")
| ok r |> std.io:print.ln("Status: {{ r.status }}")
| err e |> std.io:print.ln("Error: {{ e.msg }}")

And get:

  • Automatic library linking - No manual -lcurl flags
  • Compile-time resource safety - Can’t forget to close the connection
  • Zero garbage collection - Deterministic cleanup, no GC pauses
  • Native performance - Compiles to the same code as handwritten C

That’s Koru. Let me show you how it works.


The Complete Example

Here’s a real, working HTTP GET request:

// 100% PURE KORU - No Zig imports needed!
~import "$koru/curl"
~import "$std/io"

~koru.curl:get(url: "https://httpbin.org/get")
| ok r |> std.io:print.blk {
    === HTTP Response ===
    Status: {{ r.status }}
    Body length: {{ r.body.len }}
    }
| err e |> std.io:print.ln("Error: {{ e.msg:s }}")

Output:

=== HTTP Response ===
Status: 200
Body length: 221

That’s it. No boilerplate. No manual memory management. No forgetting to close connections.


How It Works: Build Requires

When you write:

~import "$koru/curl"

Koru does something clever. The curl library declares its build requirements:

~import "$std/build"

~std.build:requires {
    exe.linkSystemLibrary("curl");
    exe.linkLibC();
}

At compile time, these requirements flow up to your program. The compiler automatically:

  1. Configures the linker to link against libcurl
  2. Configures the linker to link against libc
  3. Sets up the C interop

You don’t pass -lcurl to the linker. You don’t edit CMakeLists.txt. You don’t configure Makefiles. The import handles all the build system complexity.

Note: You still need libcurl installed on your system (apt-get install libcurl4-openssl-dev on Ubuntu, brew install curl on macOS). What Koru eliminates is the build configuration headache, not the one-time system dependency installation.

This is build requires in action - dependencies declare what they need, and the build system just works.


Resource Safety: The [open!] Obligation

Look at how the curl library declares its events:

~pub event get { url: []const u8, allocator: ?std.mem.Allocator }
| ok *Response[open!]
| err Error

See that [open!]? That’s a phantom type with a cleanup obligation. It means:

“This returns a Response that you MUST clean up before your flow terminates.”

If you try to ignore it:

~koru.curl:get(url: "https://example.com")
| ok r |> _  // ❌ COMPILE ERROR: r has cleanup obligation!

The compiler won’t let you. You must either:

  1. Call close() explicitly
  2. Let auto-discharge insert the cleanup

Auto-Discharge: Cleanup Without Clutter

Koru’s auto-discharge system detects when you reach a terminator (|> _) with unsatisfied obligations and inserts the cleanup calls automatically.

So this code:

~koru.curl:get(url: "https://httpbin.org/get")
| ok r |> std.io:print.ln("Status: {{ r.status }}")
| err e |> std.io:print.ln("Error: {{ e.msg }}")

Gets transformed to (conceptually, there is some magic):

~koru.curl:get(url: "https://httpbin.org/get")
| ok r |> std.io:print.ln("Status: {{ r.status }}")
    |> koru.curl:close(resp: r)  // Auto-inserted!
       | closed |> _
| err e |> std.io:print.ln("Error: {{ e.msg }}")

You get the safety of explicit resource management without the verbosity.


Comparison: Python vs Koru

Python (requests library)

import requests

try:
    response = requests.get("https://httpbin.org/get")
    print(f"Status: {response.status_code}")
    print(f"Body length: {len(response.text)}")
except requests.RequestException as e:
    print(f"Error: {e}")

What’s happening under the hood:

  • Garbage collector tracks the response object
  • Connection pooling managed by runtime
  • Memory freed “eventually” when GC runs
  • ~50MB runtime overhead (Python interpreter)
  • ~100ms startup time

Koru

~import "$koru/curl"
~import "$std/io"

~koru.curl:get(url: "https://httpbin.org/get")
| ok r |> std.io:print.blk {
    Status: {{ r.status }}
    Body length: {{ r.body.len }}
    }
| err e |> std.io:print.ln("Error: {{ e.msg }}")

What’s happening under the hood:

  • Direct libcurl calls (same as C)
  • Response freed deterministically when flow ends
  • Compile-time verification of cleanup
  • ~100KB binary size
  • ~1ms startup time

Same convenience. 500x smaller. 100x faster startup. No GC pauses.


The Generated Code

Curious what Koru actually generates? Here’s the core of what the compiler produces:

pub fn flow0() void {
    const result_0 = koru_curl.get_event.handler(.{
        .url = "https://httpbin.org/get",
        .allocator = null
    });
    switch (result_0) {
        .ok => |r| {
            std.debug.print("Status: {d}\n", .{r.status});
            std.debug.print("Body length: {d}\n", .{r.body.len});
            // Auto-inserted cleanup:
            koru_curl.close_event.handler(.{ .resp = r });
        },
        .err => |e| {
            std.debug.print("Error: {s}\n", .{e.msg});
        },
    }
}

It’s just Zig. Which compiles to native code. Which runs at C speed.


POST Requests

The library also supports POST:

~import "$koru/curl"
~import "$std/io"

const payload = "{ "name": "Koru", "awesome": true }"

~koru.curl:post(url: "https://httpbin.org/post", body: payload)
| ok r |> std.io:print.blk {
    Posted! Status: {{ r.status }}
    Response: {{ r.body:s }}
    }
| err e |> std.io:print.ln("POST failed: {{ e.msg }}")

Same pattern. Same safety. Same performance.


Why Not Just Use C?

You could write this in C:

#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Response {
    char *data;
    size_t size;
};

static size_t write_callback(void *ptr, size_t size, size_t nmemb, void *userdata) {
    size_t real_size = size * nmemb;
    struct Response *resp = (struct Response *)userdata;
    char *new_data = realloc(resp->data, resp->size + real_size + 1);
    if (!new_data) return 0;
    resp->data = new_data;
    memcpy(&resp->data[resp->size], ptr, real_size);
    resp->size += real_size;
    resp->data[resp->size] = 0;
    return real_size;
}

int main() {
    CURL *curl = curl_easy_init();
    if (!curl) {
        fprintf(stderr, "Failed to init curl\n");
        return 1;
    }

    struct Response resp = {0};
    curl_easy_setopt(curl, CURLOPT_URL, "https://httpbin.org/get");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);

    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        curl_easy_cleanup(curl);
        return 1;
    }

    long http_code;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    printf("Status: %ld\n", http_code);
    printf("Body length: %zu\n", resp.size);

    free(resp.data);
    curl_easy_cleanup(curl);
    return 0;
}

And that’s just the code. To actually build this, you need to configure your build system:

# Compile with manual linker flags
gcc -o http_client http_client.c -lcurl

# Or in CMake
find_package(CURL REQUIRED)
target_link_libraries(my_app PRIVATE CURL::libcurl)

# Or in Makefile
LDFLAGS += -lcurl

Every project. Every build system. Every developer. Everyone has to figure out the linking flags, update CMakeLists.txt, edit Makefiles.

Compare to Koru (assuming libcurl is installed):

koruc my_http_client.kz
./my_http_client

No -lcurl. No CMake. No Makefile edits. The ~import "$koru/curl" statement handles all the build configuration.


40+ lines of C vs 6 lines of Koru. Same performance. But in C:

  • Forgot curl_easy_cleanup? Memory leak.
  • Forgot free(resp.data)? Memory leak.
  • Used curl after cleanup? Undefined behavior.
  • Forgot -lcurl? Linker error.
  • Wrong curl version installed? Runtime crashes.
  • No compiler help for any of this.

Koru gives you C performance with Python ergonomics and Rust-style compile-time safety.


The Library Implementation

Here’s how the curl library is implemented (simplified):

pub const Response = struct {
    handle: ?*c.CURL,
    allocator: std.mem.Allocator,
    status: i32,
    body: []u8,
};

~pub event get { url: []const u8, allocator: ?std.mem.Allocator }
| ok *Response[open!]
| err Error

~proc get {
    const alloc = allocator orelse std.heap.page_allocator;
    const handle = c.curl_easy_init();
    // ... setup and perform request ...
    return .{ .ok = resp };
}

~pub event close { resp: *Response[!open] }

~proc close {
    if (resp.handle) |h| c.curl_easy_cleanup(h);
    if (resp.body.len > 0) resp.allocator.free(resp.body);
    resp.allocator.destroy(resp);
}

The phantom type [open!] on the return creates the obligation. The [!open] on the close parameter consumes it. The compiler connects the dots.


Try It Yourself

  1. Clone koru-libs:
git clone https://github.com/korulang/koru-libs
cd koru-libs/curl/examples
  1. Run the example:
koruc 01_simple_get.kz
./a.out
  1. See the output:
=== HTTP Response ===
Status: 200
Body length: 221

What We Learned

  • Build requires: ~import "$koru/curl" handles all linker configuration automatically
  • Phantom obligations: [open!] creates compile-time cleanup requirements
  • Auto-discharge: Cleanup inserted automatically at terminators
  • Zero overhead: Same performance as handwritten C
  • No GC: Deterministic resource cleanup, no runtime overhead

This is what happens when you design a language around the idea that scripting convenience and systems performance aren’t mutually exclusive.

Welcome to HTTP in Koru. Welcome to the future of systems programming.


Further Reading