feat: add wasm build targets, profile hooks, os_demo example, and restructure manual source
- Add `wasm`, `wasm-clean`, and `install-emscripten` phony targets to Makefile for WebAssembly cross-compilation - Insert `WREN_PROFILE_ENTER`/`WREN_PROFILE_EXIT` macros in `wren_vm.h` and `wren_vm.c` to support optional runtime profiling via `WREN_PROFILE_ENABLED` - Create `example/os_demo.wren` demonstrating Platform, Process, and conditional exit usage - Update `example/regex_demo.wren` to import `Match` and exercise Match object properties, groups, and `matchAll` - Remove static HTML manual pages (`base64.html`, `dns.html`, `json.html`) and replace with structured `manual_src/` directory containing Jinja2 templates, YAML metadata, and content pages - Expand `README.md` with build targets table, manual building instructions, source layout, and guide for adding new module documentation
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
{# retoor <retoor@molodetz.nl> #}
|
||||
{% extends 'page.html' %}
|
||||
|
||||
{% set page_title = "Foreign Classes" %}
|
||||
{% set breadcrumb = [{"url": "contributing/index.html", "title": "Contributing"}, {"title": "Foreign Classes"}] %}
|
||||
{% set prev_page = {"url": "contributing/c-backed-module.html", "title": "C-Backed Modules"} %}
|
||||
{% set next_page = {"url": "contributing/async-patterns.html", "title": "Async Patterns"} %}
|
||||
|
||||
{% block article %}
|
||||
<h1>Foreign Classes</h1>
|
||||
|
||||
<p>Foreign classes allow Wren objects to hold native C data. This is used when a Wren object needs to manage resources like file handles, network sockets, database connections, or any native data structure.</p>
|
||||
|
||||
<h2>When to Use Foreign Classes</h2>
|
||||
|
||||
<ul>
|
||||
<li>Wrapping system resources (files, sockets, processes)</li>
|
||||
<li>Managing native library objects</li>
|
||||
<li>Storing data structures more complex than Wren's built-in types</li>
|
||||
<li>Resources requiring explicit cleanup</li>
|
||||
</ul>
|
||||
|
||||
<h2>Architecture</h2>
|
||||
|
||||
<p>A foreign class has three components:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Allocate function</strong>: Called when an instance is created via <code>construct new()</code></li>
|
||||
<li><strong>Finalize function</strong>: Called when the garbage collector frees the instance</li>
|
||||
<li><strong>Instance methods</strong>: Operate on the foreign data</li>
|
||||
</ol>
|
||||
|
||||
<h2>Basic Pattern</h2>
|
||||
|
||||
<h3>Wren Interface</h3>
|
||||
|
||||
<pre><code>// retoor <retoor@molodetz.nl>
|
||||
|
||||
class Buffer {
|
||||
foreign construct new(size)
|
||||
|
||||
foreign write(data)
|
||||
foreign read()
|
||||
foreign size
|
||||
foreign clear()
|
||||
}</code></pre>
|
||||
|
||||
<p>The constructor uses <code>foreign construct</code> to trigger allocation.</p>
|
||||
|
||||
<h3>C Implementation</h3>
|
||||
|
||||
<pre><code>// retoor <retoor@molodetz.nl>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "buffer.h"
|
||||
#include "wren.h"
|
||||
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t size;
|
||||
size_t capacity;
|
||||
} Buffer;
|
||||
|
||||
void bufferAllocate(WrenVM* vm) {
|
||||
Buffer* buffer = (Buffer*)wrenSetSlotNewForeign(vm, 0, 0, sizeof(Buffer));
|
||||
|
||||
double capacity = wrenGetSlotDouble(vm, 1);
|
||||
buffer->capacity = (size_t)capacity;
|
||||
buffer->size = 0;
|
||||
buffer->data = (char*)malloc(buffer->capacity);
|
||||
|
||||
if (!buffer->data) {
|
||||
buffer->capacity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void bufferFinalize(void* data) {
|
||||
Buffer* buffer = (Buffer*)data;
|
||||
if (buffer->data) {
|
||||
free(buffer->data);
|
||||
buffer->data = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void bufferWrite(WrenVM* vm) {
|
||||
Buffer* buffer = (Buffer*)wrenGetSlotForeign(vm, 0);
|
||||
const char* str = wrenGetSlotString(vm, 1);
|
||||
size_t len = strlen(str);
|
||||
|
||||
if (buffer->size + len > buffer->capacity) {
|
||||
wrenSetSlotString(vm, 0, "Buffer overflow.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(buffer->data + buffer->size, str, len);
|
||||
buffer->size += len;
|
||||
}
|
||||
|
||||
void bufferRead(WrenVM* vm) {
|
||||
Buffer* buffer = (Buffer*)wrenGetSlotForeign(vm, 0);
|
||||
|
||||
char* copy = (char*)malloc(buffer->size + 1);
|
||||
if (!copy) {
|
||||
wrenSetSlotNull(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(copy, buffer->data, buffer->size);
|
||||
copy[buffer->size] = '\0';
|
||||
|
||||
wrenSetSlotString(vm, 0, copy);
|
||||
free(copy);
|
||||
}
|
||||
|
||||
void bufferSize(WrenVM* vm) {
|
||||
Buffer* buffer = (Buffer*)wrenGetSlotForeign(vm, 0);
|
||||
wrenSetSlotDouble(vm, 0, (double)buffer->size);
|
||||
}
|
||||
|
||||
void bufferClear(WrenVM* vm) {
|
||||
Buffer* buffer = (Buffer*)wrenGetSlotForeign(vm, 0);
|
||||
buffer->size = 0;
|
||||
}</code></pre>
|
||||
|
||||
<h3>Registration</h3>
|
||||
|
||||
<pre><code>MODULE(buffer)
|
||||
CLASS(Buffer)
|
||||
ALLOCATE(bufferAllocate)
|
||||
FINALIZE(bufferFinalize)
|
||||
METHOD("write(_)", bufferWrite)
|
||||
METHOD("read()", bufferRead)
|
||||
METHOD("size", bufferSize)
|
||||
METHOD("clear()", bufferClear)
|
||||
END_CLASS
|
||||
END_MODULE</code></pre>
|
||||
|
||||
<h2>Memory Management</h2>
|
||||
|
||||
<h3>wrenSetSlotNewForeign</h3>
|
||||
|
||||
<pre><code>void* wrenSetSlotNewForeign(WrenVM* vm, int slot, int classSlot, size_t size);</code></pre>
|
||||
|
||||
<ul>
|
||||
<li><code>slot</code>: Where to place the new instance (usually 0)</li>
|
||||
<li><code>classSlot</code>: Slot containing the class (usually 0 for the current class)</li>
|
||||
<li><code>size</code>: Size of the native data structure</li>
|
||||
</ul>
|
||||
|
||||
<p>Returns a pointer to the allocated memory. This memory is managed by Wren's garbage collector.</p>
|
||||
|
||||
<h3>Finalize Function Signature</h3>
|
||||
|
||||
<pre><code>void myFinalize(void* data);</code></pre>
|
||||
|
||||
<p>The finalize function receives only a pointer to the foreign data, not the VM. This means:</p>
|
||||
|
||||
<ul>
|
||||
<li>No Wren API calls in finalize</li>
|
||||
<li>Cannot throw errors</li>
|
||||
<li>Must be fast (GC is running)</li>
|
||||
<li>Free any resources allocated in allocate or methods</li>
|
||||
</ul>
|
||||
|
||||
<h3>Accessing Foreign Data</h3>
|
||||
|
||||
<p>In instance methods, use <code>wrenGetSlotForeign</code> on slot 0:</p>
|
||||
|
||||
<pre><code>void bufferMethod(WrenVM* vm) {
|
||||
Buffer* buffer = (Buffer*)wrenGetSlotForeign(vm, 0);
|
||||
// buffer points to the struct created in bufferAllocate
|
||||
}</code></pre>
|
||||
|
||||
<h2>File Handle Example</h2>
|
||||
|
||||
<p>A practical example wrapping a file handle:</p>
|
||||
|
||||
<h3>filehandle.wren</h3>
|
||||
|
||||
<pre><code>// retoor <retoor@molodetz.nl>
|
||||
|
||||
class FileHandle {
|
||||
foreign construct open(path, mode)
|
||||
|
||||
foreign read()
|
||||
foreign write(data)
|
||||
foreign close()
|
||||
foreign isOpen
|
||||
}</code></pre>
|
||||
|
||||
<h3>filehandle.c</h3>
|
||||
|
||||
<pre><code>// retoor <retoor@molodetz.nl>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "filehandle.h"
|
||||
#include "wren.h"
|
||||
|
||||
typedef struct {
|
||||
FILE* file;
|
||||
char* path;
|
||||
} FileHandle;
|
||||
|
||||
void fileHandleAllocate(WrenVM* vm) {
|
||||
FileHandle* handle = (FileHandle*)wrenSetSlotNewForeign(vm, 0, 0, sizeof(FileHandle));
|
||||
|
||||
const char* path = wrenGetSlotString(vm, 1);
|
||||
const char* mode = wrenGetSlotString(vm, 2);
|
||||
|
||||
handle->path = strdup(path);
|
||||
handle->file = fopen(path, mode);
|
||||
|
||||
if (!handle->file) {
|
||||
wrenSetSlotString(vm, 0, "Failed to open file.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void fileHandleFinalize(void* data) {
|
||||
FileHandle* handle = (FileHandle*)data;
|
||||
|
||||
if (handle->file) {
|
||||
fclose(handle->file);
|
||||
handle->file = NULL;
|
||||
}
|
||||
|
||||
if (handle->path) {
|
||||
free(handle->path);
|
||||
handle->path = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void fileHandleRead(WrenVM* vm) {
|
||||
FileHandle* handle = (FileHandle*)wrenGetSlotForeign(vm, 0);
|
||||
|
||||
if (!handle->file) {
|
||||
wrenSetSlotString(vm, 0, "File not open.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
fseek(handle->file, 0, SEEK_END);
|
||||
long size = ftell(handle->file);
|
||||
fseek(handle->file, 0, SEEK_SET);
|
||||
|
||||
char* content = (char*)malloc(size + 1);
|
||||
if (!content) {
|
||||
wrenSetSlotString(vm, 0, "Memory allocation failed.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
fread(content, 1, size, handle->file);
|
||||
content[size] = '\0';
|
||||
|
||||
wrenSetSlotString(vm, 0, content);
|
||||
free(content);
|
||||
}
|
||||
|
||||
void fileHandleWrite(WrenVM* vm) {
|
||||
FileHandle* handle = (FileHandle*)wrenGetSlotForeign(vm, 0);
|
||||
const char* data = wrenGetSlotString(vm, 1);
|
||||
|
||||
if (!handle->file) {
|
||||
wrenSetSlotString(vm, 0, "File not open.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t written = fwrite(data, 1, strlen(data), handle->file);
|
||||
wrenSetSlotDouble(vm, 0, (double)written);
|
||||
}
|
||||
|
||||
void fileHandleClose(WrenVM* vm) {
|
||||
FileHandle* handle = (FileHandle*)wrenGetSlotForeign(vm, 0);
|
||||
|
||||
if (handle->file) {
|
||||
fclose(handle->file);
|
||||
handle->file = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void fileHandleIsOpen(WrenVM* vm) {
|
||||
FileHandle* handle = (FileHandle*)wrenGetSlotForeign(vm, 0);
|
||||
wrenSetSlotBool(vm, 0, handle->file != NULL);
|
||||
}</code></pre>
|
||||
|
||||
<h3>Usage</h3>
|
||||
|
||||
<pre><code>import "filehandle" for FileHandle
|
||||
|
||||
var file = FileHandle.open("test.txt", "w")
|
||||
file.write("Hello, World!")
|
||||
file.close()
|
||||
|
||||
file = FileHandle.open("test.txt", "r")
|
||||
System.print(file.read())
|
||||
file.close()</code></pre>
|
||||
|
||||
<h2>Multiple Foreign Classes</h2>
|
||||
|
||||
<p>A module can have multiple foreign classes:</p>
|
||||
|
||||
<pre><code>MODULE(database)
|
||||
CLASS(Connection)
|
||||
ALLOCATE(connectionAllocate)
|
||||
FINALIZE(connectionFinalize)
|
||||
METHOD("query(_)", connectionQuery)
|
||||
METHOD("close()", connectionClose)
|
||||
END_CLASS
|
||||
CLASS(Statement)
|
||||
ALLOCATE(statementAllocate)
|
||||
FINALIZE(statementFinalize)
|
||||
METHOD("bind(_,_)", statementBind)
|
||||
METHOD("execute()", statementExecute)
|
||||
END_CLASS
|
||||
END_MODULE</code></pre>
|
||||
|
||||
<h2>Resource Safety Patterns</h2>
|
||||
|
||||
<h3>Early Close</h3>
|
||||
<p>Always check if resource is still valid:</p>
|
||||
<pre><code>void handleMethod(WrenVM* vm) {
|
||||
Handle* h = (Handle*)wrenGetSlotForeign(vm, 0);
|
||||
if (!h->resource) {
|
||||
wrenSetSlotString(vm, 0, "Handle already closed.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
// ...
|
||||
}</code></pre>
|
||||
|
||||
<h3>Double-Free Prevention</h3>
|
||||
<p>Set pointers to NULL after freeing:</p>
|
||||
<pre><code>void handleClose(WrenVM* vm) {
|
||||
Handle* h = (Handle*)wrenGetSlotForeign(vm, 0);
|
||||
if (h->resource) {
|
||||
resource_free(h->resource);
|
||||
h->resource = NULL;
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h3>Defensive Finalize</h3>
|
||||
<p>Always handle partially constructed objects:</p>
|
||||
<pre><code>void handleFinalize(void* data) {
|
||||
Handle* h = (Handle*)data;
|
||||
if (h->resource) {
|
||||
resource_free(h->resource);
|
||||
}
|
||||
if (h->name) {
|
||||
free(h->name);
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h2>Common Pitfalls</h2>
|
||||
|
||||
<ul>
|
||||
<li><strong>Forgetting FINALIZE</strong>: Memory leaks for any allocated resources</li>
|
||||
<li><strong>Using VM in finalize</strong>: Causes undefined behavior</li>
|
||||
<li><strong>Wrong slot for foreign data</strong>: Instance methods get <code>this</code> in slot 0</li>
|
||||
<li><strong>Static methods on foreign class</strong>: Use <code>STATIC_METHOD</code> macro, but note that <code>wrenGetSlotForeign</code> is not available (no instance)</li>
|
||||
</ul>
|
||||
|
||||
<h2>Checklist</h2>
|
||||
|
||||
<ul>
|
||||
<li><code>foreign construct</code> in Wren class</li>
|
||||
<li>Allocate function uses <code>wrenSetSlotNewForeign</code></li>
|
||||
<li>Finalize function frees all resources</li>
|
||||
<li><code>ALLOCATE</code> and <code>FINALIZE</code> in registration</li>
|
||||
<li>Instance methods use <code>METHOD</code> (not <code>STATIC_METHOD</code>)</li>
|
||||
<li>Methods access foreign data via slot 0</li>
|
||||
<li>All methods check resource validity</li>
|
||||
</ul>
|
||||
|
||||
<h2>Next Steps</h2>
|
||||
|
||||
<p>For I/O-bound foreign classes, see <a href="async-patterns.html">Async Patterns</a> to integrate with the libuv event loop.</p>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user