Files
cell/source/qjs_enet.c
John Alanbrook e9519484cc
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
fix enet; add enet testing
2025-02-24 13:43:01 -06:00

501 lines
16 KiB
C

// qjs_enet.c
#include "quickjs.h"
#include <enet/enet.h>
#include <stdio.h>
#include <string.h>
#define countof(a) (sizeof(a)/sizeof(*(a)))
static JSClassID enet_host_id;
static JSClassID enet_peer_class_id;
/* Finalizers */
static void js_enet_host_finalizer(JSRuntime *rt, JSValue val) {
ENetHost *host = JS_GetOpaque(val, enet_host_id);
if (host) {
enet_host_destroy(host);
}
}
static void js_enet_peer_finalizer(JSRuntime *rt, JSValue val) {
ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id);
// No explicit cleanup needed for ENetPeer itself
(void)peer;
}
/* ENet init/deinit */
static JSValue js_enet_initialize(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (enet_initialize() != 0) {
return JS_ThrowInternalError(ctx, "Error initializing ENet.");
}
return JS_UNDEFINED;
}
static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
enet_deinitialize();
return JS_UNDEFINED;
}
/* Host creation */
static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
ENetHost *host;
ENetAddress address;
JSValue obj;
if (argc < 1) {
// Create client-like host, unbound
host = enet_host_create(NULL, 32, 2, 0, 0);
if (!host) {
return JS_ThrowInternalError(ctx, "Failed to create ENet host (null address).");
}
goto RET;
}
// If arg is provided, interpret as "ip:port" for server
const char *address_str = JS_ToCString(ctx, argv[0]);
if (!address_str) {
return JS_EXCEPTION; // memory or conversion error
}
char ip[64];
int port;
if (sscanf(address_str, "%63[^:]:%d", ip, &port) != 2) {
JS_FreeCString(ctx, address_str);
return JS_ThrowTypeError(ctx, "Invalid address format. Expected 'ip:port'.");
}
JS_FreeCString(ctx, address_str);
int err = enet_address_set_host_ip(&address, ip);
if (err != 0) {
return JS_ThrowInternalError(ctx, "Failed to set host IP from %s. Error %d.", ip, err);
}
address.port = port;
// Create server host with max 32 clients, 2 channels
host = enet_host_create(&address, 32, 2, 0, 0);
if (!host) {
return JS_ThrowInternalError(ctx, "Failed to create ENet host.");
}
RET:
obj = JS_NewObjectClass(ctx, enet_host_id);
if (JS_IsException(obj)) {
enet_host_destroy(host);
return obj;
}
JS_SetOpaque(obj, host);
return obj;
}
/* Host service: poll for events */
static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) {
return JS_EXCEPTION;
}
// Expect a callback function
if (argc < 1 || !JS_IsFunction(ctx, argv[0])) {
return JS_ThrowTypeError(ctx, "Expected a callback function as first argument.");
}
JSValue callback = argv[0];
JS_DupValue(ctx, callback);
// Optional timeout
int timeout = 0;
if (argc > 1) {
JS_ToInt32(ctx, &timeout, argv[1]);
}
ENetEvent event;
while (enet_host_service(host, &event, timeout) > 0) {
JSValue event_obj = JS_NewObject(ctx);
switch (event.type) {
case ENET_EVENT_TYPE_CONNECT: {
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "connect"));
JSValue peer_obj = JS_NewObjectClass(ctx, enet_peer_class_id);
if (JS_IsException(peer_obj)) {
JS_FreeValue(ctx, event_obj);
JS_FreeValue(ctx, callback);
return peer_obj;
}
JS_SetOpaque(peer_obj, event.peer);
JS_SetPropertyStr(ctx, event_obj, "peer", peer_obj);
break;
}
case ENET_EVENT_TYPE_RECEIVE: {
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "receive"));
JS_SetPropertyStr(ctx, event_obj, "channelID", JS_NewInt32(ctx, event.channelID));
char *tmp = js_mallocz(ctx, event.packet->dataLength+1);
memcpy(tmp, event.packet->data, event.packet->dataLength);
tmp[event.packet->dataLength] = '\0';
// We expect strictly a JSON object
JSValue packet_data = JS_ParseJSON(ctx,
tmp,
event.packet->dataLength,
"<enet-packet>");
js_free(ctx,tmp);
if (JS_IsException(packet_data)) {
// Malformed JSON -> throw error, abort
printf("INVALID JSON!\n");
enet_packet_destroy(event.packet);
JS_FreeValue(ctx, event_obj);
JS_FreeValue(ctx, callback);
return JS_ThrowTypeError(ctx, "Received invalid JSON (parse error).");
}
if (!JS_IsObject(packet_data)) {
// It might be a string/number/array/... -> we want only a plain object
JS_FreeValue(ctx, event_obj);
JS_FreeValue(ctx, callback);
JS_FreeValue(ctx, packet_data);
enet_packet_destroy(event.packet);
return JS_ThrowTypeError(ctx,
"Received data is not an object (must send a plain object).");
}
JS_SetPropertyStr(ctx, event_obj, "data", packet_data);
enet_packet_destroy(event.packet);
break;
}
case ENET_EVENT_TYPE_DISCONNECT:
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect"));
break;
case ENET_EVENT_TYPE_NONE:
JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "none"));
break;
}
// Invoke callback
JS_Call(ctx, callback, JS_UNDEFINED, 1, &event_obj);
JS_FreeValue(ctx, event_obj);
}
JS_FreeValue(ctx, callback);
return JS_UNDEFINED;
}
/* Host connect: client -> connect to server */
static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) {
return JS_EXCEPTION;
}
if (argc < 2) {
return JS_ThrowTypeError(ctx, "Expected 2 arguments: hostname, port.");
}
const char *hostname = JS_ToCString(ctx, argv[0]);
if (!hostname) {
return JS_EXCEPTION; // out of memory or conversion error
}
int port;
JS_ToInt32(ctx, &port, argv[1]);
ENetAddress address;
enet_address_set_host(&address, hostname);
JS_FreeCString(ctx, hostname);
address.port = port;
ENetPeer *peer = enet_host_connect(host, &address, 2, 0);
if (!peer) {
return JS_ThrowInternalError(ctx, "Failed to initiate connection.");
}
JSValue peer_obj = JS_NewObjectClass(ctx, enet_peer_class_id);
if (JS_IsException(peer_obj)) {
return peer_obj;
}
JS_SetOpaque(peer_obj, peer);
return peer_obj;
}
/* Flush queued packets */
static JSValue js_enet_host_flush(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) {
return JS_EXCEPTION;
}
enet_host_flush(host);
return JS_UNDEFINED;
}
/* Broadcast a plain object */
static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
if (!host) {
return JS_EXCEPTION;
}
if (argc < 1) {
return JS_ThrowTypeError(ctx, "Expected an object to broadcast.");
}
// Must be a JavaScript object
if (!JS_IsObject(argv[0])) {
return JS_ThrowTypeError(ctx,
"broadcast() only accepts a plain JS object, not strings/numbers.");
}
// JSON.stringify the object
JSValue json_data = JS_JSONStringify(ctx, argv[0], JS_NULL, JS_NULL);
if (JS_IsException(json_data)) {
return JS_ThrowTypeError(ctx,
"Failed to stringify object (circular ref or non-serializable).");
}
size_t data_len;
const char *data_str = JS_ToCStringLen(ctx, &data_len, json_data);
JS_FreeValue(ctx, json_data);
if (!data_str) {
return JS_EXCEPTION; // out of memory
}
ENetPacket *packet = enet_packet_create(data_str, data_len, ENET_PACKET_FLAG_RELIABLE);
JS_FreeCString(ctx, data_str);
if (!packet) {
return JS_ThrowInternalError(ctx, "Failed to create ENet packet.");
}
enet_host_broadcast(host, 0, packet);
return JS_UNDEFINED;
}
/* Peer-level operations */
static JSValue js_enet_peer_disconnect(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) {
return JS_EXCEPTION;
}
enet_peer_disconnect(peer, 0);
return JS_UNDEFINED;
}
/* Peer send must only accept an object */
static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) {
return JS_EXCEPTION;
}
if (argc < 1) {
return JS_ThrowTypeError(ctx, "Expected an object to send.");
}
if (!JS_IsObject(argv[0])) {
return JS_ThrowTypeError(ctx,
"peer.send() only accepts a plain JS object, not strings/numbers.");
}
JSValue json_data = JS_JSONStringify(ctx, argv[0], JS_NULL, JS_NULL);
if (JS_IsException(json_data)) {
return JS_ThrowTypeError(ctx,
"Failed to stringify object (circular ref or non-serializable).");
}
size_t data_len;
const char *data_str = JS_ToCStringLen(ctx, &data_len, json_data);
JS_FreeValue(ctx, json_data);
if (!data_str) {
return JS_EXCEPTION;
}
// Create packet
ENetPacket *packet = enet_packet_create(data_str, data_len, ENET_PACKET_FLAG_RELIABLE);
JS_FreeCString(ctx, data_str);
if (!packet) {
return JS_ThrowInternalError(ctx, "Failed to create ENet packet.");
}
if (enet_peer_send(peer, 0, packet) < 0) {
return JS_ThrowInternalError(ctx, "enet_peer_send returned error.");
}
return JS_UNDEFINED;
}
static JSValue js_enet_peer_disconnect_now(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) {
return JS_EXCEPTION;
}
enet_peer_disconnect_now(peer, 0);
return JS_UNDEFINED;
}
static JSValue js_enet_peer_disconnect_later(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) {
return JS_EXCEPTION;
}
enet_peer_disconnect_later(peer, 0);
return JS_UNDEFINED;
}
static JSValue js_enet_peer_reset(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) {
return JS_EXCEPTION;
}
enet_peer_reset(peer);
return JS_UNDEFINED;
}
static JSValue js_enet_peer_ping(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) {
return JS_EXCEPTION;
}
enet_peer_ping(peer);
return JS_UNDEFINED;
}
static JSValue js_enet_peer_throttle_configure(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) {
return JS_EXCEPTION;
}
int interval, acceleration, deceleration;
if (argc < 3 ||
JS_ToInt32(ctx, &interval, argv[0]) ||
JS_ToInt32(ctx, &acceleration, argv[1]) ||
JS_ToInt32(ctx, &deceleration, argv[2])) {
return JS_ThrowTypeError(ctx,
"Expected 3 int arguments: interval, acceleration, deceleration");
}
enet_peer_throttle_configure(peer, interval, acceleration, deceleration);
return JS_UNDEFINED;
}
static JSValue js_enet_peer_timeout(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
if (!peer) {
return JS_EXCEPTION;
}
int timeout_limit, timeout_min, timeout_max;
if (argc < 3 ||
JS_ToInt32(ctx, &timeout_limit, argv[0]) ||
JS_ToInt32(ctx, &timeout_min, argv[1]) ||
JS_ToInt32(ctx, &timeout_max, argv[2])) {
return JS_ThrowTypeError(ctx,
"Expected 3 integer arguments: timeout_limit, timeout_min, timeout_max");
}
enet_peer_timeout(peer, timeout_limit, timeout_min, timeout_max);
return JS_UNDEFINED;
}
/* Class definitions */
static JSClassDef enet_host = {
"ENetHost",
.finalizer = js_enet_host_finalizer,
};
static JSClassDef enet_peer_class = {
"ENetPeer",
.finalizer = js_enet_peer_finalizer,
};
/* Function lists */
static const JSCFunctionListEntry js_enet_funcs[] = {
JS_CFUNC_DEF("initialize", 0, js_enet_initialize),
JS_CFUNC_DEF("deinitialize", 0, js_enet_deinitialize),
JS_CFUNC_DEF("create_host", 1, js_enet_host_create),
};
static const JSCFunctionListEntry js_enet_host_funcs[] = {
JS_CFUNC_DEF("service", 2, js_enet_host_service),
JS_CFUNC_DEF("connect", 2, js_enet_host_connect),
JS_CFUNC_DEF("flush", 0, js_enet_host_flush),
JS_CFUNC_DEF("broadcast", 1, js_enet_host_broadcast),
};
static const JSCFunctionListEntry js_enet_peer_funcs[] = {
JS_CFUNC_DEF("send", 1, js_enet_peer_send),
JS_CFUNC_DEF("disconnect", 0, js_enet_peer_disconnect),
JS_CFUNC_DEF("disconnect_now", 0, js_enet_peer_disconnect_now),
JS_CFUNC_DEF("disconnect_later", 0, js_enet_peer_disconnect_later),
JS_CFUNC_DEF("reset", 0, js_enet_peer_reset),
JS_CFUNC_DEF("ping", 0, js_enet_peer_ping),
JS_CFUNC_DEF("throttle_configure",3, js_enet_peer_throttle_configure),
JS_CFUNC_DEF("timeout", 3, js_enet_peer_timeout),
};
/* Module entry point */
static int js_enet_init(JSContext *ctx, JSModuleDef *m);
/* This function returns the default export object */
JSValue js_enet_use(JSContext *ctx) {
// Register ENetHost class
JS_NewClassID(&enet_host_id);
JS_NewClass(JS_GetRuntime(ctx), enet_host_id, &enet_host);
JSValue host_proto = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, host_proto, js_enet_host_funcs, countof(js_enet_host_funcs));
JS_SetClassProto(ctx, enet_host_id, host_proto);
// Register ENetPeer class
JS_NewClassID(&enet_peer_class_id);
JS_NewClass(JS_GetRuntime(ctx), enet_peer_class_id, &enet_peer_class);
JSValue peer_proto = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, peer_proto, js_enet_peer_funcs, countof(js_enet_peer_funcs));
JS_SetClassProto(ctx, enet_peer_class_id, peer_proto);
// Optional: store references in a "prosperon.c_types" for your environment
JSValue global = JS_GetGlobalObject(ctx);
JSValue prosp = JS_GetPropertyStr(ctx, global, "prosperon");
JSValue c_types = JS_GetPropertyStr(ctx, prosp, "c_types");
JS_SetPropertyStr(ctx, c_types, "enet_host", JS_DupValue(ctx, host_proto));
JS_SetPropertyStr(ctx, c_types, "enet_peer", JS_DupValue(ctx, peer_proto));
JS_FreeValue(ctx, c_types);
JS_FreeValue(ctx, prosp);
JS_FreeValue(ctx, global);
// Create the default export object with top-level ENet functions
JSValue export_obj = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, export_obj, js_enet_funcs, countof(js_enet_funcs));
return export_obj;
}
static int js_enet_init(JSContext *ctx, JSModuleDef *m) {
return JS_SetModuleExport(ctx, m, "default", js_enet_use(ctx));
}
#ifdef JS_SHARED_LIBRARY
#define JS_INIT_MODULE js_init_module
#else
#define JS_INIT_MODULE js_init_module_enet
#endif
/* Module definition */
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) {
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_enet_init);
if (!m) {
return NULL;
}
JS_AddModuleExport(ctx, m, "default");
return m;
}