Added libcurl (cURL) support. (0.30.0)
This commit is contained in:
parent
bb848b6679
commit
4d82d73515
15 changed files with 304 additions and 8 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,6 +7,7 @@ build*
|
|||
cmake*
|
||||
demo_*
|
||||
dist/
|
||||
downloaded.png
|
||||
lib/*.so
|
||||
out/
|
||||
src/*.o
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
10
README.md
10
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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
24
examples/curl_download.fun
Normal file
24
examples/curl_download.fun
Normal 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")
|
||||
25
examples/curl_get_json.fun
Normal file
25
examples/curl_get_json.fun
Normal 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
32
examples/curl_post.fun
Normal 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"]))
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
29
src/parser.c
29
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)++; /* '(' */
|
||||
|
|
|
|||
24
src/vm.c
24
src/vm.c
|
|
@ -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"
|
||||
|
|
|
|||
1
src/vm.h
1
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",
|
||||
|
|
|
|||
47
src/vm/curl/download.c
Normal file
47
src/vm/curl/download.c
Normal 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
33
src/vm/curl/get.c
Normal 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
41
src/vm/curl/post.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue