Everything working.
This commit is contained in:
parent
fd6b651af7
commit
b4884196e1
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
test.db
|
2
Makefile
2
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:
|
||||
|
135
backend.cpp
135
backend.cpp
@ -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
160
crawler_based.wren
Normal 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
18
io.wren
Normal 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
119
io_backend.c
Normal 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
46
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;
|
||||
}
|
||||
|
||||
|
@ -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
36
sqlite.wren
Normal 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
739
sqlite3_backend.bak
Normal 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'; /* 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;
|
||||
}
|
||||
|
496
sqlite3_backend.c
Normal file
496
sqlite3_backend.c
Normal 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
139
sqlite_example.wren
Normal 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
80
sqlite_performance.wren
Normal 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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user