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