#include "cell.h" #define ENET_IMPLEMENTATION #include "enet.h" #include #include #include static JSClassID enet_host_id; static JSClassID enet_peer_class_id; 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_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) { ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id); JS_MarkValue(rt, *(JSValue*)peer->data, mark_func); } static void js_enet_peer_finalizer(JSRuntime *rt, JSValue val) { ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id); JS_FreeValueRT(rt, *(JSValue*)peer->data); free(peer->data); } // Initialize the ENet library. Must be called before using any ENet functionality. 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_NULL; } // Deinitialize the ENet library, cleaning up all resources. Call this when you no longer // need any ENet functionality. static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { enet_deinitialize(); return JS_NULL; } // Create an ENet host for either a client-like unbound host or a server bound to a specific // address and port: // // - If no argument is provided, creates an unbound "client-like" host with default settings // (maximum 32 peers, 2 channels, unlimited bandwidth). // - If you pass an "ip:port" string (e.g. "127.0.0.1:7777"), it creates a server bound to // that address. The server supports up to 32 peers, 2 channels, and unlimited bandwidth. // // Throws an error if host creation fails for any reason. static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { ENetHost *host; ENetAddress address; ENetAddress *send = &address; size_t peer_count = 1000; size_t channel_limit = 0; enet_uint32 incoming_bandwidth = 0; enet_uint32 outgoing_bandwidth = 0; JSValue obj; if (argc < 1 || !JS_IsObject(argv[0])) { host = enet_host_create(NULL, peer_count, channel_limit, incoming_bandwidth, outgoing_bandwidth); if (!host) return JS_ThrowInternalError(ctx, "Failed to create ENet client host"); goto wrap; } JSValue config_obj = argv[0]; JSValue addr_val = JS_GetPropertyStr(ctx, config_obj, "address"); const char *addr_str = JS_IsString(addr_val) ? JS_ToCString(ctx, addr_val) : NULL; JS_FreeValue(ctx, addr_val); if (!addr_str) send = NULL; else { JSValue port_val = JS_GetPropertyStr(ctx, config_obj, "port"); int32_t port32 = 0; JS_ToInt32(ctx, &port32, port_val); JS_FreeValue(ctx, port_val); if (strcmp(addr_str, "any") == 0) address.host = ENET_HOST_ANY; else if (strcmp(addr_str, "broadcast") == 0) enet_address_set_host_ip(&address, "255.255.255.255"); else { int err = enet_address_set_host_ip(&address, addr_str); if (err != 0) { JS_FreeCString(ctx, addr_str); return JS_ThrowInternalError(ctx, "Failed to set host IP from '%s'. Error: %d", addr_str, err); } } address.port = (enet_uint16)port32; JS_FreeCString(ctx, addr_str); } JSValue chan_val = JS_GetPropertyStr(ctx, config_obj, "channels"); JS_ToUint32(ctx, &channel_limit, chan_val); JS_FreeValue(ctx, chan_val); JSValue in_bw_val = JS_GetPropertyStr(ctx, config_obj, "incoming_bandwidth"); JS_ToUint32(ctx, &incoming_bandwidth, in_bw_val); JS_FreeValue(ctx, in_bw_val); JSValue out_bw_val = JS_GetPropertyStr(ctx, config_obj, "outgoing_bandwidth"); JS_ToUint32(ctx, &outgoing_bandwidth, out_bw_val); JS_FreeValue(ctx, out_bw_val); host = enet_host_create(send, peer_count, channel_limit, incoming_bandwidth, outgoing_bandwidth); if (!host) return JS_ThrowInternalError(ctx, "Failed to create ENet host"); wrap: obj = JS_NewObjectClass(ctx, enet_host_id); if (JS_IsException(obj)) { enet_host_destroy(host); return obj; } JS_SetOpaque(obj, host); return obj; } // Helper function to get a JSValue for an ENetPeer. static JSValue peer_get_value(JSContext *ctx, ENetPeer *peer) { if (!peer->data) { peer->data = malloc(sizeof(JSValue)); *(JSValue*)peer->data = JS_NewObjectClass(ctx, enet_peer_class_id); JS_SetOpaque(*(JSValue*)peer->data, peer); } return JS_DupValue(ctx, *(JSValue*)peer->data); } // Poll for and process any available network events (connect, receive, disconnect, or none) // from this host, calling the provided callback for each event. This function loops until // no more events are available in the current timeframe. // // :param callback: A function called once for each available event, receiving an event // object as its single argument. // :param timeout: (optional) Timeout in milliseconds. Defaults to 0 (non-blocking). // :return: None 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; if (argc < 1 || !JS_IsFunction(ctx, argv[0])) return JS_ThrowTypeError(ctx, "Expected a callback function as first argument"); JSValue callback = JS_DupValue(ctx, argv[0]); double secs; JS_ToFloat64(ctx, &secs, argv[1]); ENetEvent event; while (enet_host_service(host, &event, secs*1000.0f) > 0) { JSValue event_obj = JS_NewObject(ctx); JS_SetPropertyStr(ctx, event_obj, "peer", peer_get_value(ctx, event.peer)); switch (event.type) { case ENET_EVENT_TYPE_CONNECT: JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "connect")); 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)); // Pass raw data as string or ArrayBuffer if (event.packet->dataLength > 0) { JSValue data_val = js_new_blob_stoned_copy(ctx, event.packet->data, event.packet->dataLength); JS_SetPropertyStr(ctx, event_obj, "data", data_val); } 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_DISCONNECT_TIMEOUT: JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect_timeout")); break; case ENET_EVENT_TYPE_NONE: JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "none")); break; } // TODO: raise exception? JS_FreeValue(ctx, event_obj); } JS_FreeValue(ctx, callback); return JS_NULL; } // Initiate a connection from this host to a remote server. Throws an error if the // connection cannot be started. // // :param hostname: The hostname or IP address of the remote server (e.g. "example.com" or "127.0.0.1"). // :param port: The port number to connect to. // :return: An ENetPeer object representing the connection. 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; 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, "No available peers for initiating an ENet connection"); return peer_get_value(ctx, peer); } // Flush all pending outgoing packets for this host immediately. // // :return: None 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_NULL; } // Broadcast a string or ArrayBuffer to all connected peers on channel 0. // // :param data: A string or ArrayBuffer to broadcast to all peers. // :return: None 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 a string or ArrayBuffer to broadcast"); const char *data_str = NULL; size_t data_len = 0; uint8_t *buf = NULL; if (JS_IsString(argv[0])) { data_str = JS_ToCStringLen(ctx, &data_len, argv[0]); if (!data_str) return JS_EXCEPTION; } else if (js_is_blob(ctx,argv[0])) { buf = js_get_blob_data(ctx, &data_len, argv[0]); if (!buf) return JS_EXCEPTION; } else { return JS_ThrowTypeError(ctx, "broadcast() only accepts a string or ArrayBuffer"); } ENetPacket *packet = enet_packet_create(data_str ? (const void*)data_str : (const void*)buf, data_len, ENET_PACKET_FLAG_RELIABLE); if (data_str) JS_FreeCString(ctx, data_str); if (!packet) return JS_ThrowInternalError(ctx, "Failed to create ENet packet"); enet_host_broadcast(host, 0, packet); return JS_NULL; } static JSValue js_enet_host_get_port(JSContext *js, JSValueConst self, int argc, JSValueConst *argv) { ENetHost *host = JS_GetOpaque(self, enet_host_id); if (!host) return JS_EXCEPTION; return JS_NewInt32(js, host->address.port); } static JSValue js_enet_host_get_address(JSContext *js, JSValueConst self, int argc, JSValueConst *argv) { ENetHost *me = JS_GetOpaque(self, enet_host_id); if (!me) return JS_EXCEPTION; char ip_str[128]; if (enet_address_get_host_ip(&me->address, ip_str, sizeof(ip_str)) != 0) return JS_NULL; return JS_NewString(js, ip_str); } // Peer-level operations // Request a graceful disconnection from this peer. The connection will close after // pending data is sent. // // :return: None 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_NULL; } // Send a string or ArrayBuffer to this peer on channel 0. // // :param data: A string or ArrayBuffer to send. // :return: None 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 a string or ArrayBuffer to send"); const char *data_str = NULL; size_t data_len = 0; uint8_t *buf = NULL; if (JS_IsString(argv[0])) { data_str = JS_ToCStringLen(ctx, &data_len, argv[0]); if (!data_str) return JS_EXCEPTION; } else if (js_is_blob(ctx,argv[0])) { buf = js_get_blob_data(ctx, &data_len, argv[0]); if (!buf) return JS_EXCEPTION; } else { return JS_ThrowTypeError(ctx, "send() only accepts a string or ArrayBuffer"); } ENetPacket *packet = enet_packet_create(data_str ? (const void*)data_str : (const void*)buf, data_len, ENET_PACKET_FLAG_RELIABLE); if (data_str) 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, "Unable to send packet"); return JS_NULL; } // Immediately terminate the connection to this peer, discarding any pending data. // // :return: None 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_NULL; } // Request a disconnection from this peer after all queued packets are sent. // // :return: None 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_NULL; } // Reset this peer's connection, immediately dropping it and clearing its internal state. // // :return: None 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_NULL; } // Send a ping request to this peer to measure latency. // // :return: None 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_NULL; } // Configure the throttling behavior for this peer, controlling how ENet adjusts its sending // rate based on packet loss or congestion. // // :param interval: The interval (ms) between throttle adjustments. // :param acceleration: The factor to increase sending speed when conditions improve. // :param deceleration: The factor to decrease sending speed when conditions worsen. // :return: None 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_NULL; } 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_NULL; } // Class definitions static JSClassDef enet_host = { "ENetHost", .finalizer = js_enet_host_finalizer, }; static JSClassDef enet_peer_class = { "ENetPeer", .finalizer = js_enet_peer_finalizer, .gc_mark = js_enet_peer_mark }; JSValue js_enet_resolve_hostname(JSContext *js, JSValue self, int argc, JSValue *argv) { // TODO: implement const char *hostname = JS_ToCString(js, argv[0]); JS_FreeCString(js, hostname); return JS_NULL; } 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), JS_CFUNC_DEF("resolve_hostname", 1, js_enet_resolve_hostname), }; 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), JS_CGETSET_DEF("port", js_enet_host_get_port, NULL), JS_CGETSET_DEF("address", js_enet_host_get_address, NULL), }; static JSValue js_enet_peer_get_rtt(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); if (!peer) return JS_EXCEPTION; return JS_NewInt32(ctx, peer->roundTripTime); } static JSValue js_enet_peer_get_incoming_bandwidth(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 (peer->incomingBandwidth == 0) return JS_NewFloat64(ctx, INFINITY); return JS_NewInt32(ctx, peer->incomingBandwidth); } static JSValue js_enet_peer_get_outgoing_bandwidth(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 (peer->outgoingBandwidth == 0) return JS_NewFloat64(ctx, INFINITY); return JS_NewInt32(ctx, peer->outgoingBandwidth); } static JSValue js_enet_peer_get_last_send_time(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); if (!peer) return JS_EXCEPTION; return JS_NewInt32(ctx, peer->lastSendTime); } static JSValue js_enet_peer_get_last_receive_time(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); if (!peer) return JS_EXCEPTION; return JS_NewInt32(ctx, peer->lastReceiveTime); } static JSValue js_enet_peer_get_mtu(JSContext *ctx, JSValueConst this_val) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); if (!peer) return JS_NewFloat64(ctx, INFINITY); return JS_NewInt32(ctx, peer->mtu); } static JSValue js_enet_peer_get_outgoing_data_total(JSContext *ctx, JSValueConst this_val) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); if (!peer) return JS_NewFloat64(ctx, INFINITY); return JS_NewInt32(ctx, peer->outgoingDataTotal); } static JSValue js_enet_peer_get_incoming_data_total(JSContext *ctx, JSValueConst this_val) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); if (!peer) return JS_NewFloat64(ctx, INFINITY); return JS_NewInt32(ctx, peer->incomingDataTotal); } static JSValue js_enet_peer_get_rtt_variance(JSContext *ctx, JSValueConst this_val) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); if (!peer) return JS_NewFloat64(ctx, INFINITY); return JS_NewInt32(ctx, peer->roundTripTimeVariance); } static JSValue js_enet_peer_get_packet_loss(JSContext *ctx, JSValueConst this_val) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); if (!peer) return JS_NewFloat64(ctx, INFINITY); return JS_NewInt32(ctx, peer->packetLoss); } static JSValue js_enet_peer_get_state(JSContext *ctx, JSValueConst this_val) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); if (!peer) return JS_NewInt32(ctx, -1); return JS_NewInt32(ctx, peer->state); } static JSValue js_enet_peer_get_reliable_data_in_transit(JSContext *ctx, JSValueConst this_val) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); if (!peer) return JS_NewFloat64(ctx, INFINITY); return JS_NewInt32(ctx, peer->reliableDataInTransit); } static JSValue js_enet_peer_get_port(JSContext *js, JSValueConst self) { ENetPeer *peer = JS_GetOpaque(self, enet_peer_class_id); return JS_NewUint32(js, peer->address.port); } static JSValue js_enet_peer_get_address(JSContext *js, JSValueConst self) { ENetPeer *peer = JS_GetOpaque(self, enet_peer_class_id); char ip_str[128]; if (enet_address_get_host_ip(&peer->address, ip_str, sizeof(ip_str)) != 0) return JS_NULL; return JS_NewString(js, ip_str); } 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), JS_CGETSET_DEF("rtt", js_enet_peer_get_rtt, NULL), JS_CGETSET_DEF("incoming_bandwidth", js_enet_peer_get_incoming_bandwidth, NULL), JS_CGETSET_DEF("outgoing_bandwidth", js_enet_peer_get_outgoing_bandwidth, NULL), JS_CGETSET_DEF("last_send_time", js_enet_peer_get_last_send_time, NULL), JS_CGETSET_DEF("last_receive_time", js_enet_peer_get_last_receive_time, NULL), JS_CGETSET_DEF("mtu", js_enet_peer_get_mtu, NULL), JS_CGETSET_DEF("outgoing_data_total", js_enet_peer_get_outgoing_data_total, NULL), JS_CGETSET_DEF("incoming_data_total", js_enet_peer_get_incoming_data_total, NULL), JS_CGETSET_DEF("rtt_variance", js_enet_peer_get_rtt_variance, NULL), JS_CGETSET_DEF("packet_loss", js_enet_peer_get_packet_loss, NULL), JS_CGETSET_DEF("state", js_enet_peer_get_state, NULL), JS_CGETSET_DEF("reliable_data_in_transit", js_enet_peer_get_reliable_data_in_transit, NULL), JS_CGETSET_DEF("port", js_enet_peer_get_port, NULL), JS_CGETSET_DEF("address", js_enet_peer_get_address, NULL), }; JSValue js_enet_use(JSContext *ctx) { 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); 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); JSValue export_obj = JS_NewObject(ctx); JS_SetPropertyFunctionList(ctx, export_obj, js_enet_funcs, countof(js_enet_funcs)); return export_obj; }