Networking.

This commit is contained in:
retoor 2026-01-24 09:57:59 +01:00
parent 1855363661
commit 1a08b3adf0
25 changed files with 1371 additions and 1 deletions

79
GEMINI.md Normal file
View File

@ -0,0 +1,79 @@
# Wren CLI Project
## Project Overview
The Wren CLI is a command-line interface and REPL (Read-Eval-Print Loop) for the Wren programming language. It embeds the Wren Virtual Machine (VM) and provides standard IO capabilities (file system, networking, etc.) using `libuv`.
**Key Components:**
* **`src/cli`**: Contains the main entry point (`main.c`) and CLI-specific logic.
* **`src/module`**: Implementations of built-in modules (like `io`, `os`, `scheduler`, `timer`, `net`).
* **`deps/wren`**: The core Wren VM source code.
* **`deps/libuv`**: The asynchronous I/O library used for system interactions.
## New Features
* **Networking**: A full-featured `net` module supporting TCP `Socket` and `Server`.
* **Stderr**: `io` module now includes `Stderr` class.
## Building
The project uses `make` for Linux/macOS and Visual Studio for Windows.
**Linux:**
```bash
cd projects/make
make
```
**macOS:**
```bash
cd projects/make.mac
make
```
**Windows:**
Open `projects/vs2017/wren-cli.sln` or `projects/vs2019/wren-cli.sln` in Visual Studio.
**Output:**
The compiled binary is placed in the `bin/` directory.
* Release: `bin/wren_cli`
* Debug: `bin/wren_cli_d`
## Running
**REPL:**
Run the binary without arguments to start the interactive shell:
```bash
./bin/wren_cli
```
**Run a Script:**
Pass the path to a Wren script:
```bash
./bin/wren_cli path/to/script.wren
```
## Testing
The project uses a Python script to run the test suite.
**Run All Tests:**
```bash
python util/test.py
```
**Run Specific Suite:**
You can pass a path substring to run a subset of tests:
```bash
python util/test.py io
python util/test.py language
```
**Test Structure:**
Tests are located in the `test/` directory. They are written in Wren and use expectation comments (e.g., `// expect: ...`) which the test runner parses to verify output.
## Development Conventions
* **Module Implementation**: Built-in modules are often split between C and Wren. The Wren side (`.wren` files in `src/module`) uses `foreign` declarations which are bound to C functions (`.c` files in `src/module`).
* **Code Style**: Follows the style of the existing C and Wren files.
* **Build System**: The `Makefile`s are generated by `premake`. If you need to change the build configuration, modify `projects/premake/premake5.lua` and regenerate the projects (though you can editing Makefiles directly for small local changes).

15
example/benchmark/fib.wren vendored Normal file
View File

@ -0,0 +1,15 @@
import "os" for Process
var start = System.clock
var fib
fib = Fn.new { |n|
if (n < 2) return n
return fib.call(n - 1) + fib.call(n - 2)
}
for (i in 1..5) {
System.print("fib(28) = " + fib.call(28).toString)
}
System.print("Elapsed: " + (System.clock - start).toString + "s")

148
example/benchmark/nbody.wren vendored Normal file
View File

@ -0,0 +1,148 @@
import "os" for Process
class Body {
construct new(x, y, z, vx, vy, vz, mass) {
_x = x
_y = y
_z = z
_vx = vx
_vy = vy
_vz = vz
_mass = mass
}
x { _x }
x=(v) { _x = v }
y { _y }
y=(v) { _y = v }
z { _z }
z=(v) { _z = v }
vx { _vx }
vx=(v) { _vx = v }
vy { _vy }
vy=(v) { _vy = v }
vz { _vz }
vz=(v) { _vz = v }
mass { _mass }
offsetMomentum(px, py, pz) {
_vx = -px / 0.01227246237529089
_vy = -py / 0.01227246237529089
_vz = -pz / 0.01227246237529089
}
}
class NBody {
static init() {
var pi = 3.141592653589793
var solarMass = 4 * pi * pi
var daysPerYear = 365.24
__bodies = [
// Sun
Body.new(0, 0, 0, 0, 0, 0, solarMass),
// Jupiter
Body.new(
4.84143144246472090e+00,
-1.16032004402742839e+00,
-1.03622044471123109e-01,
1.66007664274403694e-03 * daysPerYear,
7.69901118419740425e-03 * daysPerYear,
-6.90460016972063023e-05 * daysPerYear,
9.54791938424326609e-04 * solarMass),
// Saturn
Body.new(
8.34336671824457987e+00,
4.12479856412430479e+00,
-4.03523417114321381e-01,
-2.76742510726862411e-03 * daysPerYear,
4.99852801234917238e-03 * daysPerYear,
2.30417297573763929e-05 * daysPerYear,
2.85885980666130812e-04 * solarMass),
// Uranus
Body.new(
1.28943695618239209e+01,
-1.51111514016986312e+01,
-2.23307578892655734e-01,
2.96460137564761618e-03 * daysPerYear,
2.37847173959480950e-03 * daysPerYear,
-2.96589568540237556e-05 * daysPerYear,
4.36624404335156298e-05 * solarMass),
// Neptune
Body.new(
1.53796971148509165e+01,
-2.59193146099879641e+01,
1.79258772950371181e-01,
2.68067772490389322e-03 * daysPerYear,
1.62824170038242295e-03 * daysPerYear,
-9.51592254519715870e-05 * daysPerYear,
5.15138902046611451e-05 * solarMass)
]
var px = 0
var py = 0
var pz = 0
for (b in __bodies) {
px = px + b.vx * b.mass
py = py + b.vy * b.mass
pz = pz + b.vz * b.mass
}
__bodies[0].offsetMomentum(px, py, pz)
}
static energy() {
var e = 0
for (i in 0...__bodies.count) {
var bi = __bodies[i]
e = e + 0.5 * bi.mass * (bi.vx * bi.vx + bi.vy * bi.vy + bi.vz * bi.vz)
for (j in i + 1...__bodies.count) {
var bj = __bodies[j]
var dx = bi.x - bj.x
var dy = bi.y - bj.y
var dz = bi.z - bj.z
var distance = (dx * dx + dy * dy + dz * dz).sqrt
e = e - (bi.mass * bj.mass) / distance
}
}
return e
}
static advance(dt) {
for (i in 0...__bodies.count) {
var bi = __bodies[i]
for (j in i + 1...__bodies.count) {
var bj = __bodies[j]
var dx = bi.x - bj.x
var dy = bi.y - bj.y
var dz = bi.z - bj.z
var d2 = dx * dx + dy * dy + dz * dz
var mag = dt / (d2 * d2.sqrt)
bi.vx = bi.vx - dx * bj.mass * mag
bi.vy = bi.vy - dy * bj.mass * mag
bi.vz = bi.vz - dz * bj.mass * mag
bj.vx = bj.vx + dx * bi.mass * mag
bj.vy = bj.vy + dy * bi.mass * mag
bj.vz = bj.vz + dz * bi.mass * mag
}
}
for (b in __bodies) {
b.x = b.x + dt * b.vx
b.y = b.y + dt * b.vy
b.z = b.z + dt * b.vz
}
}
static run(n) {
init()
System.print(energy())
for (i in 0...n) advance(0.01)
System.print(energy())
}
}
var start = System.clock
NBody.run(100000)
System.print("Elapsed: " + (System.clock - start).toString + "s")

57
example/chat_server.wren vendored Normal file
View File

@ -0,0 +1,57 @@
// retoor <retoor@molodetz.nl>
import "net" for Server, Socket
import "scheduler" for Scheduler
class ChatServer {
construct new(port) {
_port = port
_clients = []
_server = Server.bind("0.0.0.0", port)
System.print("Chat Server listening on port %(_port)")
}
run() {
while (true) {
var socket = _server.accept()
handleNewClient(socket)
}
}
handleNewClient(socket) {
_clients.add(socket)
var id = _clients.count
System.print("Client %(id) connected. Total: %(_clients.count)")
Fiber.new {
broadcast("Client %(id) joined the chat!\n", socket)
while (true) {
var message = socket.read()
if (message == null || message == "") break
System.print("Client %(id): %(message.trim())")
broadcast("Client %(id): %(message)", socket)
}
System.print("Client %(id) disconnected.")
_clients.remove(socket)
socket.close()
broadcast("Client %(id) left the chat.\n", null)
}.call()
}
broadcast(message, sender) {
for (client in _clients) {
if (client != sender) {
// We use a separate fiber for each write to prevent one slow
// client from blocking the broadcast to others
Fiber.new {
client.write(message)
}.call()
}
}
}
}
var server = ChatServer.new(7070)
server.run()

25
example/echo_server.wren vendored Normal file
View File

@ -0,0 +1,25 @@
import "net" for Server
import "scheduler" for Scheduler
var port = 9090
var server = Server.bind("0.0.0.0", port)
System.print("Echo Server running on port %(port)")
while (true) {
var socket = server.accept()
System.print("New connection accepted")
Fiber.new {
var socket_ = socket // Capture variable
while (true) {
var data = socket_.read()
if (data == null) {
break
}
System.print("Received: %(data.count) bytes")
socket_.write(data)
}
System.print("Connection closed")
socket_.close()
}.call()
}

57
example/file_explorer.wren vendored Normal file
View File

@ -0,0 +1,57 @@
// retoor <retoor@molodetz.nl>
import "io" for Directory, File, Stat
class FileExplorer {
static listRecursive(path, depth) {
var indent = " " * depth
var files = []
// We wrap Directory.list in a try to handle permission errors
var fiber = Fiber.new {
files = Directory.list(path)
}
fiber.try()
if (fiber.error != null) {
System.print("\%(indent)[!] Error accessing \%(path)")
return
}
// Sort files manually since String comparison isn't built-in
files.sort(Fn.new {|a, b|
var ba = a.bytes
var bb = b.bytes
var len = ba.count < bb.count ? ba.count : bb.count
for (i in 0...len) {
if (ba[i] < bb[i]) return true
if (ba[i] > bb[i]) return false
}
return ba.count < bb.count
})
for (file in files) {
if (file == "." || file == "..") continue
var fullPath = path + "/" + file
if (path == ".") fullPath = "./" + file
var stat = Stat.path(fullPath)
if (stat.isDirectory) {
System.print("\%(indent)[D] \%(file)/")
listRecursive(fullPath, depth + 1)
} else {
var size = stat.size
var unit = "B"
if (size > 1024) {
size = size / 1024
unit = "KB"
}
System.print("\%(indent)[F] \%(file) (\%(size.round)\%(unit))")
}
}
}
}
System.print("Recursive directory listing for current path:")
System.print("--------------------------------------------")
FileExplorer.listRecursive(".", 0)

42
example/http_client.wren vendored Normal file
View File

@ -0,0 +1,42 @@
// retoor <retoor@molodetz.nl>
import "net" for Socket
import "scheduler" for Scheduler
class HttpClient {
static get(host, port, path) {
System.print("Connecting to %(host):%(port)...")
var socket = Socket.connect(host, port)
var request = "GET %(path) HTTP/1.1\r\n" +
"Host: %(host)\r\n" +
"User-Agent: Wren-CLI\r\n" +
"Connection: close\r\n" +
"\r\n"
System.print("Sending request...")
socket.write(request)
System.print("Waiting for response...")
var response = ""
while (true) {
var chunk = socket.read()
if (chunk == null) break
response = response + chunk
}
socket.close()
return response
}
}
// Example usage: Fetching from a local server or a known public IP
// Note: This implementation is for raw IP/DNS if supported by uv_tcp_connect.
// Since our net.c uses uv_ip4_addr, we use localhost for the example.
var host = "127.0.0.1"
var port = 8080 // Try to connect to our own http_server example
var path = "/"
var result = HttpClient.get(host, port, path)
System.print("\n--- Response Received ---
")
System.print(result)

190
example/http_server.wren vendored Normal file
View File

@ -0,0 +1,190 @@
import "net" for Server, Socket
import "io" for File, Directory, Stat
import "scheduler" for Scheduler
class HttpServer {
construct new(port) {
_port = port
_server = Server.bind("0.0.0.0", port)
System.print("HTTP Server listening on http://localhost:%(port)")
}
run() {
while (true) {
var socket = _server.accept()
Fiber.new {
handleClient_(socket)
}.call()
}
}
handleClient_(socket) {
var requestData = socket.read()
if (requestData == null || requestData == "") {
socket.close()
return
}
var lines = requestData.split("\r\n")
if (lines.count == 0) {
socket.close()
return
}
var requestLine = lines[0].split(" ")
if (requestLine.count < 2) {
sendError_(socket, 400, "Bad Request")
return
}
var method = requestLine[0]
var path = requestLine[1]
System.print("%(method) %(path)")
if (method != "GET") {
sendError_(socket, 405, "Method Not Allowed")
return
}
path = path.replace("\%20", " ")
if (path.contains("..")) {
sendError_(socket, 403, "Forbidden")
return
}
var localPath = "." + path
if (localPath.endsWith("/")) localPath = localPath[0..-2]
if (localPath == "") localPath = "."
if (!exists(localPath)) {
sendError_(socket, 404, "Not Found")
return
}
if (isDirectory(localPath)) {
serveDirectory_(socket, localPath, path)
} else {
serveFile_(socket, localPath)
}
socket.close()
}
exists(path) {
if (File.exists(path)) return true
if (Directory.exists(path)) return true
return false
}
isDirectory(path) {
return Directory.exists(path)
}
serveDirectory_(socket, localPath, requestPath) {
var files
var fiber = Fiber.new {
files = Directory.list(localPath)
}
fiber.try()
if (fiber.error != null) {
sendError_(socket, 500, "Internal Server Error: " + fiber.error)
return
}
files.sort(Fn.new {|a, b|
var ba = a.bytes
var bb = b.bytes
var len = ba.count
if (bb.count < len) len = bb.count
for (i in 0...len) {
if (ba[i] < bb[i]) return true
if (ba[i] > bb[i]) return false
}
return ba.count < bb.count
})
var html = "<!DOCTYPE html><html><head><title>Index of %(requestPath)</title></head><body>"
html = html + "<h1>Index of %(requestPath)</h1><hr><ul>"
if (requestPath != "/") {
var parent = requestPath.split("/")
if (parent.count > 1) {
parent.removeAt(-1)
var parentPath = parent.join("/")
if (parentPath == "") parentPath = "/"
html = html + "<li><a href=\"%(parentPath)\">..</a></li>"
}
}
for (file in files) {
var href = requestPath
if (!href.endsWith("/")) href = href + "/"
href = href + file
html = html + "<li><a href=\"%(href)\">%(file)</a></li>"
}
html = html + "</ul><hr></body></html>"
sendResponse_(socket, 200, "OK", "text/html", html)
}
serveFile_(socket, localPath) {
var content
var fiber = Fiber.new {
content = File.read(localPath)
}
fiber.try()
if (fiber.error != null) {
sendError_(socket, 500, "Error reading file: " + fiber.error)
return
}
var contentType = "application/octet-stream"
if (localPath.endsWith(".html")) {
contentType = "text/html"
} else if (localPath.endsWith(".txt")) {
contentType = "text/plain"
} else if (localPath.endsWith(".wren")) {
contentType = "text/plain"
} else if (localPath.endsWith(".c")) {
contentType = "text/plain"
} else if (localPath.endsWith(".h")) {
contentType = "text/plain"
} else if (localPath.endsWith(".md")) {
contentType = "text/markdown"
} else if (localPath.endsWith(".json")) {
contentType = "application/json"
} else if (localPath.endsWith(".png")) {
contentType = "image/png"
} else if (localPath.endsWith(".jpg")) {
contentType = "image/jpeg"
}
sendResponse_(socket, 200, "OK", contentType, content)
}
sendError_(socket, code, message) {
var html = "<h1>%(code) %(message)</h1>"
sendResponse_(socket, code, message, "text/html", html)
socket.close()
}
sendResponse_(socket, code, status, contentType, body) {
var response = "HTTP/1.1 %(code) %(status)\r\n" +
"Content-Type: %(contentType)\r\n" +
"Content-Length: %(body.count)\r\n" +
"Connection: close\r\n" +
"\r\n" +
body
socket.write(response)
}
}
var server = HttpServer.new(8080)
server.run()

57
example/system_dashboard.wren vendored Normal file
View File

@ -0,0 +1,57 @@
// retoor <retoor@molodetz.nl>
import "os" for Platform, Process
import "io" for Directory
class Dashboard {
static show() {
System.print("========================================")
System.print(" WREN CLI SYSTEM DASHBOARD ")
System.print("========================================")
System.print("PLATFORM INFO:")
System.print(" OS Name: %(Platform.name)")
System.print(" POSIX: %(Platform.isPosix)")
System.print(" Home: %(Platform.homePath)")
System.print("\nPROCESS INFO:")
System.print(" PID: %(Process.pid)")
System.print(" PPID: %(Process.ppid)")
System.print(" CWD: %(Process.cwd)")
System.print(" Version: %(Process.version)")
System.print("\nARGUMENTS:")
if (Process.arguments.count == 0) {
System.print(" (none)")
} else {
for (i in 0...Process.arguments.count) {
System.print(" [%(i)]: %(Process.arguments[i])")
}
}
System.print("\nWORKING DIRECTORY FILES:")
var files = Directory.list(".")
files.sort(Fn.new {|a, b|
var ba = a.bytes
var bb = b.bytes
var len = ba.count < bb.count ? ba.count : bb.count
for (i in 0...len) {
if (ba[i] < bb[i]) return true
if (ba[i] > bb[i]) return false
}
return ba.count < bb.count
})
var count = 0
for (file in files) {
if (count > 10) {
System.print(" ... and %(files.count - 10) more")
break
}
System.print(" - %(file)")
count = count + 1
}
System.print("========================================")
}
}
Dashboard.show()

51
example/task_worker.wren vendored Normal file
View File

@ -0,0 +1,51 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler
import "timer" for Timer
class TaskQueue {
construct new() {
_tasks = []
_running = true
}
add(name, duration) {
System.print("[Queue] Adding task: \%(name) (\%(duration)ms)")
_tasks.add({
System.print("[Worker] Starting \%(name)...")
Timer.sleep(duration)
System.print("[Worker] Finished \%(name).")
})
}
start() {
System.print("[Queue] Starting task runner...")
Fiber.new {
while (_running) {
if (!_tasks.isEmpty) {
var task = _tasks.removeAt(0)
task.call()
}
Timer.sleep(100) // Yield to other fibers
}
}.call()
}
stop() { _running = false }
}
var queue = TaskQueue.new()
// Start the worker in the background
queue.start()
// Add some tasks from the main thread
queue.add("Task A", 1500)
queue.add("Task B", 500)
queue.add("Task C", 1000)
System.print("[Main] All tasks queued. Main fiber is free.")
// Keep the main loop alive for a bit to see the output
Timer.sleep(4000)
queue.stop()
System.print("[Main] Shutdown.")

1
projects/make/server.log Normal file
View File

@ -0,0 +1 @@
bash: line 2: ./bin/wren_cli: No such file or directory

View File

@ -117,6 +117,7 @@ OBJECTS += $(OBJDIR)/loop-watcher.o
OBJECTS += $(OBJDIR)/loop.o
OBJECTS += $(OBJDIR)/main.o
OBJECTS += $(OBJDIR)/modules.o
OBJECTS += $(OBJDIR)/net.o
OBJECTS += $(OBJDIR)/os.o
OBJECTS += $(OBJDIR)/path.o
OBJECTS += $(OBJDIR)/pipe.o
@ -365,6 +366,9 @@ $(OBJDIR)/vm.o: ../../src/cli/vm.c
$(OBJDIR)/io.o: ../../src/module/io.c
@echo $(notdir $<)
$(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"
$(OBJDIR)/net.o: ../../src/module/net.c
@echo $(notdir $<)
$(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"
$(OBJDIR)/os.o: ../../src/module/os.c
@echo $(notdir $<)
$(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"

4
sanity_check.wren vendored Normal file
View File

@ -0,0 +1,4 @@
System.print("Hello, world!")
var s = "string"
var n = 123
System.print("Joined: " + s + " " + n.toString)

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)

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);

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
src/module/io.wren vendored
View File

@ -303,3 +303,7 @@ class Stdin {
class Stdout {
foreign static flush()
}
class Stderr {
foreign static write(string)
}

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
src/module/net.c Normal file
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
src/module/net.h Normal file
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
src/module/net.wren vendored Normal file
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
src/module/net.wren.inc Normal file
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";

3
test_stderr.wren vendored Normal file
View File

@ -0,0 +1,3 @@
import "io" for Stderr
Stderr.write("This is an error message.\n")

10
test_suspend.wren vendored Normal file
View File

@ -0,0 +1,10 @@
System.print("Start")
Fiber.new {
System.print("In new fiber, suspending")
Fiber.suspend()
System.print("New fiber resumed")
}.call()
System.print("Back in main fiber")
Fiber.suspend()
System.print("Main fiber resumed")

8
test_transfer.wren vendored Normal file
View File

@ -0,0 +1,8 @@
var main = Fiber.current
System.print("Start")
Fiber.new {
System.print("In new fiber")
main.transfer()
System.print("Resumed")
}.transfer()
System.print("Back in main")