feat: add networking module with TCP Socket/Server and example applications

Implement a full-featured `net` module supporting TCP `Socket` and `Server` classes via libuv, including example applications (chat server, echo server, HTTP client/server, file explorer, system dashboard) and benchmark scripts (fibonacci, n-body). Add GEMINI.md project documentation and Stderr class to io module.
This commit is contained in:
2026-01-24 08:57:59 +00:00
parent abc32676bf
commit 8f1d13a86b
25 changed files with 1371 additions and 1 deletions
+37 -1
View File
@@ -4,6 +4,7 @@
#include "modules.h"
#include "io.wren.inc"
#include "net.wren.inc"
#include "os.wren.inc"
#include "repl.wren.inc"
#include "scheduler.wren.inc"
@@ -51,8 +52,22 @@ extern void stdinIsTerminal(WrenVM* vm);
extern void stdinReadStart(WrenVM* vm);
extern void stdinReadStop(WrenVM* vm);
extern void stdoutFlush(WrenVM* vm);
extern void stderrWrite(WrenVM* vm);
extern void schedulerCaptureMethods(WrenVM* vm);
extern void timerStartTimer(WrenVM* vm);
extern void socketAllocate(WrenVM* vm);
extern void socketFinalize(void* data);
extern void socketConnect(WrenVM* vm);
extern void socketWrite(WrenVM* vm);
extern void socketRead(WrenVM* vm);
extern void socketClose(WrenVM* vm);
extern void netCaptureClass(WrenVM* vm);
extern void netShutdown();
extern void serverAllocate(WrenVM* vm);
extern void serverFinalize(void* data);
extern void serverBind(WrenVM* vm);
extern void serverAccept(WrenVM* vm);
extern void serverClose(WrenVM* vm);
// The maximum number of foreign methods a single class defines. Ideally, we
// would use variable-length arrays for each class in the table below, but
@@ -69,7 +84,7 @@ extern void timerStartTimer(WrenVM* vm);
// If you add a new class to the largest module below, make sure to bump this.
// Note that it also includes an extra slot for the sentinel value indicating
// the end of the list.
#define MAX_CLASSES_PER_MODULE 6
#define MAX_CLASSES_PER_MODULE 8
// Describes one foreign method in a class.
typedef struct
@@ -167,6 +182,27 @@ static ModuleRegistry modules[] =
CLASS(Stdout)
STATIC_METHOD("flush()", stdoutFlush)
END_CLASS
CLASS(Stderr)
STATIC_METHOD("write(_)", stderrWrite)
END_CLASS
END_MODULE
MODULE(net)
CLASS(Socket)
ALLOCATE(socketAllocate)
FINALIZE(socketFinalize)
STATIC_METHOD("captureClass_()", netCaptureClass)
METHOD("connect_(_,_,_)", socketConnect)
METHOD("write_(_,_)", socketWrite)
METHOD("read_(_)", socketRead)
METHOD("close_()", socketClose)
END_CLASS
CLASS(Server)
ALLOCATE(serverAllocate)
FINALIZE(serverFinalize)
METHOD("bind_(_,_)", serverBind)
METHOD("accept_(_)", serverAccept)
METHOD("close_()", serverClose)
END_CLASS
END_MODULE
MODULE(os)
CLASS(Platform)
+5
View File
@@ -286,9 +286,14 @@ static void initVM()
uv_loop_init(loop);
}
extern void ioShutdown();
extern void netShutdown();
extern void schedulerShutdown();
static void freeVM()
{
ioShutdown();
netShutdown();
schedulerShutdown();
uv_loop_close(loop);
+7
View File
@@ -558,6 +558,13 @@ void stdoutFlush(WrenVM* vm)
wrenSetSlotNull(vm, 0);
}
void stderrWrite(WrenVM* vm)
{
const char* text = wrenGetSlotString(vm, 1);
fprintf(stderr, "%s", text);
wrenSetSlotNull(vm, 0);
}
static void allocCallback(uv_handle_t* handle, size_t suggestedSize,
uv_buf_t* buf)
{
+4
View File
@@ -303,3 +303,7 @@ class Stdin {
class Stdout {
foreign static flush()
}
class Stderr {
foreign static write(string)
}
+4
View File
@@ -306,4 +306,8 @@ static const char* ioModuleSource =
"\n"
"class Stdout {\n"
" foreign static flush()\n"
"}\n"
"\n"
"class Stderr {\n"
" foreign static write(string)\n"
"}\n";
+411
View File
@@ -0,0 +1,411 @@
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <stdio.h>
#include "uv.h"
#include "vm.h"
#include "scheduler.h"
#include "net.h"
// -----------------------------------------------------------------------------
// Helper Structures
// -----------------------------------------------------------------------------
typedef struct {
uv_tcp_t* handle;
WrenHandle* fiber; // Fiber waiting for an operation (connect, read, write)
uv_connect_t* connectReq;
uv_write_t* writeReq;
} SocketData;
typedef struct SocketListNode {
uv_tcp_t* handle;
struct SocketListNode* next;
} SocketListNode;
typedef struct {
uv_tcp_t* handle;
WrenHandle* acceptFiber; // Fiber waiting for accept()
SocketListNode* pendingHead;
SocketListNode* pendingTail;
} ServerData;
static WrenHandle* socketClassHandle = NULL;
// -----------------------------------------------------------------------------
// Socket Implementation
// -----------------------------------------------------------------------------
void socketAllocate(WrenVM* vm)
{
wrenSetSlotNewForeign(vm, 0, 0, sizeof(SocketData));
SocketData* data = (SocketData*)wrenGetSlotForeign(vm, 0);
data->handle = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
uv_tcp_init(getLoop(), data->handle);
data->handle->data = data;
data->fiber = NULL;
data->connectReq = NULL;
data->writeReq = NULL;
}
static void closeCallback(uv_handle_t* handle)
{
free(handle);
}
void socketFinalize(void* data)
{
SocketData* socketData = (SocketData*)data;
if (socketData->handle) {
uv_close((uv_handle_t*)socketData->handle, closeCallback);
}
}
static void connectCallback(uv_connect_t* req, int status)
{
SocketData* data = (SocketData*)req->data;
WrenHandle* fiber = data->fiber;
free(req);
data->connectReq = NULL;
data->fiber = NULL;
if (status < 0) {
schedulerResumeError(fiber, uv_strerror(status));
} else {
schedulerResume(fiber, false);
}
}
void socketConnect(WrenVM* vm)
{
SocketData* data = (SocketData*)wrenGetSlotForeign(vm, 0);
const char* host = wrenGetSlotString(vm, 1);
int port = (int)wrenGetSlotDouble(vm, 2);
WrenHandle* fiber = wrenGetSlotHandle(vm, 3);
struct sockaddr_in addr;
int r = uv_ip4_addr(host, port, &addr);
if (r != 0) {
schedulerResumeError(fiber, "Invalid IP address.");
return;
}
data->fiber = fiber;
data->connectReq = (uv_connect_t*)malloc(sizeof(uv_connect_t));
data->connectReq->data = data;
r = uv_tcp_connect(data->connectReq, data->handle, (const struct sockaddr*)&addr, connectCallback);
if (r != 0) {
free(data->connectReq);
data->connectReq = NULL;
schedulerResumeError(fiber, uv_strerror(r));
}
}
// Redefine writeCallback to handle the buffer
static void writeCallback_safe(uv_write_t* req, int status)
{
typedef struct {
uv_write_t req;
uv_buf_t buf;
} WriteRequest;
WriteRequest* wr = (WriteRequest*)req;
SocketData* data = (SocketData*)req->data;
WrenHandle* fiber = data->fiber;
free(wr->buf.base);
free(wr);
data->writeReq = NULL;
data->fiber = NULL;
if (status < 0) {
schedulerResumeError(fiber, uv_strerror(status));
} else {
schedulerResume(fiber, false);
}
}
void socketWrite(WrenVM* vm)
{
SocketData* data = (SocketData*)wrenGetSlotForeign(vm, 0);
const char* text = wrenGetSlotString(vm, 1);
size_t length = strlen(text);
WrenHandle* fiber = wrenGetSlotHandle(vm, 2);
data->fiber = fiber;
typedef struct {
uv_write_t req;
uv_buf_t buf;
} WriteRequest;
WriteRequest* wr = (WriteRequest*)malloc(sizeof(WriteRequest));
wr->req.data = data;
wr->buf.base = malloc(length);
wr->buf.len = length;
memcpy(wr->buf.base, text, length);
uv_write(&wr->req, (uv_stream_t*)data->handle, &wr->buf, 1, writeCallback_safe);
}
static void allocCallback(uv_handle_t* handle, size_t suggestedSize, uv_buf_t* buf)
{
buf->base = (char*)malloc(suggestedSize);
buf->len = suggestedSize;
}
static void readCallback(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf)
{
SocketData* data = (SocketData*)stream->data;
WrenHandle* fiber = data->fiber;
// Stop reading immediately. We only want one chunk.
uv_read_stop(stream);
data->fiber = NULL;
if (nread < 0) {
if (nread == UV_EOF) {
free(buf->base);
schedulerResume(fiber, true);
wrenSetSlotNull(getVM(), 2);
schedulerFinishResume();
return;
}
free(buf->base);
schedulerResumeError(fiber, uv_strerror(nread));
return;
}
// Return the data
schedulerResume(fiber, true);
wrenSetSlotBytes(getVM(), 2, buf->base, nread);
schedulerFinishResume();
free(buf->base);
}
void socketRead(WrenVM* vm)
{
SocketData* data = (SocketData*)wrenGetSlotForeign(vm, 0);
WrenHandle* fiber = wrenGetSlotHandle(vm, 1);
data->fiber = fiber;
uv_read_start((uv_stream_t*)data->handle, allocCallback, readCallback);
}
void socketClose(WrenVM* vm)
{
SocketData* data = (SocketData*)wrenGetSlotForeign(vm, 0);
if (data->handle && !uv_is_closing((uv_handle_t*)data->handle)) {
uv_close((uv_handle_t*)data->handle, closeCallback);
data->handle = NULL;
}
}
// -----------------------------------------------------------------------------
// Server Implementation
// -----------------------------------------------------------------------------
void serverAllocate(WrenVM* vm)
{
wrenSetSlotNewForeign(vm, 0, 0, sizeof(ServerData));
ServerData* data = (ServerData*)wrenGetSlotForeign(vm, 0);
data->handle = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
uv_tcp_init(getLoop(), data->handle);
data->handle->data = data;
data->acceptFiber = NULL;
data->pendingHead = NULL;
data->pendingTail = NULL;
}
void serverFinalize(void* data)
{
ServerData* serverData = (ServerData*)data;
if (serverData->handle) {
uv_close((uv_handle_t*)serverData->handle, closeCallback);
}
// Free pending list
SocketListNode* current = serverData->pendingHead;
while (current) {
SocketListNode* next = current->next;
uv_close((uv_handle_t*)current->handle, closeCallback);
free(current);
current = next;
}
}
void netCaptureClass(WrenVM* vm)
{
// Slot 0 is the Socket class itself because this is a static method on Socket.
socketClassHandle = wrenGetSlotHandle(vm, 0);
}
void netShutdown()
{
if (socketClassHandle != NULL) {
wrenReleaseHandle(getVM(), socketClassHandle);
socketClassHandle = NULL;
}
}
static void connectionCallback(uv_stream_t* server, int status)
{
ServerData* data = (ServerData*)server->data;
if (status < 0) {
return;
}
uv_tcp_t* client = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
uv_tcp_init(getLoop(), client);
if (uv_accept(server, (uv_stream_t*)client) == 0) {
if (data->acceptFiber) {
WrenHandle* fiber = data->acceptFiber;
data->acceptFiber = NULL;
WrenVM* vm = getVM();
wrenEnsureSlots(vm, 3);
if (socketClassHandle == NULL) {
fprintf(stderr, "FATAL: socketClassHandle NULL in connectionCallback\n");
exit(1);
}
wrenSetSlotHandle(vm, 1, socketClassHandle);
wrenSetSlotNewForeign(vm, 2, 1, sizeof(SocketData));
SocketData* sockData = (SocketData*)wrenGetSlotForeign(vm, 2);
sockData->handle = client;
client->data = sockData;
sockData->fiber = NULL;
sockData->connectReq = NULL;
sockData->writeReq = NULL;
schedulerResume(fiber, true);
schedulerFinishResume();
} else {
SocketListNode* node = (SocketListNode*)malloc(sizeof(SocketListNode));
node->handle = client;
node->next = NULL;
if (data->pendingTail) {
data->pendingTail->next = node;
data->pendingTail = node;
} else {
data->pendingHead = node;
data->pendingTail = node;
}
}
} else {
uv_close((uv_handle_t*)client, closeCallback);
}
}
void serverBind(WrenVM* vm)
{
ServerData* data = (ServerData*)wrenGetSlotForeign(vm, 0);
const char* host = wrenGetSlotString(vm, 1);
int port = (int)wrenGetSlotDouble(vm, 2);
struct sockaddr_in addr;
uv_ip4_addr(host, port, &addr);
uv_tcp_bind(data->handle, (const struct sockaddr*)&addr, 0);
int r = uv_listen((uv_stream_t*)data->handle, 128, connectionCallback);
if (r != 0) {
wrenEnsureSlots(vm, 3);
wrenSetSlotString(vm, 2, uv_strerror(r));
wrenAbortFiber(vm, 2);
}
}
void serverAccept(WrenVM* vm)
{
ServerData* data = (ServerData*)wrenGetSlotForeign(vm, 0);
if (data->pendingHead) {
SocketListNode* node = data->pendingHead;
data->pendingHead = node->next;
if (data->pendingHead == NULL) data->pendingTail = NULL;
uv_tcp_t* client = node->handle;
free(node);
wrenEnsureSlots(vm, 3);
if (socketClassHandle == NULL) {
fprintf(stderr, "FATAL: socketClassHandle NULL in serverAccept\n");
exit(1);
}
wrenSetSlotHandle(vm, 2, socketClassHandle);
wrenSetSlotNewForeign(vm, 0, 2, sizeof(SocketData));
SocketData* sockData = (SocketData*)wrenGetSlotForeign(vm, 0);
sockData->handle = client;
client->data = sockData;
sockData->fiber = NULL;
sockData->connectReq = NULL;
sockData->writeReq = NULL;
} else {
WrenHandle* fiber = wrenGetSlotHandle(vm, 1);
data->acceptFiber = fiber;
wrenSetSlotNull(vm, 0);
}
}
void serverClose(WrenVM* vm)
{
ServerData* data = (ServerData*)wrenGetSlotForeign(vm, 0);
if (data->handle && !uv_is_closing((uv_handle_t*)data->handle)) {
uv_close((uv_handle_t*)data->handle, closeCallback);
data->handle = NULL;
}
}
+24
View File
@@ -0,0 +1,24 @@
#ifndef net_h
#define net_h
#include "wren.h"
void netBindSocketClass(WrenVM* vm);
void netBindServerClass(WrenVM* vm);
// Socket methods
void socketAllocate(WrenVM* vm);
void socketFinalize(void* data);
void socketConnect(WrenVM* vm);
void socketWrite(WrenVM* vm);
void socketRead(WrenVM* vm);
void socketClose(WrenVM* vm);
// Server methods
void serverAllocate(WrenVM* vm);
void serverFinalize(void* data);
void serverBind(WrenVM* vm);
void serverAccept(WrenVM* vm);
void serverClose(WrenVM* vm);
#endif
+62
View File
@@ -0,0 +1,62 @@
import "scheduler" for Scheduler
foreign class Socket {
construct new() {}
static connect(host, port) {
if (!(host is String)) Fiber.abort("Host must be a string.")
if (!(port is Num)) Fiber.abort("Port must be a number.")
var socket = Socket.new()
Scheduler.await_ { socket.connect_(host, port, Fiber.current) }
return socket
}
write(data) {
if (!(data is String)) Fiber.abort("Data must be a string.")
return Scheduler.await_ { write_(data, Fiber.current) }
}
read() {
return Scheduler.await_ { read_(Fiber.current) }
}
close() {
close_()
}
foreign connect_(host, port, fiber)
foreign write_(data, fiber)
foreign read_(fiber)
foreign close_()
foreign static captureClass_()
}
Socket.captureClass_()
foreign class Server {
construct new() {}
static bind(host, port) {
if (!(host is String)) Fiber.abort("Host must be a string.")
if (!(port is Num)) Fiber.abort("Port must be a number.")
var server = Server.new()
server.bind_(host, port)
return server
}
accept() {
var socket = accept_(Fiber.current)
if (socket != null) return socket
return Scheduler.runNextScheduled_()
}
close() {
close_()
}
foreign bind_(host, port)
foreign accept_(fiber)
foreign close_()
}
+66
View File
@@ -0,0 +1,66 @@
// Please do not edit this file. It has been generated automatically
// from `src/module/net.wren` using `util/wren_to_c_string.py`
static const char* netModuleSource =
"import \"scheduler\" for Scheduler\n"
"\n"
"foreign class Socket {\n"
" construct new() {}\n"
"\n"
" static connect(host, port) {\n"
" if (!(host is String)) Fiber.abort(\"Host must be a string.\")\n"
" if (!(port is Num)) Fiber.abort(\"Port must be a number.\")\n"
" \n"
" var socket = Socket.new()\n"
" Scheduler.await_ { socket.connect_(host, port, Fiber.current) }\n"
" return socket\n"
" }\n"
"\n"
" write(data) {\n"
" if (!(data is String)) Fiber.abort(\"Data must be a string.\")\n"
" return Scheduler.await_ { write_(data, Fiber.current) }\n"
" }\n"
"\n"
" read() {\n"
" return Scheduler.await_ { read_(Fiber.current) }\n"
" }\n"
"\n"
" close() {\n"
" close_()\n"
" }\n"
"\n"
" foreign connect_(host, port, fiber)\n"
" foreign write_(data, fiber)\n"
" foreign read_(fiber)\n"
" foreign close_()\n"
" foreign static captureClass_()\n"
"}\n"
"\n"
"Socket.captureClass_()\n"
"\n"
"foreign class Server {\n"
" construct new() {}\n"
"\n"
" static bind(host, port) {\n"
" if (!(host is String)) Fiber.abort(\"Host must be a string.\")\n"
" if (!(port is Num)) Fiber.abort(\"Port must be a number.\")\n"
" \n"
" var server = Server.new()\n"
" server.bind_(host, port)\n"
" return server\n"
" }\n"
"\n"
" accept() {\n"
" var socket = accept_(Fiber.current)\n"
" if (socket != null) return socket\n"
" return Scheduler.runNextScheduled_()\n"
" }\n"
" \n"
" close() {\n"
" close_()\n"
" }\n"
"\n"
" foreign bind_(host, port)\n"
" foreign accept_(fiber)\n"
" foreign close_()\n"
"}\n";