-- ffi/curl.lua — libcurl easy interface binding. -- Phase 0: blocking POST with header list and response capture into Lua string. -- Phase 1: SSE streaming via streaming WRITEFUNCTION (this same callback hook). -- See docs/PHASE0.md §6. local ffi = require("ffi") ffi.cdef[[ typedef void CURL; struct curl_slist { char *data; struct curl_slist *next; }; CURL *curl_easy_init(void); void curl_easy_cleanup(CURL *handle); int curl_easy_perform(CURL *handle); const char *curl_easy_strerror(int code); struct curl_slist *curl_slist_append(struct curl_slist *list, const char *string); void curl_slist_free_all(struct curl_slist *list); int curl_easy_setopt(CURL *handle, int option, ...); ]] -- libcurl-dev's unversioned `libcurl.so` symlink isn't assumed; fall back to -- versioned sonames so a runtime-only host (Debian without -dev) just works. local function load_curl() local errs = {} for _, name in ipairs({"curl", "curl.so.4", "curl-gnutls.so.4"}) do local ok, lib = pcall(ffi.load, name) if ok then return lib end errs[#errs+1] = name .. ": " .. tostring(lib) end error("libcurl not loadable: " .. table.concat(errs, "; ")) end local C = load_curl() -- CURLoption codes from curl/curl.h. The bases are: -- CURLOPTTYPE_LONG = 0 -- CURLOPTTYPE_OBJECTPOINT = 10000 -- CURLOPTTYPE_FUNCTIONPOINT = 20000 local OPT = { URL = 10002, POST = 47, POSTFIELDS = 10015, HTTPHEADER = 10023, WRITEFUNCTION = 20011, NOSIGNAL = 99, TIMEOUT_MS = 155, USERAGENT = 10018, } -- Variadic FFI calls demand explicit per-argument types. Pre-cast setopt to -- the three concrete signatures Phase 0 needs; bypasses libffi-flavoured -- variadic dispatch entirely. local setopt_str = ffi.cast("int(*)(void*, int, const char*)", C.curl_easy_setopt) local setopt_long = ffi.cast("int(*)(void*, int, long)", C.curl_easy_setopt) local setopt_ptr = ffi.cast("int(*)(void*, int, void*)", C.curl_easy_setopt) local M = {} -- POST `body` to `url` with `headers` (list of "Name: value" strings) and an -- optional `timeout_ms`. -- Returns: -- string response body on success -- nil, errmsg on libcurl failure (non-zero CURLcode) function M.post(url, body, headers, timeout_ms) local handle = C.curl_easy_init() if handle == nil then return nil, "curl_easy_init returned NULL" end local chunks = {} local write_cb = ffi.cast( "size_t(*)(char*, size_t, size_t, void*)", function(ptr, size, nmemb, _) local n = tonumber(size) * tonumber(nmemb) chunks[#chunks+1] = ffi.string(ptr, n) return n end) local slist = nil for _, h in ipairs(headers or {}) do slist = C.curl_slist_append(slist, h) end setopt_str (handle, OPT.URL, url) setopt_long(handle, OPT.POST, 1) setopt_str (handle, OPT.POSTFIELDS, body) setopt_ptr (handle, OPT.HTTPHEADER, slist) setopt_ptr (handle, OPT.WRITEFUNCTION, write_cb) setopt_long(handle, OPT.NOSIGNAL, 1) setopt_str (handle, OPT.USERAGENT, "aish/0.0 (luajit-ffi)") if timeout_ms then setopt_long(handle, OPT.TIMEOUT_MS, timeout_ms) end local rc = C.curl_easy_perform(handle) local result, err if rc == 0 then result = table.concat(chunks) else err = ffi.string(C.curl_easy_strerror(rc)) end C.curl_easy_cleanup(handle) if slist ~= nil then C.curl_slist_free_all(slist) end write_cb:free() if rc == 0 then return result end return nil, err end return M