diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c370cb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +test.db diff --git a/Makefile b/Makefile index 88a4b05..e3fadcf 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/backend.cpp b/backend.cpp deleted file mode 100644 index 5526e2b..0000000 --- a/backend.cpp +++ /dev/null @@ -1,135 +0,0 @@ -// backend.cpp (Corrected) -#include "httplib.h" -#include "wren.h" -#include -#include - -// 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; -} diff --git a/crawler_based.wren b/crawler_based.wren new file mode 100644 index 0000000..970ccfb --- /dev/null +++ b/crawler_based.wren @@ -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() +} + diff --git a/io.wren b/io.wren new file mode 100644 index 0000000..7bc9ebe --- /dev/null +++ b/io.wren @@ -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) +} + diff --git a/io_backend.c b/io_backend.c new file mode 100644 index 0000000..83040f2 --- /dev/null +++ b/io_backend.c @@ -0,0 +1,119 @@ +#include "wren.h" +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#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; +} + diff --git a/main.c b/main.c index 4e3ff3f..ca2ebe4 100644 --- a/main.c +++ b/main.c @@ -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; } + diff --git a/merged_source_files.txt b/merged_source_files.txt index 1526410..f208709 100644 --- a/merged_source_files.txt +++ b/merged_source_files.txt @@ -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 +#include +#include + +// 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" diff --git a/sqlite.wren b/sqlite.wren new file mode 100644 index 0000000..e152ddb --- /dev/null +++ b/sqlite.wren @@ -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) + } +} + diff --git a/sqlite3_backend.bak b/sqlite3_backend.bak new file mode 100644 index 0000000..9de211a --- /dev/null +++ b/sqlite3_backend.bak @@ -0,0 +1,739 @@ +#include "wren.h" +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include + typedef HANDLE thread_t; + typedef CRITICAL_SECTION mutex_t; + typedef CONDITION_VARIABLE cond_t; + #define GET_THREAD_ID() GetCurrentThreadId() +#else + #include + 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'; /* NUL‑terminate 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; +} + diff --git a/sqlite3_backend.c b/sqlite3_backend.c new file mode 100644 index 0000000..a1f1b58 --- /dev/null +++ b/sqlite3_backend.c @@ -0,0 +1,496 @@ +#include "wren.h" +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include + typedef HANDLE thread_t; + typedef CRITICAL_SECTION mutex_t; + typedef CONDITION_VARIABLE cond_t; +#else + #include + 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; jcolumns[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; +} + diff --git a/sqlite_example.wren b/sqlite_example.wren new file mode 100644 index 0000000..6295a58 --- /dev/null +++ b/sqlite_example.wren @@ -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() +} + diff --git a/sqlite_performance.wren b/sqlite_performance.wren new file mode 100644 index 0000000..5b1ba9e --- /dev/null +++ b/sqlite_performance.wren @@ -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() +} + diff --git a/wren b/wren index 52447b3..a9b8313 100755 Binary files a/wren and b/wren differ