1
0
Fork 0
forked from fun/fun

Added some basic CGI support using kcgi plus a lot of fixes and content updates. (0.41.9)

This commit is contained in:
Johannes Findeisen 2026-05-09 10:47:33 +02:00
commit 604c415c0c
25 changed files with 407 additions and 13 deletions

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.10)
project(fun VERSION 0.41.8 LANGUAGES C)
project(fun VERSION 0.41.9 LANGUAGES C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
@ -430,6 +430,26 @@ if(BUILD_TESTING)
else()
message(WARNING "include_line_mapping_test.fun not found; skipping include_line_mapping CTest")
endif()
# KCGI example smoke test (only when the KCGI extension is enabled)
# We run the CGI example with minimal environment to avoid RFC warnings
# and assert the body contains the expected greeting.
if(FUN_WITH_KCGI)
set(_kcgi_example "${CMAKE_SOURCE_DIR}/examples/cgi/hello_kcgi.fun")
if(EXISTS "${_kcgi_example}")
add_test(NAME kcgi_hello
COMMAND $<TARGET_FILE:fun> "${_kcgi_example}"
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
# Provide standard library path and minimal CGI environment
set_tests_properties(kcgi_hello PROPERTIES
ENVIRONMENT "FUN_LIB_DIR=${CMAKE_SOURCE_DIR}/lib;REQUEST_METHOD=GET;QUERY_STRING=name=Fun;SERVER_NAME=localhost;SERVER_PORT=80;SCRIPT_NAME=/hello_kcgi.fun;REMOTE_ADDR=127.0.0.1"
PASS_REGULAR_EXPRESSION "Hello, Fun!"
)
else()
message(WARNING "KCGI example not found: ${_kcgi_example}; skipping kcgi_hello CTest")
endif()
endif()
endif()
# --- Doxygen docs target (optional) ---

View file

@ -88,7 +88,7 @@ See [./lib/](https://git.xw3.org/fun/fun/src/branch/main/lib) for what the stand
### Optional extensions (build-time selectable / only testing this on Linux actually):
- [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface) support builtin using [kcgi](https://kristaps.bsd.lv/kcgi/) (optional) &#9744;
- [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface) support builtin using [kcgi](https://kristaps.bsd.lv/kcgi/) (optional) — see [docs](./web/documentation/extensions/kcgi/kcgi.md) &#9745;
- [cURL (libcurl)](./web/documentation/extensions/curl/curl.md) (optional) &#9745;
- [INI (iniparser)](./web/documentation/extensions/ini/ini.md) (optional) &#9745;
- [JSON (json-c)](./web/documentation/extensions/json/json.md) (optional) &#9745;

View file

@ -20,17 +20,18 @@ include(${CMAKE_SOURCE_DIR}/cmake/Extensions/PCRE2.cmake)
include(${CMAKE_SOURCE_DIR}/cmake/Extensions/CURL.cmake)
include(${CMAKE_SOURCE_DIR}/cmake/Extensions/REPL.cmake)
include(${CMAKE_SOURCE_DIR}/cmake/Extensions/OPENSSL.cmake)
include(${CMAKE_SOURCE_DIR}/cmake/Extensions/KCGI.cmake)
# Summary of extension toggles
message(STATUS "---- Fun extension summary ----")
_fun_print_feature("SQLite (FUN_WITH_SQLITE)" FUN_WITH_SQLITE)
_fun_print_feature("PCSC (FUN_WITH_PCSC)" FUN_WITH_PCSC)
_fun_print_feature("JSON-C (FUN_WITH_JSON)" FUN_WITH_JSON)
_fun_print_feature("json-c (FUN_WITH_JSON)" FUN_WITH_JSON)
_fun_print_feature("INI/iniparser (FUN_WITH_INI)" FUN_WITH_INI)
_fun_print_feature("libxml2 (FUN_WITH_XML2)" FUN_WITH_XML2)
## libsql extension removed
_fun_print_feature("PCRE2 (FUN_WITH_PCRE2)" FUN_WITH_PCRE2)
_fun_print_feature("libcurl (FUN_WITH_CURL)" FUN_WITH_CURL)
_fun_print_feature("curl (FUN_WITH_CURL)" FUN_WITH_CURL)
_fun_print_feature("REPL (FUN_WITH_REPL)" FUN_WITH_REPL)
_fun_print_feature("OpenSSL (FUN_WITH_OPENSSL)" FUN_WITH_OPENSSL)
_fun_print_feature("kcgi (FUN_WITH_KCGI)" FUN_WITH_KCGI)
message(STATUS "--------------------------------")

View file

@ -0,0 +1,19 @@
# KCGI (kcgi)
option(FUN_WITH_KCGI "Enable kcgi support (CGI/FastCGI via kcgi)" OFF)
set(KCGI_INCLUDE_DIRS "")
set(KCGI_LINK_LIBS "")
if(FUN_WITH_KCGI)
add_definitions(-DFUN_WITH_KCGI)
find_package(PkgConfig QUIET)
if(PKG_CONFIG_FOUND)
pkg_check_modules(KCGI QUIET kcgi)
endif()
if(KCGI_FOUND)
list(APPEND KCGI_INCLUDE_DIRS ${KCGI_INCLUDE_DIRS} ${KCGI_INCLUDE_DIRS})
list(APPEND KCGI_LINK_LIBS ${KCGI_LINK_LIBS} ${KCGI_LIBRARIES})
else()
# Fallback libs commonly required for kcgi on Linux
list(APPEND KCGI_LINK_LIBS kcgi z)
endif()
endif()

View file

@ -42,7 +42,8 @@ foreach(var_pair
INIPARSER
LIBSQL
LIBXML2
OPENSSL)
OPENSSL
KCGI)
if(${var_pair}_INCLUDE_DIRS)
target_include_directories(fun_core PRIVATE ${${var_pair}_INCLUDE_DIRS})
endif()
@ -93,6 +94,10 @@ if(FUN_WITH_OPENSSL)
target_compile_definitions(fun_core PUBLIC FUN_WITH_OPENSSL=1)
endif()
if(FUN_WITH_KCGI)
target_compile_definitions(fun_core PUBLIC FUN_WITH_KCGI=1)
endif()
# LibreSSL toggle: ensure compile definitions are applied to fun_core so
# the LibreSSL code paths are compiled, and linking vars are handled above.

View file

@ -0,0 +1,14 @@
#!/usr/bin/env fun
/*
* Minimal example using KCGI wrapper.
* Run under a CGI/FastCGI environment where kcgi can parse the request.
*/
#include <net/kcgi.fun>
cgi = KCGI()
name = cgi.get_or("name", "Fun")
body = "<h1>Hello, " + to_string(name) + "!</h1>"
cgi.reply(200, "text/html; charset=utf-8", body)

46
lib/net/kcgi.fun Normal file
View file

@ -0,0 +1,46 @@
/*
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2026 Johannes Findeisen
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*/
// Thin wrapper around VM kcgi intrinsics
class KCGI()
fun _construct(this)
this.req = kcgi_parse()
fun get(this, key)
if (typeof(this.req) == "Map")
fields = this.req["fields"]
if (typeof(fields) == "Map")
return fields[key]
return nil
// Returns the field value if present and non-nil, otherwise returns 'def'.
fun get_or(this, key, def)
v = this.get(key)
if (v == nil)
return def
// Some code may stringify nil to "nil"; treat that as missing too.
if (typeof(v) == "String" && v == "nil")
return def
return v
fun reply(this, code, content_type, body)
if (kcgi_reply_start(code, content_type) == 1)
_ = kcgi_write(to_string(body))
_ = kcgi_end()
// streaming API
fun start(this, code, content_type)
return kcgi_reply_start(code, content_type)
fun write(this, chunk)
return kcgi_write(to_string(chunk))
fun end(this)
return kcgi_end()

1
make
View file

@ -29,6 +29,7 @@ if [ "$target" = "all" ]; then
-DFUN_WITH_JSON=ON \
-DFUN_WITH_INI=ON \
-DFUN_WITH_CPP=ON \
-DFUN_WITH_KCGI=ON \
-DFUN_WITH_OPENSSL=ON \
&& cmake --build build --target fun
elif [ "$target" = "all_debug" ]; then

View file

@ -143,6 +143,12 @@ typedef enum {
OP_PROC_SYSTEM, // pops command string; pushes exit code number
OP_TIME_NOW_MS, // pushes current wall-clock time in milliseconds since Unix epoch
OP_CLOCK_MONO_MS, // pushes monotonic clock in milliseconds (not wall time)
// kcgi (optional)
OP_KCGI_PARSE, // () -> Map | Nil (parse request via kcgi)
OP_KCGI_REPLY_START, // (code:int, content_type:string) -> 1/0
OP_KCGI_WRITE, // (chunk:string) -> 1/0
OP_KCGI_END, // () -> 1/0 (free request)
OP_DATE_FORMAT, // pops fmt string, ms epoch (int); pushes formatted date string using strftime
OP_ENV_ALL, // pushes map of all environment variables
OP_FUN_VERSION, // pushes version string

89
src/extensions/kcgi.c Normal file
View file

@ -0,0 +1,89 @@
/*
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2026 Johannes Findeisen
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*/
/**
* @file kcgi.c
* @brief kcgi helpers for Fun VM KCGI-related opcodes (conditional build).
*/
#ifdef FUN_WITH_KCGI
#include <kcgi.h>
#include <stdlib.h>
#include <string.h>
/* Thread-local request handle for the current CGI invocation */
#ifdef _WIN32
static __declspec(thread) struct kreq *g_kcgi_req = NULL;
#else
static __thread struct kreq *g_kcgi_req = NULL;
#endif
static Value kcgi_fields_to_map(const struct kreq *r) {
Value m = make_map_empty();
if (!r) return m;
for (size_t i = 0; i < r->fieldsz; i++) {
const char *k = r->fields[i].key ? r->fields[i].key : "";
const char *v = r->fields[i].val ? r->fields[i].val : "";
map_set(&m, k, make_string(v));
}
return m;
}
static Value kreq_to_fun(const struct kreq *r) {
Value out = make_map_empty();
if (!r) return out;
/* Basic request info */
map_set(&out, "host", make_string(r->host ? r->host : ""));
map_set(&out, "port", make_int((int64_t)r->port));
map_set(&out, "path", make_string(r->path ? r->path : ""));
map_set(&out, "suffix", make_string(r->suffix ? r->suffix : ""));
Value fields = kcgi_fields_to_map(r);
map_set(&out, "fields", fields);
return out;
}
/* Lifecycle helpers used by VM opcodes */
static int kcgi_parse_request(struct kreq **out) {
static const struct kvalid keys[] = { { NULL, NULL } }; /* accept all */
struct kreq *r = (struct kreq *)calloc(1, sizeof(*r));
if (!r) return 0;
enum kcgi_err er = khttp_parse(r, keys, 0, NULL, 0, 0);
if (er != KCGI_OK) { free(r); return 0; }
*out = r;
return 1;
}
static void kcgi_free_request(struct kreq *r) {
if (!r) return;
khttp_free(r);
free(r);
}
static int kcgi_reply_start(int code, const char *ctype) {
if (!g_kcgi_req) return 0;
/* Emit Content-Type header; Status defaults to 200 if not set */
if (ctype && *ctype) {
if (khttp_head(g_kcgi_req, "Content-Type", "%s", ctype) != KCGI_OK)
return 0;
}
if (khttp_body(g_kcgi_req) != KCGI_OK)
return 0;
return 1;
}
static int kcgi_write_str(const char *s) {
if (!g_kcgi_req) return 0;
if (!s) s = "";
size_t n = strlen(s);
return khttp_write(g_kcgi_req, s, n) == KCGI_OK;
}
#endif /* FUN_WITH_KCGI */

View file

@ -1351,6 +1351,71 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos)
free(name);
return 1;
}
/* KCGI intrinsics (optional feature; opcodes are safe no-ops when disabled) */
if (strcmp(name, "kcgi_parse") == 0) {
(*pos)++; /* '(' */
if (!consume_char(src, len, pos, ')')) {
parser_fail(*pos, "kcgi_parse expects ()");
free(name);
return 0;
}
bytecode_add_instruction(bc, OP_KCGI_PARSE, 0);
free(name);
return 1;
}
if (strcmp(name, "kcgi_reply_start") == 0) {
(*pos)++; /* '(' */
if (!emit_expression(bc, src, len, pos)) {
parser_fail(*pos, "kcgi_reply_start expects (code:int, content_type:string)");
free(name);
return 0;
}
if (!consume_char(src, len, pos, ',')) {
parser_fail(*pos, "kcgi_reply_start expects 2 args");
free(name);
return 0;
}
if (!emit_expression(bc, src, len, pos)) {
parser_fail(*pos, "kcgi_reply_start expects (code:int, content_type:string)");
free(name);
return 0;
}
if (!consume_char(src, len, pos, ')')) {
parser_fail(*pos, "Expected ')' after kcgi_reply_start args");
free(name);
return 0;
}
bytecode_add_instruction(bc, OP_KCGI_REPLY_START, 0);
free(name);
return 1;
}
if (strcmp(name, "kcgi_write") == 0) {
(*pos)++; /* '(' */
if (!emit_expression(bc, src, len, pos)) {
parser_fail(*pos, "kcgi_write expects (chunk:string)");
free(name);
return 0;
}
if (!consume_char(src, len, pos, ')')) {
parser_fail(*pos, "Expected ')' after kcgi_write arg");
free(name);
return 0;
}
bytecode_add_instruction(bc, OP_KCGI_WRITE, 0);
free(name);
return 1;
}
if (strcmp(name, "kcgi_end") == 0) {
(*pos)++; /* '(' */
if (!consume_char(src, len, pos, ')')) {
parser_fail(*pos, "kcgi_end expects ()");
free(name);
return 0;
}
bytecode_add_instruction(bc, OP_KCGI_END, 0);
free(name);
return 1;
}
if (strcmp(name, "rust_hello") == 0) {
(*pos)++; /* '(' */
if (!consume_char(src, len, pos, ')')) {

View file

@ -72,6 +72,7 @@
#include "extensions/pcsc.c"
#include "extensions/sqlite.c"
#include "extensions/xml2.c"
#include "extensions/kcgi.c"
/* forward declarations for include mapping used in error reporting */
extern char *preprocess_includes(const char *src);
@ -1089,6 +1090,14 @@ void vm_run(VM *vm, Bytecode *entry) {
#include "vm/curl/post.c"
#endif
/* KCGI ops */
#ifdef FUN_WITH_KCGI
#include "vm/kcgi/parse.c"
#include "vm/kcgi/reply_start.c"
#include "vm/kcgi/write.c"
#include "vm/kcgi/end.c"
#endif
/* OpenSSL ops (md5/sha256/sha512/ripemd160) */
#ifdef FUN_WITH_OPENSSL
#include "vm/openssl/md5.c"

10
src/vm/kcgi/end.c Normal file
View file

@ -0,0 +1,10 @@
/* KCGI_END */
case OP_KCGI_END: {
#ifdef FUN_WITH_KCGI
if (g_kcgi_req) { kcgi_free_request(g_kcgi_req); g_kcgi_req = NULL; }
push_value(vm, make_int(1));
#else
push_value(vm, make_int(0));
#endif
break;
}

20
src/vm/kcgi/parse.c Normal file
View file

@ -0,0 +1,20 @@
/* KCGI_PARSE */
case OP_KCGI_PARSE: {
#ifdef FUN_WITH_KCGI
if (g_kcgi_req) { /* safety: free previous if any */
kcgi_free_request(g_kcgi_req);
g_kcgi_req = NULL;
}
struct kreq *r = NULL;
if (!kcgi_parse_request(&r)) {
push_value(vm, make_nil());
break;
}
g_kcgi_req = r;
Value v = kreq_to_fun(r);
push_value(vm, v);
#else
push_value(vm, make_nil());
#endif
break;
}

31
src/vm/kcgi/reply_start.c Normal file
View file

@ -0,0 +1,31 @@
/* KCGI_REPLY_START */
case OP_KCGI_REPLY_START: {
#ifdef FUN_WITH_KCGI
Value vct = pop_value(vm);
Value vcode = pop_value(vm);
int code = 200;
if (vcode.type == VAL_INT) {
code = (int)vcode.i;
} else if (vcode.type == VAL_FLOAT) {
code = (int)vcode.d;
} else if (vcode.type == VAL_STRING) {
if (vcode.s) code = atoi(vcode.s);
}
char *ct = value_to_string_alloc(&vct);
free_value(vcode);
free_value(vct);
if (!ct || ct[0] == '\0') {
/* default content type */
free(ct);
ct = strdup("text/html; charset=utf-8");
}
int ok = kcgi_reply_start(code, ct);
free(ct);
push_value(vm, make_int(ok ? 1 : 0));
#else
Value a = pop_value(vm); free_value(a);
Value b = pop_value(vm); free_value(b);
push_value(vm, make_int(0));
#endif
break;
}

15
src/vm/kcgi/write.c Normal file
View file

@ -0,0 +1,15 @@
/* KCGI_WRITE */
case OP_KCGI_WRITE: {
#ifdef FUN_WITH_KCGI
Value vs = pop_value(vm);
char *s = value_to_string_alloc(&vs);
free_value(vs);
int ok = kcgi_write_str(s ? s : "");
if (s) free(s);
push_value(vm, make_int(ok ? 1 : 0));
#else
Value drop = pop_value(vm); free_value(drop);
push_value(vm, make_int(0));
#endif
break;
}

View file

@ -47,6 +47,7 @@ This section documents Fun's optional, build-time selectable extensions. Each pa
- [PCRE2 (Perl-compatible regex)](./pcre2/)
- [PC/SC (Smart cards)](./pcsc/)
- [OpenSSL](./openssl/)
- [kcgi](./kcgi/)
## Notes:

View file

@ -16,7 +16,6 @@ tags:
- optional
---
- CMake option: FUN_WITH_INI=ON
- Purpose: Read/write simple INI configuration files.
- Homepage: [https://github.com/ndevilla/iniparser](https://github.com/ndevilla/iniparser){:class="ext"}

View file

@ -15,7 +15,6 @@ tags:
- optional
---
- CMake option: FUN_WITH_JSON=ON
- Purpose: JSON parse/stringify and file helpers via json-c.
- Homepage: [https://json-c.github.io/json-c/](https://json-c.github.io/json-c/){:class="ext"}

View file

@ -0,0 +1,49 @@
layout: page
published: true
noToc: false
noComments: false
noDate: false
title: Fun - KCGI extension (optional)
subtitle: Documentation for KCGI (kcgi) extension (optional)
description: Documentation for KCGI (kcgi) extension (optional)
permalink: /documentation/extensions/kcgi/
lang: en
tags:
- extension
- kcgi
- optional
---
- CMake option: FUN_WITH_KCGI=ON
- Purpose: integrate the kcgi (CGI/FastCGI) C library; provide request parsing and response helpers for building CGI apps in Fun.
- Homepage: [https://kristaps.bsd.lv/kcgi/](https://kristaps.bsd.lv/kcgi/){:class="ext"}
## Build notes:
- Requires system kcgi development headers and libraries (pkg-config name: kcgi).
- If pkg-config is not available, build falls back to linking against libkcgi and zlib (as configured in cmake/Extensions/KCGI.cmake).
## Provided helper/opcodes:
- Function: kcgi_parse(data: none) -> Map | Nil. Parses the current CGI/FastCGI request via kcgi and returns a Map with keys like method, scheme, host, port, path, suffix, query, and fields (GET/POST map). Returns Nil on failure or when the extension is disabled.
- Function: kcgi_reply_start(code:int, content_type:string) -> 1/0. Starts the HTTP reply (sets Content-Type and opens the body).
- Function: kcgi_write(text:string) -> 1/0. Writes a chunk to the response body.
- Function: kcgi_end() -> 1/0. Finalizes the response and frees request resources.
- Opcodes: OP_KCGI_PARSE, OP_KCGI_REPLY_START, OP_KCGI_WRITE, OP_KCGI_END (internal mappings for the functions above).
## Quickstart:
- Configure: cmake -S . -B build -DFUN_WITH_KCGI=ON
- Build: cmake --build build --target fun
- Run example:
- FUN_LIB_DIR="$(pwd)/lib" ./build/fun examples/cgi/hello_kcgi.fun
## Example output:
- Content-Type: text/html; charset=utf-8
- <h1>Hello, Fun!</h1>
## Notes:
- Running outside a real CGI/FastCGI environment may emit RFC warnings (e.g., missing REMOTE_ADDR); these are benign for local testing.
- When FUN_WITH_KCGI is OFF, the helpers behave as no-ops (returning Nil/0) to keep scripts portable.

View file

@ -15,7 +15,6 @@ tags:
- optional
---
- CMake option: FUN_WITH_OPENSSL=ON
- Purpose: provide small crypto helpers backed by OpenSSL. Includes md5, sha256, sha512, ripemd160 helpers.
- Homepage: [https://www.openssl.org/](https://www.openssl.org/){:class="ext"}

View file

@ -18,7 +18,6 @@ tags:
- regex
---
- CMake option: FUN_WITH_PCRE2=ON
- Purpose: Advanced regular expressions via PCRE2.
- Homepage: [https://www.pcre.org/](https://www.pcre.org/){:class="ext"}

View file

@ -16,7 +16,6 @@ tags:
- smart
---
- CMake option: FUN_WITH_PCSC=ON
- Purpose: Access smart card readers/cards via PC/SC (pcsclite).
- Homepage: [https://pcsclite.apdu.fr/](https://pcsclite.apdu.fr/){:class="ext"}

View file

@ -15,7 +15,6 @@ tags:
- sqlite
---
- CMake option: FUN_WITH_SQLITE=ON
- Purpose: Access SQLite databases via the native C API.
- Homepage: [https://www.sqlite.org/](https://www.sqlite.org/){:class="ext"}

View file

@ -16,7 +16,6 @@ tags:
- xml
---
- CMake option: FUN_WITH_XML2=ON
- Purpose: Minimal XML parsing helpers using libxml2.
- Homepage: [http://xmlsoft.org/](http://xmlsoft.org/){:class="ext"}