Everything working.

This commit is contained in:
retoor 2025-07-30 05:11:49 +02:00
parent fd6b651af7
commit b4884196e1
14 changed files with 1979 additions and 152 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
test.db

View File

@ -1,6 +1,6 @@
make:
gcc main.c wren.c -lcurl -lm -lpthread -o wren
gcc main.c wren.c -g -O0 -fsanitize=address -lcurl -lm -lpthread -lsqlite3 -o wren
make3:

View File

@ -1,135 +0,0 @@
// backend.cpp (Corrected)
#include "httplib.h"
#include "wren.h"
#include <iostream>
#include <string>
// A struct to hold the response data for our foreign object
struct ResponseData {
bool isError;
int statusCode;
std::string body;
};
// --- Response Class Foreign Methods ---
void responseAllocate(WrenVM* vm) {
// This is the constructor for the Response class.
ResponseData* data = (ResponseData*)wrenSetSlotNewForeign(vm, 0, 0, sizeof(ResponseData));
data->isError = false;
data->statusCode = 0;
}
void responseIsError(WrenVM* vm) {
ResponseData* data = (ResponseData*)wrenGetSlotForeign(vm, 0);
wrenSetSlotBool(vm, 0, data->isError);
}
void responseStatusCode(WrenVM* vm) {
ResponseData* data = (ResponseData*)wrenGetSlotForeign(vm, 0);
wrenSetSlotDouble(vm, 0, data->statusCode);
}
void responseBody(WrenVM* vm) {
ResponseData* data = (ResponseData*)wrenGetSlotForeign(vm, 0);
wrenSetSlotBytes(vm, 0, data->body.c_str(), data->body.length());
}
void responseJson(WrenVM* vm) {
// For a real implementation, you would use a JSON library here.
// For this example, we just return the body text.
ResponseData* data = (ResponseData*)wrenGetSlotForeign(vm, 0);
wrenSetSlotBytes(vm, 0, data->body.c_str(), data->body.length());
}
// --- Requests Class Foreign Methods ---
void requestsGet(WrenVM* vm) {
const char* url = wrenGetSlotString(vm, 1);
// TODO: Handle headers from slot 2.
httplib::Client cli("jsonplaceholder.typicode.com");
auto res = cli.Get("/posts/1");
// CHANGED: We need two slots: one for the Response class, one for the new instance.
wrenEnsureSlots(vm, 2);
// CHANGED: Get the 'Response' class from the 'requests' module and put it in slot 1.
wrenGetVariable(vm, "requests", "Response", 1);
// CHANGED: Create a new foreign object instance of the class in slot 1.
// The new instance is placed in slot 0, which becomes the return value.
ResponseData* data = (ResponseData*)wrenSetSlotNewForeign(vm, 0, 1, sizeof(ResponseData));
if (res) {
data->isError = false;
data->statusCode = res->status;
data->body = res->body;
} else {
data->isError = true;
data->statusCode = -1;
data->body = "GET request failed.";
}
}
void requestsPost(WrenVM* vm) {
const char* url = wrenGetSlotString(vm, 1);
const char* body = wrenGetSlotString(vm, 2);
const char* contentType = wrenGetSlotString(vm, 3);
// TODO: Handle headers from slot 4.
httplib::Client cli("jsonplaceholder.typicode.com");
auto res = cli.Post("/posts", body, contentType);
// CHANGED: We need two slots: one for the Response class, one for the new instance.
wrenEnsureSlots(vm, 2);
// CHANGED: Get the 'Response' class from the 'requests' module and put it in slot 1.
wrenGetVariable(vm, "requests", "Response", 1);
// CHANGED: Create a new foreign object instance of the class in slot 1.
// The new instance is placed in slot 0, which becomes the return value.
ResponseData* data = (ResponseData*)wrenSetSlotNewForeign(vm, 0, 1, sizeof(ResponseData));
if (res) {
data->isError = false;
data->statusCode = res->status;
data->body = res->body;
} else {
data->isError = true;
data->statusCode = -1;
data->body = "POST request failed.";
}
}
// --- FFI Binding Functions ---
WrenForeignMethodFn bindForeignMethod(WrenVM* vm, const char* module,
const char* className, bool isStatic, const char* signature) {
if (strcmp(module, "requests") != 0) return NULL;
if (strcmp(className, "Requests") == 0 && isStatic) {
if (strcmp(signature, "get_(_,_)") == 0) return requestsGet;
if (strcmp(signature, "post_(_,_,_,_)") == 0) return requestsPost;
}
if (strcmp(className, "Response") == 0 && !isStatic) {
if (strcmp(signature, "isError") == 0) return responseIsError;
if (strcmp(signature, "statusCode") == 0) return responseStatusCode;
if (strcmp(signature, "body") == 0) return responseBody;
if (strcmp(signature, "json()") == 0) return responseJson;
}
return NULL;
}
WrenForeignClassMethods bindForeignClass(WrenVM* vm, const char* module, const char* className) {
WrenForeignClassMethods methods = {0};
if (strcmp(module, "requests") == 0) {
if (strcmp(className, "Response") == 0) {
methods.allocate = responseAllocate;
}
}
return methods;
}

160
crawler_based.wren Normal file
View File

@ -0,0 +1,160 @@
// crawler.wren
import "requests" for Requests, Response
class Crawler {
construct new(baseUrl) {
if (!baseUrl.endsWith("/")) {
baseUrl = baseUrl + "/"
}
_baseUrl = baseUrl
_toVisit = [baseUrl]
_visited = {}
_inFlight = 0
_startTime = System.clock
}
run() {
System.print("Starting crawler on base URL: %(_baseUrl)")
crawlNext_() // Start the first batch of requests
// The main event loop for the crawler. Keep yielding to the C host
// as long as there is work to do. This keeps the fiber alive.
while (_inFlight > 0 || _toVisit.count > 0) {
Fiber.yield()
}
// Once the loop finishes, all crawling is done.
var duration = System.clock - _startTime
System.print("Crawling finished in %(duration.toString) seconds.")
System.print("%(_visited.count) pages crawled.")
Host.signalDone() // Signal the C host to exit
}
crawlNext_() {
// Throttle requests to be a good web citizen.
var maxInFlight = 4
while (_toVisit.count > 0 && _inFlight < maxInFlight) {
var url = _toVisit.removeAt(0)
if (_visited.containsKey(url)) {
continue
}
_visited[url] = true
_inFlight = _inFlight + 1
System.print("Crawling: %(url) (In flight: %(_inFlight))")
Requests.get(url, null, Fn.new {|err, res|
handleResponse_(err, res, url)
})
}
}
handleResponse_(err, res, url) {
_inFlight = _inFlight - 1
System.print("Finished: %(url) (In flight: %(_inFlight))")
if (err != null) {
System.print("Error crawling %(url): %(err)")
crawlNext_() // A slot opened up, try to crawl more.
return
}
if (res.statusCode >= 400) {
System.print("Failed to crawl %(url) - Status: %(res.statusCode)")
crawlNext_() // A slot opened up, try to crawl more.
return
}
// The response body is already a string, no need to call toString().
var body = res.body
findLinks_(body, url)
crawlNext_() // A slot opened up, try to crawl more.
}
findLinks_(html, pageUrl) {
// A simple but effective way to find href attributes.
// This looks for `href="` followed by any characters that are not a quote.
var links = html.replace("href=\"", "href=\"\n").split("\n")
for (line in links) {
if (line.contains("\"")) {
var parts = line.split("\"")
if (parts.count > 1) {
var link = parts[0]
addUrl_(link, pageUrl)
}
}
}
}
addUrl_(link, pageUrl) {
// Ignore mailto, anchors, and other schemes
if (link.startsWith("mailto:") || link.startsWith("#") || link.startsWith("javascript:")) return
var newUrl = ""
if (link.startsWith("http://") || link.startsWith("https://")) {
newUrl = link
} else if (link.startsWith("/")) {
// Handle absolute paths
var uri = parseUri_(_baseUrl)
newUrl = "%(uri["scheme"])://%(uri["host"])%(link)"
} else {
// Handle relative paths
var lastSlash = pageUrl.lastIndexOf("/")
var base = pageUrl[0..lastSlash]
newUrl = "%(base)/%(link)"
}
// Normalize URL to handle ".." and "."
newUrl = normalizeUrl_(newUrl)
// Only crawl URLs that are within the base URL's scope and haven't been seen.
if (newUrl.startsWith(_baseUrl) && !_visited.containsKey(newUrl) && !_toVisit.contains(newUrl)) {
_toVisit.add(newUrl)
}
}
parseUri_(url) {
var parts = {}
var schemeEnd = url.indexOf("://")
parts["scheme"] = url[0...schemeEnd]
var hostStart = schemeEnd + 3
var hostEnd = url.indexOf("/", hostStart)
if (hostEnd == -1) hostEnd = url.count
parts["host"] = url[hostStart...hostEnd]
return parts
}
normalizeUrl_(url) {
var parts = url.split("/")
var stack = []
for (part in parts) {
if (part == "" || part == ".") continue
if (part == "..") {
if (stack.count > 0) stack.removeAt(-1)
} else {
stack.add(part)
}
}
var result = stack.join("/")
// This is a bit of a hack to fix the double slashes after the scheme
if (url.startsWith("http")) {
return result.replace(":/", "://")
}
return result
}
}
// This class is used to signal the C host application to terminate.
// The C code will look for this static method.
class Host {
foreign static signalDone()
}
// The main entry point for our script.
var mainFiber = Fiber.new {
var crawler = Crawler.new("https://molodetz.nl")
crawler.run()
}

18
io.wren Normal file
View File

@ -0,0 +1,18 @@
// io.wren
foreign class File {
// Checks if a file exists at the given path.
foreign static exists(path)
// Deletes the file at the given path. Returns true if successful.
foreign static delete(path)
// Reads the entire contents of the file at the given path.
// Returns the contents as a string, or null if the file could not be read.
foreign static read(path)
// Writes the given string contents to the file at the given path.
// Returns true if the write was successful.
foreign static write(path, contents)
}

119
io_backend.c Normal file
View File

@ -0,0 +1,119 @@
#include "wren.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/stat.h>
#endif
// Helper to validate that the value in a slot is a string.
static bool io_validate_string(WrenVM* vm, int slot, const char* name) {
if (wrenGetSlotType(vm, slot) == WREN_TYPE_STRING) return true;
// The error is placed in slot 0, which becomes the return value.
wrenSetSlotString(vm, 0, "Argument must be a string.");
wrenAbortFiber(vm, 0);
return false;
}
// --- File Class Foreign Methods ---
void fileExists(WrenVM* vm) {
if (!io_validate_string(vm, 1, "path")) return;
const char* path = wrenGetSlotString(vm, 1);
#ifdef _WIN32
DWORD attrib = GetFileAttributes(path);
wrenSetSlotBool(vm, 0, (attrib != INVALID_FILE_ATTRIBUTES && !(attrib & FILE_ATTRIBUTE_DIRECTORY)));
#else
struct stat buffer;
wrenSetSlotBool(vm, 0, (stat(path, &buffer) == 0));
#endif
}
void fileDelete(WrenVM* vm) {
if (!io_validate_string(vm, 1, "path")) return;
const char* path = wrenGetSlotString(vm, 1);
if (remove(path) == 0) {
wrenSetSlotBool(vm, 0, true);
} else {
wrenSetSlotBool(vm, 0, false);
}
}
void fileRead(WrenVM* vm) {
if (!io_validate_string(vm, 1, "path")) return;
const char* path = wrenGetSlotString(vm, 1);
FILE* file = fopen(path, "rb");
if (file == NULL) {
wrenSetSlotNull(vm, 0);
return;
}
fseek(file, 0L, SEEK_END);
size_t fileSize = ftell(file);
rewind(file);
char* buffer = (char*)malloc(fileSize + 1);
if (buffer == NULL) {
fclose(file);
wrenSetSlotString(vm, 0, "Could not allocate memory to read file.");
wrenAbortFiber(vm, 0);
return;
}
size_t bytesRead = fread(buffer, sizeof(char), fileSize, file);
if (bytesRead < fileSize) {
free(buffer);
fclose(file);
wrenSetSlotString(vm, 0, "Could not read entire file.");
wrenAbortFiber(vm, 0);
return;
}
buffer[bytesRead] = '\0';
fclose(file);
wrenSetSlotBytes(vm, 0, buffer, bytesRead);
free(buffer);
}
void fileWrite(WrenVM* vm) {
if (!io_validate_string(vm, 1, "path")) return;
if (!io_validate_string(vm, 2, "contents")) return;
const char* path = wrenGetSlotString(vm, 1);
int length;
const char* contents = wrenGetSlotBytes(vm, 2, &length);
FILE* file = fopen(path, "wb");
if (file == NULL) {
wrenSetSlotBool(vm, 0, false);
return;
}
size_t bytesWritten = fwrite(contents, sizeof(char), length, file);
fclose(file);
wrenSetSlotBool(vm, 0, bytesWritten == (size_t)length);
}
// --- FFI Binding ---
WrenForeignMethodFn bindIoForeignMethod(WrenVM* vm, const char* module, const char* className, bool isStatic, const char* signature) {
if (strcmp(module, "io") != 0) return NULL;
if (strcmp(className, "File") == 0 && isStatic) {
if (strcmp(signature, "exists(_)") == 0) return fileExists;
if (strcmp(signature, "delete(_)") == 0) return fileDelete;
if (strcmp(signature, "read(_)") == 0) return fileRead;
if (strcmp(signature, "write(_,_)") == 0) return fileWrite;
}
return NULL;
}

46
main.c
View File

@ -11,9 +11,14 @@
#endif
#include "wren.h"
// It's common practice to include the C source directly for small-to-medium
// modules like this to simplify the build process.
#include "requests_backend.c"
#include "socket_backend.c"
#include "string_backend.c" // Include the new string backend
#include "string_backend.c"
#include "sqlite3_backend.c"
#include "io_backend.c"
// --- Global flag to control the main loop ---
static volatile bool g_mainFiberIsDone = false;
@ -82,39 +87,41 @@ static WrenLoadModuleResult loadModule(WrenVM* vm, const char* name) {
// --- Combined Foreign Function Binders ---
WrenForeignMethodFn combinedBindForeignMethod(WrenVM* vm, const char* module, const char* className, bool isStatic, const char* signature) {
// Delegate to the socket backend's binder
if (strcmp(module, "socket") == 0) {
return bindSocketForeignMethod(vm, module, className, isStatic, signature);
}
// Delegate to the requests backend's binder
if (strcmp(module, "requests") == 0) {
return bindForeignMethod(vm, module, className, isStatic, signature);
}
if (strcmp(module, "sqlite") == 0) {
return bindSqliteForeignMethod(vm, module, className, isStatic, signature);
}
if (strcmp(module, "io") == 0) {
return bindIoForeignMethod(vm, module, className, isStatic, signature);
}
if (strcmp(className, "String") == 0) {
return bindStringForeignMethod(vm, module, className, isStatic, signature);
}
// Handle host-specific methods
if (strcmp(module, "main") == 0 && strcmp(className, "Host") == 0 && isStatic) {
if (strcmp(signature, "signalDone()") == 0) return hostSignalDone;
}
return NULL;
}
WrenForeignClassMethods combinedBindForeignClass(WrenVM* vm, const char* module, const char* className) {
// Delegate to the socket backend's class binder
if (strcmp(module, "socket") == 0) {
return bindSocketForeignClass(vm, module, className);
}
// Delegate to the requests backend's class binder
if (strcmp(module, "requests") == 0) {
return bindForeignClass(vm, module, className);
}
if (strcmp(module, "sqlite") == 0) {
return bindSqliteForeignClass(vm, module, className);
}
if (strcmp(module, "io") == 0) {
WrenForeignClassMethods methods = {0, 0};
return methods;
}
WrenForeignClassMethods methods = {0, 0};
return methods;
}
@ -127,7 +134,6 @@ int main(int argc, char* argv[]) {
return 1;
}
// Initialize libcurl for the requests module
curl_global_init(CURL_GLOBAL_ALL);
WrenConfiguration config;
@ -140,15 +146,17 @@ int main(int argc, char* argv[]) {
WrenVM* vm = wrenNewVM(&config);
// ** Initialize BOTH managers **
// ** Initialize ALL managers **
socketManager_create(vm);
httpManager_create(vm);
dbManager_create(vm);
char* mainSource = readFile(argv[1]);
if (!mainSource) {
fprintf(stderr, "Could not open script: %s\n", argv[1]);
socketManager_destroy();
httpManager_destroy();
dbManager_destroy();
wrenFreeVM(vm);
curl_global_cleanup();
return 1;
@ -160,6 +168,7 @@ int main(int argc, char* argv[]) {
if (g_mainFiberIsDone) {
socketManager_destroy();
httpManager_destroy();
dbManager_destroy();
wrenFreeVM(vm);
curl_global_cleanup();
return 1;
@ -172,14 +181,16 @@ int main(int argc, char* argv[]) {
// === Main Event Loop ===
while (!g_mainFiberIsDone) {
// ** Process completions for BOTH managers **
// ** Process completions for ALL managers **
socketManager_processCompletions();
httpManager_processCompletions();
dbManager_processCompletions();
// Resume the main Wren fiber
wrenEnsureSlots(vm, 1);
wrenSetSlotHandle(vm, 0, mainFiberHandle);
WrenInterpretResult result = wrenCall(vm, callHandle);
if (result == WREN_RESULT_RUNTIME_ERROR) {
g_mainFiberIsDone = true;
}
@ -195,13 +206,15 @@ int main(int argc, char* argv[]) {
// Process any final completions before shutting down
socketManager_processCompletions();
httpManager_processCompletions();
dbManager_processCompletions();
wrenReleaseHandle(vm, mainFiberHandle);
wrenReleaseHandle(vm, callHandle);
// ** Destroy BOTH managers **
// ** Destroy ALL managers **
socketManager_destroy();
httpManager_destroy();
dbManager_destroy();
wrenFreeVM(vm);
curl_global_cleanup();
@ -209,3 +222,4 @@ int main(int argc, char* argv[]) {
printf("\nHost application finished.\n");
return 0;
}

View File

@ -614,6 +614,7 @@ WrenForeignClassMethods bindSocketForeignClass(WrenVM* vm, const char* module, c
#include "wren.h"
#include "requests_backend.c"
#include "socket_backend.c"
#include "string_backend.c" // Include the new string backend
// --- Global flag to control the main loop ---
static volatile bool g_mainFiberIsDone = false;
@ -692,6 +693,10 @@ WrenForeignMethodFn combinedBindForeignMethod(WrenVM* vm, const char* module, co
return bindForeignMethod(vm, module, className, isStatic, signature);
}
if (strcmp(className, "String") == 0) {
return bindStringForeignMethod(vm, module, className, isStatic, signature);
}
// Handle host-specific methods
if (strcmp(module, "main") == 0 && strcmp(className, "Host") == 0 && isStatic) {
if (strcmp(signature, "signalDone()") == 0) return hostSignalDone;
@ -1755,6 +1760,161 @@ WREN_API void wrenSetUserData(WrenVM* vm, void* userData);
// End of wren.h
// Start of string_backend.c
// string_backend.c
#include "wren.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
// Helper to validate that the value in a slot is a string.
static bool validateString(WrenVM* vm, int slot, const char* name) {
if (wrenGetSlotType(vm, slot) == WREN_TYPE_STRING) return true;
wrenSetSlotString(vm, 0, "Argument must be a string.");
wrenAbortFiber(vm, 0);
return false;
}
// Implements String.endsWith(_).
void stringEndsWith(WrenVM* vm) {
if (!validateString(vm, 1, "Suffix")) return;
int stringLength, suffixLength;
const char* string = wrenGetSlotBytes(vm, 0, &stringLength);
const char* suffix = wrenGetSlotBytes(vm, 1, &suffixLength);
if (suffixLength > stringLength) {
wrenSetSlotBool(vm, 0, false);
return;
}
wrenSetSlotBool(vm, 0, memcmp(string + stringLength - suffixLength, suffix, suffixLength) == 0);
}
// Implements String.startsWith(_).
void stringStartsWith(WrenVM* vm) {
if (!validateString(vm, 1, "Prefix")) return;
int stringLength, prefixLength;
const char* string = wrenGetSlotBytes(vm, 0, &stringLength);
const char* prefix = wrenGetSlotBytes(vm, 1, &prefixLength);
if (prefixLength > stringLength) {
wrenSetSlotBool(vm, 0, false);
return;
}
wrenSetSlotBool(vm, 0, memcmp(string, prefix, prefixLength) == 0);
}
// Implements String.replace(_, _).
void stringReplace(WrenVM* vm) {
if (!validateString(vm, 1, "From")) return;
if (!validateString(vm, 2, "To")) return;
int haystackLen, fromLen, toLen;
const char* haystack = wrenGetSlotBytes(vm, 0, &haystackLen);
const char* from = wrenGetSlotBytes(vm, 1, &fromLen);
const char* to = wrenGetSlotBytes(vm, 2, &toLen);
if (fromLen == 0) {
wrenSetSlotString(vm, 0, haystack); // Nothing to replace.
return;
}
// Allocate a buffer for the result. This is a rough estimate.
// A more robust implementation would calculate the exact size or reallocate.
size_t resultCapacity = haystackLen * (toLen > fromLen ? toLen / fromLen + 1 : 1) + 1;
char* result = (char*)malloc(resultCapacity);
if (!result) {
// Handle allocation failure
wrenSetSlotString(vm, 0, "Memory allocation failed.");
wrenAbortFiber(vm, 0);
return;
}
char* dest = result;
const char* p = haystack;
const char* end = haystack + haystackLen;
while (p < end) {
const char* found = strstr(p, from);
if (found) {
size_t len = found - p;
memcpy(dest, p, len);
dest += len;
memcpy(dest, to, toLen);
dest += toLen;
p = found + fromLen;
} else {
size_t len = end - p;
memcpy(dest, p, len);
dest += len;
p = end;
}
}
*dest = '\0';
wrenSetSlotString(vm, 0, result);
free(result);
}
// Implements String.split(_).
void stringSplit(WrenVM* vm) {
if (!validateString(vm, 1, "Delimiter")) return;
int haystackLen, delimLen;
const char* haystack = wrenGetSlotBytes(vm, 0, &haystackLen);
const char* delim = wrenGetSlotBytes(vm, 1, &delimLen);
if (delimLen == 0) {
wrenSetSlotString(vm, 0, "Delimiter cannot be empty.");
wrenAbortFiber(vm, 0);
return;
}
wrenSetSlotNewList(vm, 0); // Create the list to return.
const char* p = haystack;
const char* end = haystack + haystackLen;
while (p < end) {
const char* found = strstr(p, delim);
if (found) {
wrenSetSlotBytes(vm, 1, p, found - p);
wrenInsertInList(vm, 0, -1, 1);
p = found + delimLen;
} else {
wrenSetSlotBytes(vm, 1, p, end - p);
wrenInsertInList(vm, 0, -1, 1);
p = end;
}
}
// If the string ends with the delimiter, add an empty string.
if (haystackLen > 0 && (haystackLen >= delimLen) &&
strcmp(haystack + haystackLen - delimLen, delim) == 0) {
wrenSetSlotBytes(vm, 1, "", 0);
wrenInsertInList(vm, 0, -1, 1);
}
}
// Binds the foreign methods for the String class.
WrenForeignMethodFn bindStringForeignMethod(WrenVM* vm, const char* module, const char* className, bool isStatic, const char* signature) {
if (strcmp(module, "main") != 0 && strcmp(module, "core") != 0) return NULL;
if (strcmp(className, "String") != 0 || isStatic) return NULL;
if (strcmp(signature, "endsWith(_)") == 0) return stringEndsWith;
if (strcmp(signature, "startsWith(_)") == 0) return stringStartsWith;
if (strcmp(signature, "replace(_,_)") == 0) return stringReplace;
if (strcmp(signature, "split(_)") == 0) return stringSplit;
return NULL;
}
// End of string_backend.c
// Start of async_http.c
#include "httplib.h"
#include "wren.h"

36
sqlite.wren Normal file
View File

@ -0,0 +1,36 @@
// sqlite.wren
foreign class Database {
construct new() {}
foreign open_(path, callback)
foreign exec_(sql, callback)
foreign query_(sql, callback)
foreign close_(callback)
// Opens a database at the given path.
// The callback will be invoked with (err).
open(path, callback) {
open_(path, callback)
}
// Executes a SQL statement that does not return rows.
// The callback will be invoked with (err).
exec(sql, callback) {
exec_(sql, callback)
}
// Executes a SQL query that returns rows.
// The callback will be invoked with (err, rows).
// `rows` will be a list of maps, where each map represents a row.
query(sql, callback) {
query_(sql, callback)
}
// Closes the database connection.
// The callback will be invoked with (err).
close(callback) {
close_(callback)
}
}

739
sqlite3_backend.bak Normal file
View File

@ -0,0 +1,739 @@
#include "wren.h"
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#ifdef _WIN32
#include <windows.h>
typedef HANDLE thread_t;
typedef CRITICAL_SECTION mutex_t;
typedef CONDITION_VARIABLE cond_t;
#define GET_THREAD_ID() GetCurrentThreadId()
#else
#include <pthread.h>
typedef pthread_t thread_t;
typedef pthread_mutex_t mutex_t;
typedef pthread_cond_t cond_t;
#define GET_THREAD_ID() pthread_self()
#endif
#define TRACE() printf("[TRACE] %s:%d\n", __FUNCTION__, __LINE__)
// --- Data Structures ---
typedef enum {
DB_OP_OPEN,
DB_OP_EXEC,
DB_OP_QUERY,
DB_OP_CLOSE
} DBOp;
typedef struct {
sqlite3* db;
} DatabaseData;
// C-side representation of query results to pass from worker to main thread.
typedef struct DbValue {
int type;
union {
double num;
struct {
char* text;
int length;
} str;
} as;
} DbValue;
typedef struct DbRow {
char** columns;
DbValue* values;
int count;
struct DbRow* next;
} DbRow;
typedef struct DbContext {
WrenVM* vm;
DBOp operation;
WrenHandle* callback;
WrenHandle* dbHandle;
char* path;
sqlite3* newDb;
char* sql;
sqlite3* db;
bool success;
char* errorMessage;
DbRow* resultRows;
struct DbContext* next;
} DbContext;
// --- Thread-Safe Queue ---
typedef struct {
DbContext *head, *tail;
mutex_t mutex;
cond_t cond;
} DbThreadSafeQueue;
void db_queue_init(DbThreadSafeQueue* q) {
TRACE();
q->head = q->tail = NULL;
#ifdef _WIN32
InitializeCriticalSection(&q->mutex);
InitializeConditionVariable(&q->cond);
#else
pthread_mutex_init(&q->mutex, NULL);
pthread_cond_init(&q->cond, NULL);
#endif
TRACE();
}
void db_queue_destroy(DbThreadSafeQueue* q) {
TRACE();
#ifdef _WIN32
DeleteCriticalSection(&q->mutex);
#else
pthread_mutex_destroy(&q->mutex);
pthread_cond_destroy(&q->cond);
#endif
TRACE();
}
void db_queue_push(DbThreadSafeQueue* q, DbContext* context) {
TRACE();
#ifdef _WIN32
EnterCriticalSection(&q->mutex);
#else
pthread_mutex_lock(&q->mutex);
#endif
TRACE();
if(context) context->next = NULL;
TRACE();
if (q->tail) q->tail->next = context;
else q->head = context;
TRACE();
q->tail = context;
TRACE();
#ifdef _WIN32
WakeConditionVariable(&q->cond);
LeaveCriticalSection(&q->mutex);
#else
pthread_cond_signal(&q->cond);
pthread_mutex_unlock(&q->mutex);
#endif
TRACE();
}
DbContext* db_queue_pop(DbThreadSafeQueue* q) {
TRACE();
#ifdef _WIN32
EnterCriticalSection(&q->mutex);
while (q->head == NULL) {
SleepConditionVariableCS(&q->cond, &q->mutex, INFINITE);
}
#else
pthread_mutex_lock(&q->mutex);
while (q->head == NULL) {
pthread_cond_wait(&q->cond, &q->mutex);
}
#endif
TRACE();
DbContext* context = q->head;
TRACE();
q->head = q->head->next;
TRACE();
if (q->head == NULL) q->tail = NULL;
TRACE();
#ifdef _WIN32
LeaveCriticalSection(&q->mutex);
#else
pthread_mutex_unlock(&q->mutex);
#endif
TRACE();
return context;
}
bool db_queue_empty(DbThreadSafeQueue* q) {
TRACE();
bool empty;
#ifdef _WIN32
EnterCriticalSection(&q->mutex);
empty = (q->head == NULL);
LeaveCriticalSection(&q->mutex);
#else
pthread_mutex_lock(&q->mutex);
empty = (q->head == NULL);
pthread_mutex_unlock(&q->mutex);
#endif
TRACE();
return empty;
}
// --- Async DB Manager ---
typedef struct {
WrenVM* vm;
volatile bool running;
thread_t threads[2];
DbThreadSafeQueue requestQueue;
DbThreadSafeQueue completionQueue;
} AsyncDbManager;
static AsyncDbManager* dbManager = NULL;
void free_db_result_rows(DbRow* rows) {
TRACE();
while (rows) {
TRACE();
DbRow* next = rows->next;
if (rows->columns) {
TRACE();
for (int i = 0; i < rows->count; i++) {
free(rows->columns[i]);
}
free(rows->columns);
}
if (rows->values) {
TRACE();
for (int i = 0; i < rows->count; i++) {
if (rows->values[i].type == SQLITE_TEXT || rows->values[i].type == SQLITE_BLOB) {
free(rows->values[i].as.str.text);
}
}
free(rows->values);
}
free(rows);
rows = next;
}
TRACE();
}
void free_db_context(DbContext* context) {
TRACE();
if (context == NULL) { TRACE(); return; }
TRACE();
free(context->path);
TRACE();
free(context->sql);
TRACE();
free(context->errorMessage);
TRACE();
if (context->dbHandle) wrenReleaseHandle(context->vm, context->dbHandle);
TRACE();
if (context->callback) wrenReleaseHandle(context->vm, context->callback);
TRACE();
if (context->resultRows) free_db_result_rows(context->resultRows);
TRACE();
free(context);
TRACE();
}
static void set_context_error(DbContext* context, const char* message) {
TRACE();
if (context == NULL) { TRACE(); return; }
TRACE();
context->success = false;
TRACE();
if (context->errorMessage) {
free(context->errorMessage);
}
TRACE();
if (message) {
context->errorMessage = strdup(message);
} else {
context->errorMessage = strdup("An unknown database error occurred (possibly out of memory).");
}
TRACE();
}
#ifdef _WIN32
DWORD WINAPI dbWorkerThread(LPVOID arg);
#else
void* dbWorkerThread(void* arg);
#endif
void dbManager_create(WrenVM* vm) {
TRACE();
if (dbManager != NULL) { TRACE(); return; }
TRACE();
dbManager = (AsyncDbManager*)malloc(sizeof(AsyncDbManager));
TRACE();
if (dbManager == NULL) { TRACE(); return; }
TRACE();
dbManager->vm = vm;
TRACE();
dbManager->running = true;
TRACE();
db_queue_init(&dbManager->requestQueue);
TRACE();
db_queue_init(&dbManager->completionQueue);
TRACE();
for (int i = 0; i < 2; ++i) {
#ifdef _WIN32
dbManager->threads[i] = CreateThread(NULL, 0, dbWorkerThread, dbManager, 0, NULL);
#else
pthread_create(&dbManager->threads[i], NULL, dbWorkerThread, dbManager);
#endif
}
TRACE();
}
void dbManager_destroy() {
TRACE();
if (!dbManager) { TRACE(); return; }
TRACE();
dbManager->running = false;
TRACE();
for (int i = 0; i < 2; ++i) {
db_queue_push(&dbManager->requestQueue, NULL);
}
TRACE();
for (int i = 0; i < 2; ++i) {
#ifdef _WIN32
WaitForSingleObject(dbManager->threads[i], INFINITE);
CloseHandle(dbManager->threads[i]);
#else
pthread_join(dbManager->threads[i], NULL);
#endif
}
TRACE();
while(!db_queue_empty(&dbManager->requestQueue)) free_db_context(db_queue_pop(&dbManager->requestQueue));
TRACE();
while(!db_queue_empty(&dbManager->completionQueue)) free_db_context(db_queue_pop(&dbManager->completionQueue));
TRACE();
db_queue_destroy(&dbManager->requestQueue);
TRACE();
db_queue_destroy(&dbManager->completionQueue);
TRACE();
free(dbManager);
TRACE();
dbManager = NULL;
TRACE();
}
void dbManager_processCompletions() {
TRACE();
if (!dbManager || !dbManager->vm || db_queue_empty(&dbManager->completionQueue)) { TRACE(); return; }
TRACE();
WrenHandle* callHandle = wrenMakeCallHandle(dbManager->vm, "call(_,_)");
TRACE();
if (callHandle == NULL) { TRACE(); return; }
TRACE();
while (!db_queue_empty(&dbManager->completionQueue)) {
TRACE();
DbContext* context = db_queue_pop(&dbManager->completionQueue);
TRACE();
if (context == NULL) { TRACE(); continue; }
TRACE();
if (context->success && context->dbHandle) {
TRACE();
wrenEnsureSlots(dbManager->vm, 1);
TRACE();
wrenSetSlotHandle(dbManager->vm, 0, context->dbHandle);
TRACE();
DatabaseData* dbData = (DatabaseData*)wrenGetSlotForeign(dbManager->vm, 0);
TRACE();
if (dbData) {
TRACE();
if (context->operation == DB_OP_OPEN) {
dbData->db = context->newDb;
} else if (context->operation == DB_OP_CLOSE) {
dbData->db = NULL;
}
}
}
TRACE();
if (context->callback == NULL) {
TRACE();
free_db_context(context);
continue;
}
TRACE();
wrenEnsureSlots(dbManager->vm, 3);
TRACE();
wrenSetSlotHandle(dbManager->vm, 0, context->callback);
TRACE();
if (context->success) {
TRACE();
wrenSetSlotNull(dbManager->vm, 1); // error
TRACE();
if (context->resultRows) {
TRACE();
wrenSetSlotNewList(dbManager->vm, 2);
DbRow* row = context->resultRows;
while(row) {
wrenSetSlotNewMap(dbManager->vm, 1);
for (int i = 0; i < row->count; i++) {
wrenSetSlotString(dbManager->vm, 0, row->columns[i]); // key
DbValue* val = &row->values[i];
switch (val->type) {
case SQLITE_INTEGER: case SQLITE_FLOAT:
wrenSetSlotDouble(dbManager->vm, 1, val->as.num); break;
case SQLITE_TEXT:
wrenSetSlotBytes(dbManager->vm, 1, val->as.str.text, val->as.str.length); break;
case SQLITE_BLOB:
wrenSetSlotBytes(dbManager->vm, 1, val->as.str.text, val->as.str.length); break;
case SQLITE_NULL:
wrenSetSlotNull(dbManager->vm, 1); break;
}
wrenSetMapValue(dbManager->vm, 1, 0, 1);
}
wrenInsertInList(dbManager->vm, 2, -1, 1);
row = row->next;
}
} else {
TRACE();
wrenSetSlotNull(dbManager->vm, 2);
}
} else {
TRACE();
wrenSetSlotString(dbManager->vm, 1, context->errorMessage ? context->errorMessage : "Unknown error.");
TRACE();
wrenSetSlotNull(dbManager->vm, 2);
}
TRACE();
wrenCall(dbManager->vm, callHandle);
TRACE();
free_db_context(context);
}
TRACE();
wrenReleaseHandle(dbManager->vm, callHandle);
TRACE();
}
// --- Worker Thread ---
#ifdef _WIN32
DWORD WINAPI dbWorkerThread(LPVOID arg) {
#else
void* dbWorkerThread(void* arg) {
#endif
TRACE();
AsyncDbManager* manager = (AsyncDbManager*)arg;
TRACE();
while (manager->running) {
TRACE();
DbContext* context = db_queue_pop(&manager->requestQueue);
TRACE();
if (!context || !manager->running) {
TRACE();
if (context) free_db_context(context);
break;
}
TRACE();
switch (context->operation) {
case DB_OP_OPEN: {
TRACE();
int rc = sqlite3_open_v2(context->path, &context->newDb,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
NULL);
TRACE();
if (rc != SQLITE_OK) {
TRACE();
set_context_error(context, sqlite3_errmsg(context->newDb));
TRACE();
sqlite3_close(context->newDb);
TRACE();
context->newDb = NULL;
} else {
TRACE();
context->success = true;
}
break;
}
case DB_OP_EXEC: {
TRACE();
if (!context->db) {
TRACE();
set_context_error(context, "Database is not open.");
break;
}
TRACE();
char* err = NULL;
TRACE();
int rc = sqlite3_exec(context->db, context->sql, 0, 0, &err);
TRACE();
if (rc != SQLITE_OK) {
TRACE();
set_context_error(context, err);
TRACE();
sqlite3_free(err);
} else {
TRACE();
context->success = true;
}
break;
}
case DB_OP_QUERY: {
TRACE();
if (!context->db) {
TRACE();
set_context_error(context, "Database is not open.");
break;
}
TRACE();
sqlite3_stmt* stmt;
TRACE();
int rc = sqlite3_prepare_v2(context->db, context->sql, -1, &stmt, 0);
TRACE();
if (rc != SQLITE_OK) {
TRACE();
set_context_error(context, sqlite3_errmsg(context->db));
break;
}
TRACE();
int colCount = sqlite3_column_count(stmt);
DbRow* head = NULL;
DbRow* tail = NULL;
bool oom = false;
TRACE();
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
TRACE();
DbRow* row = (DbRow*)calloc(1, sizeof(DbRow));
if (row == NULL) { oom = true; break; }
row->count = colCount;
row->columns = (char**)malloc(sizeof(char*) * colCount);
row->values = (DbValue*)malloc(sizeof(DbValue) * colCount);
if (row->columns == NULL || row->values == NULL) {
free(row->columns); free(row->values); free(row);
oom = true; break;
}
for (int i = 0; i < colCount; i++) {
const char* colName = sqlite3_column_name(stmt, i);
row->columns[i] = colName ? strdup(colName) : strdup("");
if (row->columns[i] == NULL) {
for (int j = 0; j < i; j++) free(row->columns[j]);
free(row->columns); free(row->values); free(row);
oom = true; goto query_loop_end;
}
DbValue* val = &row->values[i];
val->type = sqlite3_column_type(stmt, i);
switch (val->type) {
case SQLITE_INTEGER: case SQLITE_FLOAT:
val->as.num = sqlite3_column_double(stmt, i);
break;
case SQLITE_TEXT: case SQLITE_BLOB: {
const void* blob = sqlite3_column_blob(stmt, i);
int len = sqlite3_column_bytes(stmt, i);
val->as.str.text = (char*)malloc(len);
if (val->as.str.text == NULL) {
for (int j = 0; j <= i; j++) free(row->columns[j]);
free(row->columns); free(row->values); free(row);
oom = true; goto query_loop_end;
}
memcpy(val->as.str.text, blob, len);
val->as.str.length = len;
break;
}
case SQLITE_NULL: break;
}
}
if (head == NULL) head = tail = row;
else { tail->next = row; tail = row; }
}
query_loop_end:;
TRACE();
if (oom) {
set_context_error(context, "Memory allocation failed during query.");
free_db_result_rows(head);
} else if (rc != SQLITE_DONE) {
set_context_error(context, sqlite3_errmsg(context->db));
free_db_result_rows(head);
} else {
context->success = true;
context->resultRows = head;
}
TRACE();
sqlite3_finalize(stmt);
break;
}
case DB_OP_CLOSE: {
TRACE();
if (context->db) {
sqlite3_close(context->db);
}
TRACE();
context->success = true;
break;
}
}
TRACE();
db_queue_push(&manager->completionQueue, context);
}
TRACE();
return 0;
}
// --- Wren FFI ---
void dbAllocate(WrenVM* vm) {
TRACE();
DatabaseData* data = (DatabaseData*)wrenSetSlotNewForeign(vm, 0, 0, sizeof(DatabaseData));
if (data) data->db = NULL;
TRACE();
}
void dbFinalize(void* data) {
TRACE();
DatabaseData* dbData = (DatabaseData*)data;
if (dbData && dbData->db) {
sqlite3_close(dbData->db);
}
TRACE();
}
static void ensureDbManager(WrenVM* vm)
{
if (!dbManager) dbManager_create(vm);
}
static void create_db_context(WrenVM* vm,
DBOp op,
int sqlSlot,
int cbSlot)
{
TRACE();
DbContext* context = calloc(1, sizeof(*context));
if (!context)
{
wrenSetSlotString(vm, 0, "Out of memory creating DbContext.");
wrenAbortFiber(vm, 0);
return;
}
context->vm = vm;
context->operation = op;
/* ------------------------------------------------------------------ */
/* 1. Grab the SQL bytes *first* (before any API call that might GC) */
/* ------------------------------------------------------------------ */
if (sqlSlot != -1)
{
if (wrenGetSlotType(vm, sqlSlot) != WREN_TYPE_STRING)
{
wrenSetSlotString(vm, 0, "SQL argument must be a string.");
wrenAbortFiber(vm, 0);
free(context);
return;
}
int len = 0;
const char* bytes = wrenGetSlotBytes(vm, sqlSlot, &len);
context->sql = malloc((size_t)len + 1);
if (!context->sql)
{
wrenSetSlotString(vm, 0, "Out of memory copying SQL string.");
wrenAbortFiber(vm, 0);
free(context);
return;
}
memcpy(context->sql, bytes, (size_t)len);
context->sql[len] = '\0'; /* NULterminate for SQLite */
}
/* -------------------------------------------------------------- */
/* 2. Now take the handles these *may* allocate / trigger GC */
/* -------------------------------------------------------------- */
context->dbHandle = wrenGetSlotHandle(vm, 0);
context->callback = wrenGetSlotHandle(vm, cbSlot);
/* -------------------------------------------------------------- */
/* 3. Stash live DB pointer and queue the request */
/* -------------------------------------------------------------- */
DatabaseData* dbData = wrenGetSlotForeign(vm, 0);
if (!dbData)
{
set_context_error(context, "Internal error: bad Database object.");
db_queue_push(&dbManager->requestQueue, context);
return;
}
context->db = dbData->db;
db_queue_push(&dbManager->requestQueue, context);
TRACE();
}
void dbOpen(WrenVM* vm) {
ensureDbManager(vm);
TRACE();
DbContext* context = (DbContext*)calloc(1, sizeof(DbContext));
if (context == NULL) {
wrenSetSlotString(vm, 0, "Failed to allocate memory for database operation.");
wrenAbortFiber(vm, 0);
return;
}
context->vm = vm;
context->operation = DB_OP_OPEN;
const char* path_str = wrenGetSlotString(vm, 1);
if (path_str) context->path = strdup(path_str);
if (context->path == NULL) {
free(context);
wrenSetSlotString(vm, 0, "Failed to allocate memory for database path.");
wrenAbortFiber(vm, 0);
return;
}
context->dbHandle = wrenGetSlotHandle(vm, 0);
context->callback = wrenGetSlotHandle(vm, 2);
db_queue_push(&dbManager->requestQueue, context);
TRACE();
}
void dbExec(WrenVM* vm) {
ensureDbManager(vm);
TRACE();
create_db_context(vm, DB_OP_EXEC, 1, 2);
TRACE();
}
void dbQuery(WrenVM* vm) {
ensureDbManager(vm);
TRACE();
TRACE();
create_db_context(vm, DB_OP_QUERY, 1, 2);
TRACE();
}
void dbClose(WrenVM* vm) {
ensureDbManager(vm);
TRACE();
TRACE();
create_db_context(vm, DB_OP_CLOSE, -1, 1);
TRACE();
}
WrenForeignMethodFn bindSqliteForeignMethod(WrenVM* vm, const char* module, const char* className, bool isStatic, const char* signature) {
TRACE();
if (strcmp(module, "sqlite") != 0) return NULL;
if (strcmp(className, "Database") == 0 && !isStatic) {
if (strcmp(signature, "open_(_,_)") == 0) return dbOpen;
if (strcmp(signature, "exec_(_,_)") == 0) return dbExec;
if (strcmp(signature, "query_(_,_)") == 0) return dbQuery;
if (strcmp(signature, "close_(_)") == 0) return dbClose;
}
return NULL;
}
WrenForeignClassMethods bindSqliteForeignClass(WrenVM* vm, const char* module, const char* className) {
TRACE();
if (strcmp(module, "sqlite") == 0 && strcmp(className, "Database") == 0) {
WrenForeignClassMethods methods = {dbAllocate, dbFinalize};
return methods;
}
WrenForeignClassMethods methods = {0, 0};
return methods;
}

496
sqlite3_backend.c Normal file
View File

@ -0,0 +1,496 @@
#include "wren.h"
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#ifdef _WIN32
#include <windows.h>
typedef HANDLE thread_t;
typedef CRITICAL_SECTION mutex_t;
typedef CONDITION_VARIABLE cond_t;
#else
#include <pthread.h>
typedef pthread_t thread_t;
typedef pthread_mutex_t mutex_t;
typedef pthread_cond_t cond_t;
#endif
// --- Data Structures ---
typedef enum {
DB_OP_OPEN,
DB_OP_EXEC,
DB_OP_QUERY,
DB_OP_CLOSE
} DBOp;
typedef struct {
sqlite3* db;
} DatabaseData;
// C-side representation of query results to pass from worker to main thread.
typedef struct DbValue {
int type;
union {
double num;
struct {
char* text;
int length;
} str;
} as;
} DbValue;
typedef struct DbRow {
char** columns;
DbValue* values;
int count;
struct DbRow* next;
} DbRow;
typedef struct DbContext {
WrenVM* vm;
DBOp operation;
WrenHandle* callback;
WrenHandle* dbHandle;
char* path;
sqlite3* newDb;
char* sql;
sqlite3* db;
bool success;
char* errorMessage;
DbRow* resultRows;
struct DbContext* next;
} DbContext;
// --- Thread-Safe Queue ---
typedef struct {
DbContext *head, *tail;
mutex_t mutex;
cond_t cond;
} DbThreadSafeQueue;
void db_queue_init(DbThreadSafeQueue* q) {
q->head = q->tail = NULL;
#ifdef _WIN32
InitializeCriticalSection(&q->mutex);
InitializeConditionVariable(&q->cond);
#else
pthread_mutex_init(&q->mutex, NULL);
pthread_cond_init(&q->cond, NULL);
#endif
}
void db_queue_destroy(DbThreadSafeQueue* q) {
#ifdef _WIN32
DeleteCriticalSection(&q->mutex);
#else
pthread_mutex_destroy(&q->mutex);
pthread_cond_destroy(&q->cond);
#endif
}
void db_queue_push(DbThreadSafeQueue* q, DbContext* context) {
#ifdef _WIN32
EnterCriticalSection(&q->mutex);
#else
pthread_mutex_lock(&q->mutex);
#endif
if(context) context->next = NULL;
if (q->tail) q->tail->next = context;
else q->head = context;
q->tail = context;
#ifdef _WIN32
WakeConditionVariable(&q->cond);
LeaveCriticalSection(&q->mutex);
#else
pthread_cond_signal(&q->cond);
pthread_mutex_unlock(&q->mutex);
#endif
}
DbContext* db_queue_pop(DbThreadSafeQueue* q) {
#ifdef _WIN32
EnterCriticalSection(&q->mutex);
while (q->head == NULL) {
SleepConditionVariableCS(&q->cond, &q->mutex, INFINITE);
}
#else
pthread_mutex_lock(&q->mutex);
while (q->head == NULL) {
pthread_cond_wait(&q->cond, &q->mutex);
}
#endif
DbContext* context = q->head;
q->head = q->head->next;
if (q->head == NULL) q->tail = NULL;
#ifdef _WIN32
LeaveCriticalSection(&q->mutex);
#else
pthread_mutex_unlock(&q->mutex);
#endif
return context;
}
bool db_queue_empty(DbThreadSafeQueue* q) {
bool empty;
#ifdef _WIN32
EnterCriticalSection(&q->mutex);
empty = (q->head == NULL);
LeaveCriticalSection(&q->mutex);
#else
pthread_mutex_lock(&q->mutex);
empty = (q->head == NULL);
pthread_mutex_unlock(&q->mutex);
#endif
return empty;
}
// --- Async DB Manager ---
typedef struct {
WrenVM* vm;
volatile bool running;
thread_t threads[2];
DbThreadSafeQueue requestQueue;
DbThreadSafeQueue completionQueue;
} AsyncDbManager;
static AsyncDbManager* dbManager = NULL;
void free_db_result_rows(DbRow* rows) {
while (rows) {
DbRow* next = rows->next;
if (rows->columns) {
for (int i = 0; i < rows->count; i++) {
free(rows->columns[i]);
}
free(rows->columns);
}
if (rows->values) {
for (int i = 0; i < rows->count; i++) {
if (rows->values[i].type == SQLITE_TEXT || rows->values[i].type == SQLITE_BLOB) {
free(rows->values[i].as.str.text);
}
}
free(rows->values);
}
free(rows);
rows = next;
}
}
void free_db_context(DbContext* context) {
if (context == NULL) return;
free(context->path);
free(context->sql);
free(context->errorMessage);
if (context->dbHandle) wrenReleaseHandle(context->vm, context->dbHandle);
if (context->callback) wrenReleaseHandle(context->vm, context->callback);
if (context->resultRows) free_db_result_rows(context->resultRows);
free(context);
}
static void set_context_error(DbContext* context, const char* message) {
if (context == NULL) return;
context->success = false;
if (context->errorMessage) free(context->errorMessage);
context->errorMessage = message ? strdup(message) : strdup("An unknown database error occurred.");
}
#ifdef _WIN32
DWORD WINAPI dbWorkerThread(LPVOID arg);
#else
void* dbWorkerThread(void* arg);
#endif
void dbManager_create(WrenVM* vm) {
if (dbManager != NULL) return;
dbManager = (AsyncDbManager*)malloc(sizeof(AsyncDbManager));
if (dbManager == NULL) return;
dbManager->vm = vm;
dbManager->running = true;
db_queue_init(&dbManager->requestQueue);
db_queue_init(&dbManager->completionQueue);
for (int i = 0; i < 2; ++i) {
#ifdef _WIN32
dbManager->threads[i] = CreateThread(NULL, 0, dbWorkerThread, dbManager, 0, NULL);
#else
pthread_create(&dbManager->threads[i], NULL, dbWorkerThread, dbManager);
#endif
}
}
void dbManager_destroy() {
if (!dbManager) return;
dbManager->running = false;
for (int i = 0; i < 2; ++i) db_queue_push(&dbManager->requestQueue, NULL);
for (int i = 0; i < 2; ++i) {
#ifdef _WIN32
WaitForSingleObject(dbManager->threads[i], INFINITE);
CloseHandle(dbManager->threads[i]);
#else
pthread_join(dbManager->threads[i], NULL);
#endif
}
while(!db_queue_empty(&dbManager->requestQueue)) free_db_context(db_queue_pop(&dbManager->requestQueue));
while(!db_queue_empty(&dbManager->completionQueue)) free_db_context(db_queue_pop(&dbManager->completionQueue));
db_queue_destroy(&dbManager->requestQueue);
db_queue_destroy(&dbManager->completionQueue);
free(dbManager);
dbManager = NULL;
}
void dbManager_processCompletions() {
if (!dbManager || !dbManager->vm || db_queue_empty(&dbManager->completionQueue)) return;
while (!db_queue_empty(&dbManager->completionQueue)) {
DbContext* context = db_queue_pop(&dbManager->completionQueue);
if (context == NULL) continue;
if (context->success && context->dbHandle) {
wrenEnsureSlots(dbManager->vm, 1);
wrenSetSlotHandle(dbManager->vm, 0, context->dbHandle);
DatabaseData* dbData = (DatabaseData*)wrenGetSlotForeign(dbManager->vm, 0);
if (dbData) {
if (context->operation == DB_OP_OPEN) dbData->db = context->newDb;
else if (context->operation == DB_OP_CLOSE) dbData->db = NULL;
}
}
if (context->callback == NULL) {
free_db_context(context);
continue;
}
WrenHandle* callHandle = NULL;
int numArgs = 0;
if (context->operation == DB_OP_QUERY) {
callHandle = wrenMakeCallHandle(dbManager->vm, "call(_,_)");
numArgs = 2;
} else {
callHandle = wrenMakeCallHandle(dbManager->vm, "call(_)");
numArgs = 1;
}
if (callHandle == NULL) {
free_db_context(context);
continue;
}
// Ensure enough slots for callback, args, and temp work.
// Slots 0, 1, 2 are for the callback and its arguments.
// Slots 3, 4, 5 are for temporary work building maps.
wrenEnsureSlots(dbManager->vm, 6);
wrenSetSlotHandle(dbManager->vm, 0, context->callback);
if (context->success) {
wrenSetSlotNull(dbManager->vm, 1); // error is null
if (numArgs == 2) { // Query case
if (context->resultRows) {
wrenSetSlotNewList(dbManager->vm, 2); // Result list in slot 2
DbRow* row = context->resultRows;
while(row) {
wrenSetSlotNewMap(dbManager->vm, 3); // Temp map for row in slot 3
for (int i = 0; i < row->count; i++) {
// Use slots 4 and 5 for key/value to avoid conflicts
wrenSetSlotString(dbManager->vm, 4, row->columns[i]);
DbValue* val = &row->values[i];
switch (val->type) {
case SQLITE_INTEGER: case SQLITE_FLOAT:
wrenSetSlotDouble(dbManager->vm, 5, val->as.num); break;
case SQLITE_TEXT: case SQLITE_BLOB:
wrenSetSlotBytes(dbManager->vm, 5, val->as.str.text, val->as.str.length); break;
case SQLITE_NULL:
wrenSetSlotNull(dbManager->vm, 5); break;
}
wrenSetMapValue(dbManager->vm, 3, 4, 5); // map=3, key=4, val=5
}
wrenInsertInList(dbManager->vm, 2, -1, 3); // list=2, element=3
row = row->next;
}
} else {
wrenSetSlotNewList(dbManager->vm, 2); // Return empty list for success with no rows
}
}
} else {
wrenSetSlotString(dbManager->vm, 1, context->errorMessage ? context->errorMessage : "Unknown error.");
if (numArgs == 2) wrenSetSlotNull(dbManager->vm, 2);
}
wrenCall(dbManager->vm, callHandle);
wrenReleaseHandle(dbManager->vm, callHandle);
free_db_context(context);
}
}
// --- Worker Thread ---
#ifdef _WIN32
DWORD WINAPI dbWorkerThread(LPVOID arg) {
#else
void* dbWorkerThread(void* arg) {
#endif
AsyncDbManager* manager = (AsyncDbManager*)arg;
while (manager->running) {
DbContext* context = db_queue_pop(&manager->requestQueue);
if (!context || !manager->running) {
if (context) free_db_context(context);
break;
}
switch (context->operation) {
case DB_OP_OPEN: {
int rc = sqlite3_open_v2(context->path, &context->newDb,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
NULL);
if (rc != SQLITE_OK) {
set_context_error(context, sqlite3_errmsg(context->newDb));
sqlite3_close(context->newDb);
context->newDb = NULL;
} else {
context->success = true;
}
break;
}
case DB_OP_EXEC: {
if (!context->db) { set_context_error(context, "Database is not open."); break; }
char* err = NULL;
int rc = sqlite3_exec(context->db, context->sql, 0, 0, &err);
if (rc != SQLITE_OK) {
set_context_error(context, err);
sqlite3_free(err);
} else {
context->success = true;
}
break;
}
case DB_OP_QUERY: {
if (!context->db) { set_context_error(context, "Database is not open."); break; }
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(context->db, context->sql, -1, &stmt, 0);
if (rc != SQLITE_OK) { set_context_error(context, sqlite3_errmsg(context->db)); break; }
int colCount = sqlite3_column_count(stmt);
DbRow* head = NULL, *tail = NULL;
bool oom = false;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
DbRow* row = (DbRow*)calloc(1, sizeof(DbRow));
if (!row) { oom = true; break; }
row->count = colCount;
row->columns = (char**)malloc(sizeof(char*) * colCount);
row->values = (DbValue*)malloc(sizeof(DbValue) * colCount);
if (!row->columns || !row->values) { free(row->columns); free(row->values); free(row); oom = true; break; }
for (int i = 0; i < colCount; i++) {
const char* colName = sqlite3_column_name(stmt, i);
row->columns[i] = colName ? strdup(colName) : strdup("");
if (!row->columns[i]) { for (int j=0; j<i; j++) free(row->columns[j]); free(row->columns); free(row->values); free(row); oom = true; goto query_loop_end; }
DbValue* val = &row->values[i];
val->type = sqlite3_column_type(stmt, i);
switch (val->type) {
case SQLITE_INTEGER: case SQLITE_FLOAT: val->as.num = sqlite3_column_double(stmt, i); break;
case SQLITE_TEXT: case SQLITE_BLOB: {
const void* blob = sqlite3_column_blob(stmt, i);
int len = sqlite3_column_bytes(stmt, i);
val->as.str.text = (char*)malloc(len);
if (!val->as.str.text) { for (int j=0; j<=i; j++) free(row->columns[j]); free(row->columns); free(row->values); free(row); oom = true; goto query_loop_end; }
memcpy(val->as.str.text, blob, len);
val->as.str.length = len;
break;
}
case SQLITE_NULL: break;
}
}
if (!head) head = tail = row; else { tail->next = row; tail = row; }
}
query_loop_end:;
if (oom) { set_context_error(context, "Out of memory during query."); free_db_result_rows(head); }
else if (rc != SQLITE_DONE) { set_context_error(context, sqlite3_errmsg(context->db)); free_db_result_rows(head); }
else { context->success = true; context->resultRows = head; }
sqlite3_finalize(stmt);
break;
}
case DB_OP_CLOSE: {
if (context->db) sqlite3_close(context->db);
context->success = true;
break;
}
}
db_queue_push(&manager->completionQueue, context);
}
return 0;
}
// --- Wren FFI ---
void dbAllocate(WrenVM* vm) {
DatabaseData* data = (DatabaseData*)wrenSetSlotNewForeign(vm, 0, 0, sizeof(DatabaseData));
if (data) data->db = NULL;
}
void dbFinalize(void* data) {
DatabaseData* dbData = (DatabaseData*)data;
if (dbData && dbData->db) sqlite3_close(dbData->db);
}
static void create_db_context(WrenVM* vm, DBOp op, int sqlSlot, int cbSlot) {
DbContext* context = (DbContext*)calloc(1, sizeof(DbContext));
if (!context) { wrenSetSlotString(vm, 0, "Out of memory."); wrenAbortFiber(vm, 0); return; }
context->vm = vm;
context->operation = op;
context->dbHandle = wrenGetSlotHandle(vm, 0);
context->callback = wrenGetSlotHandle(vm, cbSlot);
if (sqlSlot != -1) {
if (wrenGetSlotType(vm, sqlSlot) != WREN_TYPE_STRING) {
wrenSetSlotString(vm, 0, "SQL argument must be a string.");
wrenAbortFiber(vm, 0);
free_db_context(context);
return;
}
const char* sql_str = wrenGetSlotString(vm, sqlSlot);
if (sql_str) context->sql = strdup(sql_str);
if (!context->sql) { set_context_error(context, "Out of memory."); db_queue_push(&dbManager->requestQueue, context); return; }
}
DatabaseData* dbData = (DatabaseData*)wrenGetSlotForeign(vm, 0);
if (!dbData) { set_context_error(context, "Invalid database object."); db_queue_push(&dbManager->requestQueue, context); return; }
context->db = dbData->db;
db_queue_push(&dbManager->requestQueue, context);
}
void dbOpen(WrenVM* vm) {
DbContext* context = (DbContext*)calloc(1, sizeof(DbContext));
if (!context) { wrenSetSlotString(vm, 0, "Out of memory."); wrenAbortFiber(vm, 0); return; }
context->vm = vm;
context->operation = DB_OP_OPEN;
const char* path_str = wrenGetSlotString(vm, 1);
if (path_str) context->path = strdup(path_str);
if (!context->path) { free(context); wrenSetSlotString(vm, 0, "Out of memory."); wrenAbortFiber(vm, 0); return; }
context->dbHandle = wrenGetSlotHandle(vm, 0);
context->callback = wrenGetSlotHandle(vm, 2);
db_queue_push(&dbManager->requestQueue, context);
}
void dbExec(WrenVM* vm) { create_db_context(vm, DB_OP_EXEC, 1, 2); }
void dbQuery(WrenVM* vm) { create_db_context(vm, DB_OP_QUERY, 1, 2); }
void dbClose(WrenVM* vm) { create_db_context(vm, DB_OP_CLOSE, -1, 1); }
WrenForeignMethodFn bindSqliteForeignMethod(WrenVM* vm, const char* module, const char* className, bool isStatic, const char* signature) {
if (strcmp(module, "sqlite") != 0) return NULL;
if (strcmp(className, "Database") == 0 && !isStatic) {
if (strcmp(signature, "open_(_,_)") == 0) return dbOpen;
if (strcmp(signature, "exec_(_,_)") == 0) return dbExec;
if (strcmp(signature, "query_(_,_)") == 0) return dbQuery;
if (strcmp(signature, "close_(_)") == 0) return dbClose;
}
return NULL;
}
WrenForeignClassMethods bindSqliteForeignClass(WrenVM* vm, const char* module, const char* className) {
if (strcmp(module, "sqlite") == 0 && strcmp(className, "Database") == 0) {
WrenForeignClassMethods methods = {dbAllocate, dbFinalize};
return methods;
}
WrenForeignClassMethods methods = {0, 0};
return methods;
}

139
sqlite_example.wren Normal file
View File

@ -0,0 +1,139 @@
import "sqlite" for Database
import "io" for File
// This class provides a hook back into the C host to terminate the application.
foreign class Host {
foreign static signalDone()
}
// All database operations are asynchronous. We chain them together using
// callbacks to ensure they execute in the correct order.
var mainFiber = Fiber.new {
var db = Database.new()
var dbPath = "test.db"
var isDone = false // Flag to keep the fiber alive.
// Clean up database file from previous runs, if it exists.
if (File.exists(dbPath)) {
File.delete(dbPath)
}
System.print("--- Starting SQLite CRUD Example ---")
// 1. Open the database connection.
db.open(dbPath) { |err|
if (err) {
System.print("Error opening database: %(err)")
isDone = true // Signal completion on error.
return
}
System.print("Database opened successfully at '%(dbPath)'.")
// 2. CREATE: Create a new table.
var createSql = "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT);"
db.exec(createSql) { |err|
if (err) {
System.print("Error creating table: %(err)")
isDone = true
return
}
System.print("Table 'users' created.")
// 3. CREATE: Insert a new user.
var insertSql = "INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');"
db.exec(insertSql) { |err|
if (err) {
System.print("Error inserting user: %(err)")
isDone = true
return
}
System.print("Inserted user 'Alice'.")
// 4. READ: Query all users.
db.query("SELECT * FROM users;") { |err, rows|
if (err) {
System.print("Error querying users: %(err)")
isDone = true
return
}
System.print("\n--- Current Users (after insert): ---")
for (row in rows) {
System.print(" ID: %(row["id"]), Name: %(row["name"]), Email: %(row["email"])")
}
// 5. UPDATE: Change Alice's email.
var updateSql = "UPDATE users SET email = 'alice.smith@example.com' WHERE name = 'Alice';"
db.exec(updateSql) { |err|
if (err) {
System.print("Error updating user: %(err)")
isDone = true
return
}
System.print("\nUpdated Alice's email.")
// 6. READ: Query again to see the update.
db.query("SELECT * FROM users;") { |err, rows|
if (err) {
System.print("Error querying users: %(err)")
isDone = true
return
}
System.print("\n--- Current Users (after update): ---")
for (row in rows) {
System.print(" ID: %(row["id"]), Name: %(row["name"]), Email: %(row["email"])")
}
// 7. DELETE: Remove Alice from the database.
var deleteSql = "DELETE FROM users WHERE name = 'Alice';"
db.exec(deleteSql) { |err|
if (err) {
System.print("Error deleting user: %(err)")
isDone = true
return
}
System.print("\nDeleted Alice.")
// 8. READ: Query one last time to confirm deletion.
db.query("SELECT * FROM users;") { |err, rows|
if (err) {
System.print("Error querying users: %(err)")
isDone = true
return
}
System.print("\n--- Current Users (after delete): ---")
if (rows.isEmpty) {
System.print(" (No users found)")
} else {
for (row in rows) {
System.print(" ID: %(row["id"]), Name: %(row["name"]), Email: %(row["email"])")
}
}
// 9. Close the database connection.
db.close() { |err|
if (err) {
System.print("Error closing database: %(err)")
} else {
System.print("\nDatabase closed successfully.")
}
System.print("\n--- SQLite CRUD Example Finished ---")
isDone = true // This is the final step, signal completion.
}
}
}
}
}
}
}
}
}
// Keep the fiber alive by yielding until the 'isDone' flag is set.
while (!isDone) {
Fiber.yield()
}
// All asynchronous operations are complete, now we can safely exit.
Host.signalDone()
}

80
sqlite_performance.wren Normal file
View File

@ -0,0 +1,80 @@
import "sqlite" for Database
import "io" for File
foreign class Host {
foreign static signalDone()
}
var mainFiber = Fiber.new {
var db = Database.new()
var dbPath = "test.db"
var isDone = false
if (File.exists(dbPath)) File.delete(dbPath)
System.print("--- Starting SQLite CRUD Example ---")
db.open(dbPath) { |err|
if (err) {
System.print("Error opening database: %(err)")
isDone = true
return
}
System.print("Database opened successfully at '%(dbPath)'.")
var createSql = "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT);"
db.exec(createSql) { |err|
if (err) {
System.print("Error creating table: %(err)")
isDone = true
return
}
System.print("Table 'users' created.")
// Insert and read 1000 times
var insertCount = 0
var doInsertAndRead
doInsertAndRead = Fn.new {
if (insertCount >= 1000) {
// Finished, close db
db.close() { |err|
if (err) {
System.print("Error closing database: %(err)")
} else {
System.print("\nDatabase closed successfully.")
}
System.print("\n--- SQLite CRUD Example Finished ---")
isDone = true
}
return
}
var name = "User_%(insertCount)"
var email = "user%(insertCount)@example.com"
var insertSql = "INSERT INTO users (name, email) VALUES ('%(name)', '%(email)');"
db.exec(insertSql) { |err|
if (err) {
System.print("Error inserting user %(name): %(err)")
isDone = true
return
}
db.query("SELECT * FROM users WHERE name = '%(name)';") { |err, rows|
if (err) {
System.print("Error querying user %(name): %(err)")
isDone = true
return
}
for (row in rows) {
System.print("Inserted and read: ID: %(row["id"]), Name: %(row["name"]), Email: %(row["email"])")
}
insertCount = insertCount + 1
doInsertAndRead.call()
}
}
}
doInsertAndRead.call()
}
}
while (!isDone) Fiber.yield()
Host.signalDone()
}

BIN
wren

Binary file not shown.