HTTP in Koru: Python Simplicity, C Performance
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
-lcurlflags - 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:
- Configures the linker to link against libcurl
- Configures the linker to link against libc
- 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-devon Ubuntu,brew install curlon 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:
- Call
close()explicitly - 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
curlafter 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
- Clone koru-libs:
git clone https://github.com/korulang/koru-libs
cd koru-libs/curl/examples - Run the example:
koruc 01_simple_get.kz
./a.out - 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.