1
0
Fork 0
forked from fun/fun

Added libcurl (cURL) support. (0.30.0)

This commit is contained in:
Johannes Findeisen 2025-11-26 00:35:22 +01:00
commit 4d82d73515
15 changed files with 304 additions and 8 deletions

1
.gitignore vendored
View file

@ -7,6 +7,7 @@ build*
cmake*
demo_*
dist/
downloaded.png
lib/*.so
out/
src/*.o

View file

@ -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)

View file

@ -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:

View file

@ -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:

View file

@ -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 <you@hanez.org>
* 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")

View file

@ -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 <you@hanez.org>
* 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"])

32
examples/curl_post.fun Normal file
View file

@ -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 <you@hanez.org>
* 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"]))

View file

@ -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";

View file

@ -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

View file

@ -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)++; /* '(' */

View file

@ -32,6 +32,25 @@
#include <pcre2.h>
#endif
/* Ensure libcurl headers and helpers are defined at file scope (not inside vm_run) */
#ifdef FUN_WITH_CURL
#include <curl/curl.h>
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"

View file

@ -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",

47
src/vm/curl/download.c Normal file
View file

@ -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;
}

33
src/vm/curl/get.c Normal file
View file

@ -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;
}

41
src/vm/curl/post.c Normal file
View file

@ -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;
}