From 4d82d735150bac4b79c2e74d670ebe1c3d33811a Mon Sep 17 00:00:00 2001 From: hanez Date: Wed, 26 Nov 2025 00:35:22 +0100 Subject: [PATCH] Added libcurl (cURL) support. (0.30.0) --- .gitignore | 1 + CMakeLists.txt | 30 +++++++++++++++++++++++- README.md | 10 ++++---- docs/handbook.md | 7 +++--- examples/curl_download.fun | 24 +++++++++++++++++++ examples/curl_get_json.fun | 25 ++++++++++++++++++++ examples/curl_post.fun | 32 ++++++++++++++++++++++++++ src/bytecode.c | 3 +++ src/bytecode.h | 5 ++++ src/parser.c | 29 +++++++++++++++++++++++ src/vm.c | 24 +++++++++++++++++++ src/vm.h | 1 + src/vm/curl/download.c | 47 ++++++++++++++++++++++++++++++++++++++ src/vm/curl/get.c | 33 ++++++++++++++++++++++++++ src/vm/curl/post.c | 41 +++++++++++++++++++++++++++++++++ 15 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 examples/curl_download.fun create mode 100644 examples/curl_get_json.fun create mode 100644 examples/curl_post.fun create mode 100644 src/vm/curl/download.c create mode 100644 src/vm/curl/get.c create mode 100644 src/vm/curl/post.c diff --git a/.gitignore b/.gitignore index 05bf729..5870479 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build* cmake* demo_* dist/ +downloaded.png lib/*.so out/ src/*.o diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a132f9..8c81c1c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.29.0 LANGUAGES C) +project(fun VERSION 0.30.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -87,6 +87,26 @@ if(FUN_WITH_PCRE2) endif() endif() +# Optional CURL (libcurl) support +option(FUN_WITH_CURL "Enable libcurl HTTP client support" OFF) +set(CURL_INCLUDE_DIRS "") +set(CURL_LINK_LIBS "") +if(FUN_WITH_CURL) + message(STATUS "Building with libcurl support") + add_definitions(-DFUN_WITH_CURL) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(LIBCURL QUIET libcurl) + endif() + if(LIBCURL_FOUND) + list(APPEND CURL_INCLUDE_DIRS ${LIBCURL_INCLUDE_DIRS}) + list(APPEND CURL_LINK_LIBS ${LIBCURL_LIBRARIES}) + include_directories(${CURL_INCLUDE_DIRS}) + else() + list(APPEND CURL_LINK_LIBS curl) + endif() +endif() + # Debug option to enable verbose parser/VM logging option(FUN_DEBUG "Enable extra debug logging in Fun" OFF) @@ -140,6 +160,14 @@ if(PCRE2_LINK_LIBS) target_link_libraries(fun_core PUBLIC ${PCRE2_LINK_LIBS}) endif() +# libcurl include and link (if enabled) +if(CURL_INCLUDE_DIRS) + target_include_directories(fun_core PRIVATE ${CURL_INCLUDE_DIRS}) +endif() +if(CURL_LINK_LIBS) + target_link_libraries(fun_core PUBLIC ${CURL_LINK_LIBS}) +endif() + # Link threads if available on UNIX if(Threads_FOUND) target_link_libraries(fun_core PUBLIC Threads::Threads) diff --git a/README.md b/README.md index 2426bb1..5030290 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ Fun is and will ever be 100% free under the terms of the [Apache-2.0 License](ht ### Extras +- [cURL](https://curl.se/) support builtin using [libcurl](https://curl.se/libcurl/) (optional) ☑ - [JSON](https://www.json.org/) support builtin using [json-c](https://github.com/json-c/json-c) (optional) ☑ -- [ODBC](https://learn.microsoft.com/en-us/sql/odbc/reference/odbc-overview?view=sql-server-ver16) support builtin for flexible database connectivity using [unixODBC](https://www.unixodbc.org/) (optional) ☐ -- [PC/SC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) ☑ +- [PCSC](https://pcscworkgroup.com/) smart card support builtin using [PCSC lite](https://pcsclite.apdu.fr/) (optional) ☑ - [PCRE2](https://pcre2project.github.io/pcre2/) support builtin for Perl-Compatible Regular Expressions (optional) ☑ - [SQLite](https://sqlite.org/) support builtin (optional) ☐ - [Tk](https://www.tcl-lang.org/) support builtin for GUI application development (optional) ☐ @@ -138,9 +138,11 @@ cmake --build build --target fun CMake options you can toggle (all require NAME=VALUE): - FUN_DEBUG=ON|OFF — verbose debug logging in the VM (default OFF) -- FUN_WITH_REPL=ON|OFF — enable building the interactive REPL (default ON) -- FUN_WITH_PCSC=ON|OFF — enable PC/SC smart card support (default OFF) +- FUN_WITH_CURL=ON|OFF — enable CURL support using libcurl (default OFF) - FUN_WITH_JSON=ON|OFF — enable JSON support via json-c (default OFF) +- FUN_WITH_PCRE2=ON|OFF — enable PCRE2 Perl-Compatible Regular Expressions support (default OFF) +- FUN_WITH_PCSC=ON|OFF — enable PCSC smart card support (default OFF) +- FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default ON) That's it! For testing it, run: diff --git a/docs/handbook.md b/docs/handbook.md index a36fddb..69045ed 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -85,10 +85,11 @@ But be sure to build Fun with -DFUN_WITH_REPL=ON. All CMake options must be passed as -DNAME=VALUE: - FUN_DEBUG=ON|OFF — verbose debug logging in the VM (default OFF) -- FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default ON) -- FUN_WITH_PCRE2=ON|OFF — enable PCRE2 Perl-Compatible Regular Expressions support (default OFF) -- FUN_WITH_PCSC=ON|OFF — enable PC/SC smart card support (default OFF) +- FUN_WITH_CURL=ON|OFF — enable CURL support using libcurl (default OFF) - FUN_WITH_JSON=ON|OFF — enable JSON support via json-c (default OFF) +- FUN_WITH_PCRE2=ON|OFF — enable PCRE2 Perl-Compatible Regular Expressions support (default OFF) +- FUN_WITH_PCSC=ON|OFF — enable PCSC smart card support (default OFF) +- FUN_WITH_REPL=ON|OFF — enable the interactive REPL (default ON) If you encounter an error such as: diff --git a/examples/curl_download.fun b/examples/curl_download.fun new file mode 100644 index 0000000..a0f72de --- /dev/null +++ b/examples/curl_download.fun @@ -0,0 +1,24 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-25 + */ + +/* + * Demonstrates curl_download saving a file to disk. + */ + +url = "https://httpbin.org/image/png" +path = "./downloaded.png" +ok = curl_download(url, path) +if ok == 1 + print("Downloaded to " + path) +else + print("Download failed") diff --git a/examples/curl_get_json.fun b/examples/curl_get_json.fun new file mode 100644 index 0000000..713cf88 --- /dev/null +++ b/examples/curl_get_json.fun @@ -0,0 +1,25 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-25 + */ + +/* + * Demonstrates curl_get and JSON.parse working together. + */ + +url = "https://httpbin.org/json" +resp = curl_get(url) +print("Raw length: " + to_string(len(resp))) + +// If JSON support is enabled, parse it +obj = json_parse(resp) +if obj != nil + print("Title: " + obj["slideshow"]["title"]) diff --git a/examples/curl_post.fun b/examples/curl_post.fun new file mode 100644 index 0000000..988c458 --- /dev/null +++ b/examples/curl_post.fun @@ -0,0 +1,32 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2025-11-25 + */ + +/* + * Demonstrates curl_post sending form data and printing response. + */ + +url = "https://httpbin.org/post" +data = "name=Fun&lang=fun" +resp = curl_post(url, data) +print("Response: " + resp) + +// If JSON support is enabled, parse it +obj = json_parse(resp) +if obj != nil + print("Content-Type: " + to_string(obj["headers"]["Content-Type"])) + +if obj != nil + print("Host: " + to_string(obj["headers"]["Host"])) + +if obj != nil + print("Origin: " + to_string(obj["origin"])) diff --git a/src/bytecode.c b/src/bytecode.c index 8955fd0..2120c8e 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -145,6 +145,9 @@ static const char *opcode_name(OpCode op) { case OP_JSON_STRINGIFY: return "JSON_STRINGIFY"; case OP_JSON_FROM_FILE: return "JSON_FROM_FILE"; case OP_JSON_TO_FILE: return "JSON_TO_FILE"; + case OP_CURL_GET: return "CURL_GET"; + case OP_CURL_POST: return "CURL_POST"; + case OP_CURL_DOWNLOAD: return "CURL_DOWNLOAD"; case OP_PCSC_ESTABLISH: return "PCSC_ESTABLISH"; case OP_PCSC_RELEASE: return "PCSC_RELEASE"; case OP_PCSC_LIST_READERS: return "PCSC_LIST_READERS"; diff --git a/src/bytecode.h b/src/bytecode.h index 089c1c7..e38e7f9 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -147,6 +147,11 @@ typedef enum { OP_JSON_FROM_FILE, // pops path string; pushes value (or Nil) OP_JSON_TO_FILE, // pops pretty(bool), value, path; pushes 1/0 + // CURL (libcurl) + OP_CURL_GET, // pops [headers map?], url; pushes response string (or "") + OP_CURL_POST, // pops [headers map?], body string, url; pushes response string (or "") + OP_CURL_DOWNLOAD, // pops [headers map?], path, url; pushes 1/0 + // PCSC (smart card) opcodes OP_PCSC_ESTABLISH, // returns context id (>0) or 0 OP_PCSC_RELEASE, // pops ctx id; returns 1/0 diff --git a/src/parser.c b/src/parser.c index 690cfb3..7f19a44 100644 --- a/src/parser.c +++ b/src/parser.c @@ -778,6 +778,35 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* CURL builtins (minimal interface like JSON) */ + if (strcmp(name, "curl_get") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "curl_get expects (url)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after curl_get arg"); free(name); return 0; } + bytecode_add_instruction(bc, OP_CURL_GET, 0); + free(name); + return 1; + } + if (strcmp(name, "curl_post") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "curl_post expects (url, body)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "curl_post expects (url, body)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "curl_post expects (url, body)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after curl_post args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_CURL_POST, 0); + free(name); + return 1; + } + if (strcmp(name, "curl_download") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "curl_download expects (url, path)"); free(name); return 0; } + if (!consume_char(src, len, pos, ',')) { parser_fail(*pos, "curl_download expects (url, path)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "curl_download expects (url, path)"); free(name); return 0; } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after curl_download args"); free(name); return 0; } + bytecode_add_instruction(bc, OP_CURL_DOWNLOAD, 0); + free(name); + return 1; + } /* PCSC builtins */ if (strcmp(name, "pcsc_establish") == 0) { (*pos)++; /* '(' */ diff --git a/src/vm.c b/src/vm.c index c20c485..16d0a41 100644 --- a/src/vm.c +++ b/src/vm.c @@ -32,6 +32,25 @@ #include #endif +/* Ensure libcurl headers and helpers are defined at file scope (not inside vm_run) */ +#ifdef FUN_WITH_CURL +#include +typedef struct { char *d; size_t n; } FunCurlBuf; +static size_t fun_curl_write_cb(void *ptr, size_t sz, size_t nm, void *ud) { + size_t add = sz * nm; + FunCurlBuf *b = (FunCurlBuf*)ud; + char *p = (char*)realloc(b->d, b->n + add + 1); + if (!p) return 0; + memcpy(p + b->n, ptr, add); + b->d = p; b->n += add; b->d[b->n] = '\0'; + return add; +} +static size_t fun_curl_file_write_cb(void *ptr, size_t sz, size_t nm, void *ud) { + FILE *f = (FILE*)ud; + return fwrite(ptr, sz, nm, f); +} +#endif + /* forward declarations for include mapping used in error reporting */ extern char *preprocess_includes(const char *src); static int map_expanded_line_to_include(const char *path, int line, char *out_path, size_t out_path_cap, int *out_line); @@ -679,6 +698,11 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/json/from_file.c" #include "vm/json/to_file.c" + /* CURL ops */ + #include "vm/curl/get.c" + #include "vm/curl/post.c" + #include "vm/curl/download.c" + /* PCRE2 ops */ #include "vm/pcre2/test.c" #include "vm/pcre2/match.c" diff --git a/src/vm.h b/src/vm.h index b534065..6b685de 100644 --- a/src/vm.h +++ b/src/vm.h @@ -39,6 +39,7 @@ static const char *opcode_names[] = { "THREAD_SPAWN","THREAD_JOIN","SLEEP_MS", "BAND","BOR","BXOR","BNOT","SHL","SHR","ROTL","ROTR", "JSON_PARSE","JSON_STRINGIFY","JSON_FROM_FILE","JSON_TO_FILE", + "CURL_GET","CURL_POST","CURL_DOWNLOAD", "PCSC_ESTABLISH","PCSC_RELEASE","PCSC_LIST_READERS","PCSC_CONNECT","PCSC_DISCONNECT","PCSC_TRANSMIT", "PCRE2_TEST","PCRE2_MATCH","PCRE2_FINDALL", "SOCK_TCP_LISTEN","SOCK_TCP_ACCEPT","SOCK_TCP_CONNECT","SOCK_SEND","SOCK_RECV","SOCK_CLOSE","SOCK_UNIX_LISTEN","SOCK_UNIX_CONNECT", diff --git a/src/vm/curl/download.c b/src/vm/curl/download.c new file mode 100644 index 0000000..51d9db6 --- /dev/null +++ b/src/vm/curl/download.c @@ -0,0 +1,47 @@ +/** + * libcurl DOWNLOAD builtin + */ +case OP_CURL_DOWNLOAD: { +#ifdef FUN_WITH_CURL + Value vpath = pop_value(vm); + Value vurl = pop_value(vm); + char *url = value_to_string_alloc(&vurl); + char *path = value_to_string_alloc(&vpath); + free_value(vurl); + free_value(vpath); + if (!url || !path) { + if (url) free(url); + if (path) free(path); + push_value(vm, make_int(0)); + break; + } + FILE *fp = fopen(path, "wb"); + if (!fp) { + free(url); free(path); + push_value(vm, make_int(0)); + break; + } + CURL *h = curl_easy_init(); + if (!h) { + fclose(fp); + free(url); free(path); + push_value(vm, make_int(0)); + break; + } + curl_easy_setopt(h, CURLOPT_URL, url); + curl_easy_setopt(h, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, fun_curl_file_write_cb); + curl_easy_setopt(h, CURLOPT_WRITEDATA, fp); + CURLcode rc = curl_easy_perform(h); + curl_easy_cleanup(h); + fclose(fp); + free(url); free(path); + if (rc != CURLE_OK) { push_value(vm, make_int(0)); break; } + push_value(vm, make_int(1)); +#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; +} diff --git a/src/vm/curl/get.c b/src/vm/curl/get.c new file mode 100644 index 0000000..7d15b17 --- /dev/null +++ b/src/vm/curl/get.c @@ -0,0 +1,33 @@ +/** + * libcurl GET builtin + */ +case OP_CURL_GET: { +#ifdef FUN_WITH_CURL + Value vurl = pop_value(vm); + char *url = value_to_string_alloc(&vurl); + free_value(vurl); + if (!url) { push_value(vm, make_string("")); break; } + FunCurlBuf buf = { NULL, 0 }; + CURL *h = curl_easy_init(); + if (!h) { free(url); push_value(vm, make_string("")); break; } + curl_easy_setopt(h, CURLOPT_URL, url); + curl_easy_setopt(h, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, fun_curl_write_cb); + curl_easy_setopt(h, CURLOPT_WRITEDATA, &buf); + CURLcode rc = curl_easy_perform(h); + curl_easy_cleanup(h); + free(url); + if (rc != CURLE_OK) { + if (buf.d) free(buf.d); + push_value(vm, make_string("")); + break; + } + Value s = make_string(buf.d ? buf.d : ""); + if (buf.d) free(buf.d); + push_value(vm, s); +#else + Value v = pop_value(vm); free_value(v); + push_value(vm, make_string("")); +#endif + break; +} diff --git a/src/vm/curl/post.c b/src/vm/curl/post.c new file mode 100644 index 0000000..08f0b15 --- /dev/null +++ b/src/vm/curl/post.c @@ -0,0 +1,41 @@ +/** + * libcurl POST builtin + */ +case OP_CURL_POST: { +#ifdef FUN_WITH_CURL + Value vbody = pop_value(vm); + Value vurl = pop_value(vm); + char *url = value_to_string_alloc(&vurl); + char *body = value_to_string_alloc(&vbody); + free_value(vurl); + free_value(vbody); + if (!url) { if (body) free(body); push_value(vm, make_string("")); break; } + if (!body) body = strdup(""); + FunCurlBuf buf = { NULL, 0 }; + CURL *h = curl_easy_init(); + if (!h) { free(url); free(body); push_value(vm, make_string("")); break; } + curl_easy_setopt(h, CURLOPT_URL, url); + curl_easy_setopt(h, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(h, CURLOPT_POST, 1L); + curl_easy_setopt(h, CURLOPT_POSTFIELDS, body); + curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, fun_curl_write_cb); + curl_easy_setopt(h, CURLOPT_WRITEDATA, &buf); + CURLcode rc = curl_easy_perform(h); + curl_easy_cleanup(h); + free(url); + free(body); + if (rc != CURLE_OK) { + if (buf.d) free(buf.d); + push_value(vm, make_string("")); + break; + } + Value s = make_string(buf.d ? buf.d : ""); + if (buf.d) free(buf.d); + push_value(vm, s); +#else + Value a = pop_value(vm); free_value(a); + Value b = pop_value(vm); free_value(b); + push_value(vm, make_string("")); +#endif + break; +}