@mikrojs/native 0.4.0 → 0.5.0-pr-26.g363c07b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CMakeLists.txt +3 -1
- package/include/mikrojs/private.h +3 -0
- package/include/mikrojs/udp.h +7 -0
- package/package.json +3 -2
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/internal.d.ts +24 -0
- package/runtime/test/test.ts +5 -3
- package/runtime/udp/types.ts +54 -0
- package/runtime/udp/udp.ts +57 -0
- package/src/mik_udp.cpp +784 -0
- package/src/mikrojs.cpp +1 -0
package/CMakeLists.txt
CHANGED
|
@@ -49,6 +49,7 @@ set(MIKROJS_CORE_SOURCES
|
|
|
49
49
|
src/mik_sys.cpp
|
|
50
50
|
src/mik_repl.cpp
|
|
51
51
|
src/mik_app_config.cpp
|
|
52
|
+
src/mik_udp.cpp
|
|
52
53
|
)
|
|
53
54
|
|
|
54
55
|
add_library(mikrojs STATIC
|
|
@@ -79,7 +80,7 @@ endif()
|
|
|
79
80
|
include(cmake/mikrojs_bytecode.cmake)
|
|
80
81
|
mikrojs_generate_bytecode(
|
|
81
82
|
RUNTIME_DIR "${CMAKE_CURRENT_SOURCE_DIR}/runtime"
|
|
82
|
-
MODULES cbor env result schema fs http/helpers http/request i2c kv/nvs kv/rtc kv/shared neopixel pin pwm reader sleep spi sntp stdio stream sys test uart wifi
|
|
83
|
+
MODULES cbor env result schema fs http/helpers http/request i2c kv/nvs kv/rtc kv/shared neopixel pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi
|
|
83
84
|
MODULE_PREFIX "mikrojs"
|
|
84
85
|
SYMBOL_PREFIX "mikrojs"
|
|
85
86
|
TARGET gen_bytecode
|
|
@@ -159,6 +160,7 @@ if(BUILD_TESTING)
|
|
|
159
160
|
test/reader_test.cpp
|
|
160
161
|
test/stream_test.cpp
|
|
161
162
|
test/runtime_recycle_test.cpp
|
|
163
|
+
test/udp_test.cpp
|
|
162
164
|
)
|
|
163
165
|
|
|
164
166
|
target_link_libraries(mikrojs_tests PRIVATE mikrojs)
|
|
@@ -212,6 +212,9 @@ JSModuleDef* mik__cbor_init(JSContext* ctx);
|
|
|
212
212
|
/* Result module (mik_result.cpp) */
|
|
213
213
|
JSModuleDef* mik__result_init(JSContext* ctx);
|
|
214
214
|
|
|
215
|
+
/* UDP module (mik_udp.cpp) */
|
|
216
|
+
JSModuleDef* mik__udp_init(JSContext* ctx);
|
|
217
|
+
|
|
215
218
|
bool mik__repl_is_evaluating(void);
|
|
216
219
|
|
|
217
220
|
/* REPL protocol mode (mik_repl.cpp) — used by mik_console.cpp, mik_stdio.cpp */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikrojs/native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-pr-26.g363c07b",
|
|
4
4
|
"description": "Mikro.js C++ runtime library and Node.js native addon",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esp32",
|
|
@@ -69,13 +69,14 @@
|
|
|
69
69
|
"./runtime/sys/types": "./runtime/sys/types.ts",
|
|
70
70
|
"./runtime/test/types": "./runtime/test/types.ts",
|
|
71
71
|
"./runtime/uart/types": "./runtime/uart/types.ts",
|
|
72
|
+
"./runtime/udp/types": "./runtime/udp/types.ts",
|
|
72
73
|
"./runtime/wifi/types": "./runtime/wifi/types.ts"
|
|
73
74
|
},
|
|
74
75
|
"dependencies": {
|
|
75
76
|
"cmake-js": "^8.0.0",
|
|
76
77
|
"node-addon-api": "^8.7.0",
|
|
77
78
|
"node-gyp-build": "^4.8.4",
|
|
78
|
-
"@mikrojs/quickjs": "0.
|
|
79
|
+
"@mikrojs/quickjs": "0.5.0-pr-26.g363c07b"
|
|
79
80
|
},
|
|
80
81
|
"devDependencies": {
|
|
81
82
|
"@swc/core": "^1.15.30",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/runtime/internal.d.ts
CHANGED
|
@@ -454,3 +454,27 @@ declare module 'native:wifi' {
|
|
|
454
454
|
new (): Wifi
|
|
455
455
|
}
|
|
456
456
|
}
|
|
457
|
+
|
|
458
|
+
declare module 'native:udp' {
|
|
459
|
+
import type {
|
|
460
|
+
BindOptions,
|
|
461
|
+
PeerAddress,
|
|
462
|
+
UdpError,
|
|
463
|
+
UdpFamily,
|
|
464
|
+
} from '@mikrojs/native/runtime/udp/types'
|
|
465
|
+
import type {Result} from 'mikrojs/result'
|
|
466
|
+
|
|
467
|
+
export interface NativeUdpSocket {
|
|
468
|
+
readonly port: number
|
|
469
|
+
readonly family: UdpFamily | 'dual'
|
|
470
|
+
dropped: number
|
|
471
|
+
|
|
472
|
+
setOnMessage(fn: ((msg: Uint8Array, from: PeerAddress) => void) | null): void
|
|
473
|
+
send(data: Uint8Array, to: PeerAddress): Promise<Result<void, UdpError>>
|
|
474
|
+
joinMulticastGroup(address: string): Result<void, UdpError>
|
|
475
|
+
leaveMulticastGroup(address: string): Result<void, UdpError>
|
|
476
|
+
close(): void
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function bind(opts: BindOptions): Promise<Result<NativeUdpSocket, UdpError>>
|
|
480
|
+
}
|
package/runtime/test/test.ts
CHANGED
|
@@ -388,9 +388,11 @@ function formatThrown(e: unknown): string {
|
|
|
388
388
|
if (e instanceof Error) return e.message
|
|
389
389
|
if (e && typeof e === 'object') {
|
|
390
390
|
const obj = e as {name?: unknown; message?: unknown}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
391
|
+
const name = typeof obj.name === 'string' ? obj.name : undefined
|
|
392
|
+
const message = typeof obj.message === 'string' ? obj.message : undefined
|
|
393
|
+
if (name && message) return `${name}: ${message}`
|
|
394
|
+
if (message) return message
|
|
395
|
+
if (name) return name
|
|
394
396
|
}
|
|
395
397
|
return String(e)
|
|
396
398
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type {Result} from '../result/types.js'
|
|
2
|
+
|
|
3
|
+
export type UdpFamily = 'ipv4' | 'ipv6'
|
|
4
|
+
|
|
5
|
+
export interface BindOptions {
|
|
6
|
+
port: number
|
|
7
|
+
address?: string
|
|
8
|
+
family?: UdpFamily
|
|
9
|
+
recvQueue?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PeerAddress {
|
|
13
|
+
address: string
|
|
14
|
+
port: number
|
|
15
|
+
family: UdpFamily
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MulticastGroup {
|
|
19
|
+
address: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type UdpError =
|
|
23
|
+
| {name: 'BindFailed'; message: string}
|
|
24
|
+
| {name: 'AddressInUse'}
|
|
25
|
+
| {name: 'SendFailed'; message: string}
|
|
26
|
+
| {name: 'MessageTooLarge'}
|
|
27
|
+
| {name: 'NotReachable'}
|
|
28
|
+
| {name: 'OutOfMemory'}
|
|
29
|
+
| {name: 'JoinGroupFailed'; message: string}
|
|
30
|
+
| {name: 'LeaveGroupFailed'; message: string}
|
|
31
|
+
| {name: 'Closed'}
|
|
32
|
+
| {name: 'NotBound'}
|
|
33
|
+
|
|
34
|
+
export interface UdpSocket {
|
|
35
|
+
readonly port: number
|
|
36
|
+
readonly family: UdpFamily | 'dual'
|
|
37
|
+
/**
|
|
38
|
+
* Counter incremented each time an inbound datagram is dropped:
|
|
39
|
+
* - the per-socket queue is full,
|
|
40
|
+
* - no `onMessage` handler is set when a packet arrives,
|
|
41
|
+
* - or the datagram is larger than the 1500-byte receive buffer
|
|
42
|
+
* (typical Ethernet MTU; covers CoAP / mDNS / SNTP / DNS).
|
|
43
|
+
*/
|
|
44
|
+
dropped: number
|
|
45
|
+
|
|
46
|
+
onMessage: ((msg: Uint8Array, from: PeerAddress) => void) | null
|
|
47
|
+
|
|
48
|
+
send(data: Uint8Array | string, to: PeerAddress): Promise<Result<void, UdpError>>
|
|
49
|
+
joinMulticastGroup(group: MulticastGroup): Result<void, UdpError>
|
|
50
|
+
leaveMulticastGroup(group: MulticastGroup): Result<void, UdpError>
|
|
51
|
+
close(): void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export declare function bind(opts: BindOptions): Promise<Result<UdpSocket, UdpError>>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {ok} from 'mikrojs/result'
|
|
2
|
+
import {bind as nativeBind, type NativeUdpSocket} from 'native:udp'
|
|
3
|
+
|
|
4
|
+
import type {Result} from '../result/types.js'
|
|
5
|
+
import type {BindOptions, MulticastGroup, PeerAddress, UdpError, UdpSocket} from './types.js'
|
|
6
|
+
|
|
7
|
+
const utf8 = new TextEncoder()
|
|
8
|
+
|
|
9
|
+
function toBytes(data: Uint8Array | string): Uint8Array {
|
|
10
|
+
return typeof data === 'string' ? utf8.encode(data) : data
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function makeSocket(handle: NativeUdpSocket): UdpSocket {
|
|
14
|
+
let onMessage: ((msg: Uint8Array, from: PeerAddress) => void) | null = null
|
|
15
|
+
|
|
16
|
+
const sock: UdpSocket = {
|
|
17
|
+
get port() {
|
|
18
|
+
return handle.port
|
|
19
|
+
},
|
|
20
|
+
get family() {
|
|
21
|
+
return handle.family
|
|
22
|
+
},
|
|
23
|
+
get dropped() {
|
|
24
|
+
return handle.dropped
|
|
25
|
+
},
|
|
26
|
+
set dropped(value: number) {
|
|
27
|
+
handle.dropped = value
|
|
28
|
+
},
|
|
29
|
+
get onMessage() {
|
|
30
|
+
return onMessage
|
|
31
|
+
},
|
|
32
|
+
set onMessage(fn) {
|
|
33
|
+
onMessage = fn
|
|
34
|
+
handle.setOnMessage(fn)
|
|
35
|
+
},
|
|
36
|
+
send(data, to) {
|
|
37
|
+
return handle.send(toBytes(data), to)
|
|
38
|
+
},
|
|
39
|
+
joinMulticastGroup(group: MulticastGroup) {
|
|
40
|
+
return handle.joinMulticastGroup(group.address)
|
|
41
|
+
},
|
|
42
|
+
leaveMulticastGroup(group: MulticastGroup) {
|
|
43
|
+
return handle.leaveMulticastGroup(group.address)
|
|
44
|
+
},
|
|
45
|
+
close() {
|
|
46
|
+
handle.close()
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return sock
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function bind(opts: BindOptions): Promise<Result<UdpSocket, UdpError>> {
|
|
54
|
+
const result = await nativeBind(opts)
|
|
55
|
+
if (!result.ok) return result
|
|
56
|
+
return ok(makeSocket(result.value))
|
|
57
|
+
}
|
package/src/mik_udp.cpp
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
#include "mikrojs/udp.h"
|
|
2
|
+
|
|
3
|
+
#include "mikrojs/mikrojs.h"
|
|
4
|
+
#include "mikrojs/platform.h"
|
|
5
|
+
#include "mikrojs/utils.h"
|
|
6
|
+
|
|
7
|
+
#include <arpa/inet.h>
|
|
8
|
+
#include <fcntl.h>
|
|
9
|
+
#include <netinet/in.h>
|
|
10
|
+
#include <sys/socket.h>
|
|
11
|
+
#include <unistd.h>
|
|
12
|
+
|
|
13
|
+
#include <cerrno>
|
|
14
|
+
#include <cstdint>
|
|
15
|
+
#include <cstdio>
|
|
16
|
+
#include <cstring>
|
|
17
|
+
#include <vector>
|
|
18
|
+
|
|
19
|
+
/* ── Per-socket state ────────────────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* One of these per open UDP socket. Lifetime is tied to the JS UdpSocket
|
|
22
|
+
* object; the finalizer (mik__udp_finalizer) closes the OS socket and
|
|
23
|
+
* removes the entry from g_open_sockets.
|
|
24
|
+
*
|
|
25
|
+
* Closed sockets are kept in g_open_sockets with closed=true until the
|
|
26
|
+
* GC actually finalizes the JS object — mik__udp_consume skips them.
|
|
27
|
+
*/
|
|
28
|
+
struct UdpSocketState {
|
|
29
|
+
JSContext* ctx; /* weak ref; do not free */
|
|
30
|
+
int fd;
|
|
31
|
+
int family; /* AF_INET, AF_INET6 (dual via V6ONLY=0) */
|
|
32
|
+
bool dual; /* true => v6 socket with V6ONLY=0 */
|
|
33
|
+
int port; /* actual bound port */
|
|
34
|
+
JSValue on_message; /* duped JSValue, JS_UNDEFINED if not set */
|
|
35
|
+
uint32_t dropped;
|
|
36
|
+
int recv_queue; /* per-tick drain limit */
|
|
37
|
+
bool closed;
|
|
38
|
+
bool dead; /* finalized; pending sweep (see consume) */
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
static JSClassID udp_socket_class_id;
|
|
42
|
+
|
|
43
|
+
/* Global registry of open sockets across all runtimes. The loop consumer
|
|
44
|
+
* filters by ctx, so multi-runtime scenarios are handled even though the
|
|
45
|
+
* vector is process-wide. Single-threaded access is assumed (one tick at
|
|
46
|
+
* a time per runtime; sockets aren't shared across runtimes). */
|
|
47
|
+
static std::vector<UdpSocketState*> g_open_sockets;
|
|
48
|
+
|
|
49
|
+
/* Runtimes that have had the loop consumer registered. Tracking per-
|
|
50
|
+
* runtime rather than process-global lets multiple runtimes coexist
|
|
51
|
+
* and lets new runtimes get a fresh consumer after old ones are freed.
|
|
52
|
+
* A vector with linear scan is sufficient: typical N=1, never more
|
|
53
|
+
* than a handful, and avoids pulling in std::unordered_set's hash-
|
|
54
|
+
* table machinery just for membership checks. */
|
|
55
|
+
static std::vector<MIKRuntime*> g_consumer_registered_runtimes;
|
|
56
|
+
|
|
57
|
+
/* Re-entrance guard for mik__udp_consume. A user onMessage callback can
|
|
58
|
+
* synchronously trigger a UdpSocket finalizer (e.g. by dropping the last
|
|
59
|
+
* reference to a sibling socket). Erasing from g_open_sockets mid-iteration
|
|
60
|
+
* would invalidate indices; deleting `s` for the currently-iterating socket
|
|
61
|
+
* would be a UAF. While iterating, the finalizer marks `s->dead = true` and
|
|
62
|
+
* defers cleanup; consume sweeps dead entries when the depth returns to 0. */
|
|
63
|
+
static int g_iteration_depth = 0;
|
|
64
|
+
|
|
65
|
+
/* ── Forward declarations ────────────────────────────────────────────*/
|
|
66
|
+
static void mik__udp_consume(JSContext* ctx);
|
|
67
|
+
static void mik__udp_destroy(JSContext* ctx);
|
|
68
|
+
static JSValue mik__udp_err(JSContext* ctx, const char* name, const char* fmt, ...)
|
|
69
|
+
__attribute__((format(printf, 3, 4)));
|
|
70
|
+
|
|
71
|
+
/* ── Helpers ────────────────────────────────────────────────────────*/
|
|
72
|
+
|
|
73
|
+
static JSValue mik__udp_err(JSContext* ctx, const char* name, const char* fmt, ...) {
|
|
74
|
+
char buf[160];
|
|
75
|
+
va_list ap;
|
|
76
|
+
va_start(ap, fmt);
|
|
77
|
+
vsnprintf(buf, sizeof(buf), fmt, ap);
|
|
78
|
+
va_end(ap);
|
|
79
|
+
return mik__result_err_named(ctx, name, "%s", buf);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static JSValue mik__udp_err_tag(JSContext* ctx, const char* name) {
|
|
83
|
+
return mik__result_err_tag(ctx, name);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Parse an address string + port into a sockaddr_storage. Returns 0 on
|
|
87
|
+
* success, -1 on parse failure. Family hint: AF_UNSPEC tries v4 first
|
|
88
|
+
* then v6; AF_INET / AF_INET6 are forced. */
|
|
89
|
+
static int mik__parse_addr(const char* address, int port, int family_hint,
|
|
90
|
+
struct sockaddr_storage* out, socklen_t* out_len) {
|
|
91
|
+
memset(out, 0, sizeof(*out));
|
|
92
|
+
|
|
93
|
+
/* IPv4 attempt */
|
|
94
|
+
if (family_hint == AF_UNSPEC || family_hint == AF_INET) {
|
|
95
|
+
struct sockaddr_in* sin = (struct sockaddr_in*)out;
|
|
96
|
+
if (inet_pton(AF_INET, address, &sin->sin_addr) == 1) {
|
|
97
|
+
sin->sin_family = AF_INET;
|
|
98
|
+
sin->sin_port = htons((uint16_t)port);
|
|
99
|
+
*out_len = sizeof(*sin);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
if (family_hint == AF_INET) return -1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* IPv6 attempt — strip optional %scope suffix */
|
|
106
|
+
char tmp[80];
|
|
107
|
+
const char* pct = strchr(address, '%');
|
|
108
|
+
const char* a = address;
|
|
109
|
+
uint32_t scope_id = 0;
|
|
110
|
+
if (pct) {
|
|
111
|
+
size_t n = (size_t)(pct - address);
|
|
112
|
+
if (n >= sizeof(tmp)) return -1;
|
|
113
|
+
memcpy(tmp, address, n);
|
|
114
|
+
tmp[n] = '\0';
|
|
115
|
+
a = tmp;
|
|
116
|
+
scope_id = (uint32_t)strtoul(pct + 1, nullptr, 10);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)out;
|
|
120
|
+
if (inet_pton(AF_INET6, a, &sin6->sin6_addr) == 1) {
|
|
121
|
+
sin6->sin6_family = AF_INET6;
|
|
122
|
+
sin6->sin6_port = htons((uint16_t)port);
|
|
123
|
+
sin6->sin6_scope_id = scope_id;
|
|
124
|
+
*out_len = sizeof(*sin6);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return -1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Format sockaddr_storage into address string + port. v4-mapped v6
|
|
132
|
+
* addresses are normalized back to plain v4 form. Returns the family
|
|
133
|
+
* tag ('ipv4' / 'ipv6') for the JS object. */
|
|
134
|
+
static const char* mik__format_addr(const struct sockaddr_storage* sa, char* buf, size_t buflen,
|
|
135
|
+
int* out_port) {
|
|
136
|
+
if (sa->ss_family == AF_INET) {
|
|
137
|
+
const struct sockaddr_in* sin = (const struct sockaddr_in*)sa;
|
|
138
|
+
inet_ntop(AF_INET, &sin->sin_addr, buf, buflen);
|
|
139
|
+
*out_port = ntohs(sin->sin_port);
|
|
140
|
+
return "ipv4";
|
|
141
|
+
}
|
|
142
|
+
const struct sockaddr_in6* sin6 = (const struct sockaddr_in6*)sa;
|
|
143
|
+
|
|
144
|
+
/* v4-mapped: ::ffff:x.x.x.x → x.x.x.x */
|
|
145
|
+
if (IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr)) {
|
|
146
|
+
struct in_addr v4;
|
|
147
|
+
memcpy(&v4, ((const uint8_t*)&sin6->sin6_addr) + 12, sizeof(v4));
|
|
148
|
+
inet_ntop(AF_INET, &v4, buf, buflen);
|
|
149
|
+
*out_port = ntohs(sin6->sin6_port);
|
|
150
|
+
return "ipv4";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
inet_ntop(AF_INET6, &sin6->sin6_addr, buf, buflen);
|
|
154
|
+
/* Append %scope if present */
|
|
155
|
+
if (sin6->sin6_scope_id) {
|
|
156
|
+
size_t cur = strlen(buf);
|
|
157
|
+
snprintf(buf + cur, buflen - cur, "%%%u", sin6->sin6_scope_id);
|
|
158
|
+
}
|
|
159
|
+
*out_port = ntohs(sin6->sin6_port);
|
|
160
|
+
return "ipv6";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Build a PeerAddress JS object: {address, port, family} */
|
|
164
|
+
static JSValue mik__make_peer(JSContext* ctx, const char* address, int port, const char* family) {
|
|
165
|
+
JSValue obj = JS_NewObject(ctx);
|
|
166
|
+
JS_SetPropertyStr(ctx, obj, "address", JS_NewString(ctx, address));
|
|
167
|
+
JS_SetPropertyStr(ctx, obj, "port", JS_NewInt32(ctx, port));
|
|
168
|
+
JS_SetPropertyStr(ctx, obj, "family", JS_NewString(ctx, family));
|
|
169
|
+
return obj;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Read {address, port} (and optional family) from a JS PeerAddress. */
|
|
173
|
+
static int mik__read_peer(JSContext* ctx, JSValue obj, char* addr_out, size_t addr_size,
|
|
174
|
+
int* port_out) {
|
|
175
|
+
JSValue addr_val = JS_GetPropertyStr(ctx, obj, "address");
|
|
176
|
+
JSValue port_val = JS_GetPropertyStr(ctx, obj, "port");
|
|
177
|
+
int rc = -1;
|
|
178
|
+
|
|
179
|
+
if (JS_IsString(addr_val) && JS_IsNumber(port_val)) {
|
|
180
|
+
const char* s = JS_ToCString(ctx, addr_val);
|
|
181
|
+
if (s) {
|
|
182
|
+
size_t len = strlen(s);
|
|
183
|
+
if (len < addr_size) {
|
|
184
|
+
memcpy(addr_out, s, len + 1);
|
|
185
|
+
int32_t p;
|
|
186
|
+
if (JS_ToInt32(ctx, &p, port_val) == 0) {
|
|
187
|
+
*port_out = (int)p;
|
|
188
|
+
rc = 0;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
JS_FreeCString(ctx, s);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
JS_FreeValue(ctx, addr_val);
|
|
196
|
+
JS_FreeValue(ctx, port_val);
|
|
197
|
+
return rc;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static UdpSocketState* mik__socket_get(JSContext* ctx, JSValue obj) {
|
|
201
|
+
return (UdpSocketState*)JS_GetOpaque2(ctx, obj, udp_socket_class_id);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* ── Methods ─────────────────────────────────────────────────────────*/
|
|
205
|
+
|
|
206
|
+
static JSValue mik__udp_send(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
207
|
+
UdpSocketState* s = mik__socket_get(ctx, this_val);
|
|
208
|
+
if (!s) {
|
|
209
|
+
JSValue err = mik__udp_err_tag(ctx, "Closed");
|
|
210
|
+
return MIK_NewResolvedPromise(ctx, 1, &err);
|
|
211
|
+
}
|
|
212
|
+
if (s->closed) {
|
|
213
|
+
JSValue err = mik__udp_err_tag(ctx, "Closed");
|
|
214
|
+
return MIK_NewResolvedPromise(ctx, 1, &err);
|
|
215
|
+
}
|
|
216
|
+
if (argc < 2) {
|
|
217
|
+
return JS_ThrowTypeError(ctx, "send: expected (data, to)");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Get bytes */
|
|
221
|
+
size_t data_len;
|
|
222
|
+
uint8_t* data = JS_GetUint8Array(ctx, &data_len, argv[0]);
|
|
223
|
+
if (!data) {
|
|
224
|
+
return JS_ThrowTypeError(ctx, "send: expected data to be Uint8Array");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* Get peer address */
|
|
228
|
+
char addr[80];
|
|
229
|
+
int port;
|
|
230
|
+
if (mik__read_peer(ctx, argv[1], addr, sizeof(addr), &port) != 0) {
|
|
231
|
+
return JS_ThrowTypeError(ctx, "send: invalid peer address");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
struct sockaddr_storage dst;
|
|
235
|
+
socklen_t dst_len = 0;
|
|
236
|
+
int hint = AF_UNSPEC;
|
|
237
|
+
if (s->family == AF_INET) hint = AF_INET;
|
|
238
|
+
else if (s->family == AF_INET6 && !s->dual) hint = AF_INET6;
|
|
239
|
+
|
|
240
|
+
if (mik__parse_addr(addr, port, hint, &dst, &dst_len) != 0) {
|
|
241
|
+
JSValue err = mik__udp_err(ctx, "SendFailed", "invalid address: %s", addr);
|
|
242
|
+
return MIK_NewResolvedPromise(ctx, 1, &err);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* For dual-stack v6 socket sending to an IPv4 peer, convert to v4-mapped. */
|
|
246
|
+
if (s->dual && dst.ss_family == AF_INET) {
|
|
247
|
+
struct sockaddr_in6 mapped;
|
|
248
|
+
memset(&mapped, 0, sizeof(mapped));
|
|
249
|
+
mapped.sin6_family = AF_INET6;
|
|
250
|
+
mapped.sin6_port = ((struct sockaddr_in*)&dst)->sin_port;
|
|
251
|
+
/* ::ffff:0:0 + the v4 address */
|
|
252
|
+
uint8_t* p = (uint8_t*)&mapped.sin6_addr;
|
|
253
|
+
p[10] = 0xff;
|
|
254
|
+
p[11] = 0xff;
|
|
255
|
+
memcpy(p + 12, &((struct sockaddr_in*)&dst)->sin_addr, 4);
|
|
256
|
+
memcpy(&dst, &mapped, sizeof(mapped));
|
|
257
|
+
dst_len = sizeof(mapped);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
ssize_t n = sendto(s->fd, data, data_len, 0, (struct sockaddr*)&dst, dst_len);
|
|
261
|
+
if (n < 0) {
|
|
262
|
+
const char* name = "SendFailed";
|
|
263
|
+
if (errno == EMSGSIZE) name = "MessageTooLarge";
|
|
264
|
+
else if (errno == ENETUNREACH || errno == EHOSTUNREACH) name = "NotReachable";
|
|
265
|
+
else if (errno == ENOMEM || errno == ENOBUFS) name = "OutOfMemory";
|
|
266
|
+
JSValue err = mik__udp_err(ctx, name, "sendto: %s", strerror(errno));
|
|
267
|
+
return MIK_NewResolvedPromise(ctx, 1, &err);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
JSValue ok = mik__result_ok_void(ctx);
|
|
271
|
+
return MIK_NewResolvedPromise(ctx, 1, &ok);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
static JSValue mik__udp_join_group(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
275
|
+
UdpSocketState* s = mik__socket_get(ctx, this_val);
|
|
276
|
+
if (!s || s->closed) return mik__udp_err_tag(ctx, "Closed");
|
|
277
|
+
if (argc < 1 || !JS_IsString(argv[0])) {
|
|
278
|
+
return JS_ThrowTypeError(ctx, "joinMulticastGroup: expected address string");
|
|
279
|
+
}
|
|
280
|
+
const char* addr = JS_ToCString(ctx, argv[0]);
|
|
281
|
+
if (!addr) return JS_EXCEPTION;
|
|
282
|
+
|
|
283
|
+
int rc;
|
|
284
|
+
if (s->family == AF_INET) {
|
|
285
|
+
struct ip_mreq req;
|
|
286
|
+
memset(&req, 0, sizeof(req));
|
|
287
|
+
if (inet_pton(AF_INET, addr, &req.imr_multiaddr) != 1) {
|
|
288
|
+
JS_FreeCString(ctx, addr);
|
|
289
|
+
return mik__udp_err(ctx, "JoinGroupFailed", "invalid IPv4 group");
|
|
290
|
+
}
|
|
291
|
+
req.imr_interface.s_addr = htonl(INADDR_ANY);
|
|
292
|
+
rc = setsockopt(s->fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &req, sizeof(req));
|
|
293
|
+
} else {
|
|
294
|
+
struct ipv6_mreq req;
|
|
295
|
+
memset(&req, 0, sizeof(req));
|
|
296
|
+
if (inet_pton(AF_INET6, addr, &req.ipv6mr_multiaddr) != 1) {
|
|
297
|
+
JS_FreeCString(ctx, addr);
|
|
298
|
+
return mik__udp_err(ctx, "JoinGroupFailed", "invalid IPv6 group");
|
|
299
|
+
}
|
|
300
|
+
req.ipv6mr_interface = 0;
|
|
301
|
+
rc = setsockopt(s->fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &req, sizeof(req));
|
|
302
|
+
}
|
|
303
|
+
JS_FreeCString(ctx, addr);
|
|
304
|
+
|
|
305
|
+
if (rc < 0) {
|
|
306
|
+
return mik__udp_err(ctx, "JoinGroupFailed", "%s", strerror(errno));
|
|
307
|
+
}
|
|
308
|
+
return mik__result_ok_void(ctx);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
static JSValue mik__udp_leave_group(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
312
|
+
UdpSocketState* s = mik__socket_get(ctx, this_val);
|
|
313
|
+
if (!s || s->closed) return mik__udp_err_tag(ctx, "Closed");
|
|
314
|
+
if (argc < 1 || !JS_IsString(argv[0])) {
|
|
315
|
+
return JS_ThrowTypeError(ctx, "leaveMulticastGroup: expected address string");
|
|
316
|
+
}
|
|
317
|
+
const char* addr = JS_ToCString(ctx, argv[0]);
|
|
318
|
+
if (!addr) return JS_EXCEPTION;
|
|
319
|
+
|
|
320
|
+
int rc;
|
|
321
|
+
if (s->family == AF_INET) {
|
|
322
|
+
struct ip_mreq req;
|
|
323
|
+
memset(&req, 0, sizeof(req));
|
|
324
|
+
if (inet_pton(AF_INET, addr, &req.imr_multiaddr) != 1) {
|
|
325
|
+
JS_FreeCString(ctx, addr);
|
|
326
|
+
return mik__udp_err(ctx, "LeaveGroupFailed", "invalid IPv4 group");
|
|
327
|
+
}
|
|
328
|
+
req.imr_interface.s_addr = htonl(INADDR_ANY);
|
|
329
|
+
rc = setsockopt(s->fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &req, sizeof(req));
|
|
330
|
+
} else {
|
|
331
|
+
struct ipv6_mreq req;
|
|
332
|
+
memset(&req, 0, sizeof(req));
|
|
333
|
+
if (inet_pton(AF_INET6, addr, &req.ipv6mr_multiaddr) != 1) {
|
|
334
|
+
JS_FreeCString(ctx, addr);
|
|
335
|
+
return mik__udp_err(ctx, "LeaveGroupFailed", "invalid IPv6 group");
|
|
336
|
+
}
|
|
337
|
+
req.ipv6mr_interface = 0;
|
|
338
|
+
rc = setsockopt(s->fd, IPPROTO_IPV6, IPV6_LEAVE_GROUP, &req, sizeof(req));
|
|
339
|
+
}
|
|
340
|
+
JS_FreeCString(ctx, addr);
|
|
341
|
+
|
|
342
|
+
if (rc < 0) {
|
|
343
|
+
return mik__udp_err(ctx, "LeaveGroupFailed", "%s", strerror(errno));
|
|
344
|
+
}
|
|
345
|
+
return mik__result_ok_void(ctx);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
static JSValue mik__udp_close(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
349
|
+
UdpSocketState* s = mik__socket_get(ctx, this_val);
|
|
350
|
+
if (!s || s->closed) return JS_UNDEFINED;
|
|
351
|
+
s->closed = true;
|
|
352
|
+
if (s->fd >= 0) {
|
|
353
|
+
close(s->fd);
|
|
354
|
+
s->fd = -1;
|
|
355
|
+
}
|
|
356
|
+
return JS_UNDEFINED;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
static JSValue mik__udp_set_on_message(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
360
|
+
UdpSocketState* s = mik__socket_get(ctx, this_val);
|
|
361
|
+
if (!s) return JS_UNDEFINED;
|
|
362
|
+
JS_FreeValue(ctx, s->on_message);
|
|
363
|
+
s->on_message = (argc > 0 && JS_IsFunction(ctx, argv[0])) ? JS_DupValue(ctx, argv[0])
|
|
364
|
+
: JS_UNDEFINED;
|
|
365
|
+
return JS_UNDEFINED;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* port / family are written as values at bind time (see install_methods),
|
|
369
|
+
* not as live getters — they don't change after bind. dropped IS a live
|
|
370
|
+
* getter/setter because the loop consumer increments it. */
|
|
371
|
+
|
|
372
|
+
static JSValue mik__udp_get_dropped(JSContext* ctx, JSValue this_val) {
|
|
373
|
+
UdpSocketState* s = mik__socket_get(ctx, this_val);
|
|
374
|
+
if (!s) return JS_NewInt32(ctx, 0);
|
|
375
|
+
return JS_NewUint32(ctx, s->dropped);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
static JSValue mik__udp_set_dropped(JSContext* ctx, JSValue this_val, JSValue val) {
|
|
379
|
+
UdpSocketState* s = mik__socket_get(ctx, this_val);
|
|
380
|
+
if (!s) return JS_UNDEFINED;
|
|
381
|
+
uint32_t v;
|
|
382
|
+
if (JS_ToUint32(ctx, &v, val) == 0) s->dropped = v;
|
|
383
|
+
return JS_UNDEFINED;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* ── Class definition ────────────────────────────────────────────────*/
|
|
387
|
+
|
|
388
|
+
static void mik__udp_finalizer(JSRuntime* rt, JSValue val) {
|
|
389
|
+
UdpSocketState* s = (UdpSocketState*)JS_GetOpaque(val, udp_socket_class_id);
|
|
390
|
+
if (!s) return;
|
|
391
|
+
|
|
392
|
+
if (!s->closed && s->fd >= 0) {
|
|
393
|
+
close(s->fd);
|
|
394
|
+
s->fd = -1;
|
|
395
|
+
}
|
|
396
|
+
JS_FreeValueRT(rt, s->on_message);
|
|
397
|
+
s->on_message = JS_UNDEFINED;
|
|
398
|
+
s->closed = true;
|
|
399
|
+
|
|
400
|
+
if (g_iteration_depth > 0) {
|
|
401
|
+
/* Inside consume: defer erase + delete to the post-iteration sweep
|
|
402
|
+
* so we don't shift indices or free `s` while it's being walked. */
|
|
403
|
+
s->dead = true;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
for (auto it = g_open_sockets.begin(); it != g_open_sockets.end(); ++it) {
|
|
408
|
+
if (*it == s) {
|
|
409
|
+
g_open_sockets.erase(it);
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
delete s;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* Cycle-collector hook: expose the held onMessage closure so QuickJS can
|
|
417
|
+
* traverse through the opaque socket. Without this, a closure that captures
|
|
418
|
+
* the socket (e.g. via globalThis) creates a cycle the GC can't break. */
|
|
419
|
+
static void mik__udp_gc_mark(JSRuntime* rt, JSValue val, JS_MarkFunc* mark_func) {
|
|
420
|
+
UdpSocketState* s = (UdpSocketState*)JS_GetOpaque(val, udp_socket_class_id);
|
|
421
|
+
if (!s) return;
|
|
422
|
+
JS_MarkValue(rt, s->on_message, mark_func);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
static JSClassDef mik__udp_socket_class = {
|
|
426
|
+
.class_name = "UdpSocket",
|
|
427
|
+
.finalizer = mik__udp_finalizer,
|
|
428
|
+
.gc_mark = mik__udp_gc_mark,
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
/* Methods are installed directly on each socket object (not via proto) —
|
|
432
|
+
* ESP-IDF's 32-bit QuickJS NaN-boxed build did not reliably resolve
|
|
433
|
+
* proto-installed accessors on JS_NewObjectClass instances, even though
|
|
434
|
+
* JS_GetOpaque2 and the class registration both worked. Putting methods
|
|
435
|
+
* directly on the object sidesteps the class-proto resolution path. */
|
|
436
|
+
static void mik__udp_install_methods(JSContext* ctx, JSValue obj, UdpSocketState* s) {
|
|
437
|
+
JS_DefinePropertyValueStr(ctx, obj, "send",
|
|
438
|
+
JS_NewCFunction(ctx, mik__udp_send, "send", 2),
|
|
439
|
+
JS_PROP_C_W_E);
|
|
440
|
+
JS_DefinePropertyValueStr(ctx, obj, "joinMulticastGroup",
|
|
441
|
+
JS_NewCFunction(ctx, mik__udp_join_group, "joinMulticastGroup", 1),
|
|
442
|
+
JS_PROP_C_W_E);
|
|
443
|
+
JS_DefinePropertyValueStr(ctx, obj, "leaveMulticastGroup",
|
|
444
|
+
JS_NewCFunction(ctx, mik__udp_leave_group, "leaveMulticastGroup", 1),
|
|
445
|
+
JS_PROP_C_W_E);
|
|
446
|
+
JS_DefinePropertyValueStr(ctx, obj, "close",
|
|
447
|
+
JS_NewCFunction(ctx, mik__udp_close, "close", 0),
|
|
448
|
+
JS_PROP_C_W_E);
|
|
449
|
+
JS_DefinePropertyValueStr(ctx, obj, "setOnMessage",
|
|
450
|
+
JS_NewCFunction(ctx, mik__udp_set_on_message, "setOnMessage", 1),
|
|
451
|
+
JS_PROP_C_W_E);
|
|
452
|
+
/* port / family are stable for the socket's lifetime — set as values */
|
|
453
|
+
JS_DefinePropertyValueStr(ctx, obj, "port", JS_NewInt32(ctx, s->port), JS_PROP_C_W_E);
|
|
454
|
+
if (s->dual) {
|
|
455
|
+
JS_DefinePropertyValueStr(ctx, obj, "family", JS_NewString(ctx, "dual"), JS_PROP_C_W_E);
|
|
456
|
+
} else {
|
|
457
|
+
JS_DefinePropertyValueStr(
|
|
458
|
+
ctx, obj, "family",
|
|
459
|
+
JS_NewString(ctx, s->family == AF_INET ? "ipv4" : "ipv6"), JS_PROP_C_W_E);
|
|
460
|
+
}
|
|
461
|
+
/* dropped is a live getter/setter onto s->dropped; install via the
|
|
462
|
+
* function-list helper so the type-erased function casts go through
|
|
463
|
+
* the same path the rest of the codebase uses. */
|
|
464
|
+
static const JSCFunctionListEntry dropped_entry[] = {
|
|
465
|
+
MIK_CGETSET_DEF("dropped", mik__udp_get_dropped, mik__udp_set_dropped),
|
|
466
|
+
};
|
|
467
|
+
JS_SetPropertyFunctionList(ctx, obj, dropped_entry, 1);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/* ── bind() factory ──────────────────────────────────────────────────*/
|
|
471
|
+
|
|
472
|
+
static JSValue mik__udp_bind(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
473
|
+
if (argc < 1 || !JS_IsObject(argv[0])) {
|
|
474
|
+
return JS_ThrowTypeError(ctx, "bind: expected options object");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
JSValue opts = argv[0];
|
|
478
|
+
|
|
479
|
+
/* port: number (required) */
|
|
480
|
+
JSValue port_val = JS_GetPropertyStr(ctx, opts, "port");
|
|
481
|
+
int32_t port = 0;
|
|
482
|
+
if (JS_ToInt32(ctx, &port, port_val)) {
|
|
483
|
+
JS_FreeValue(ctx, port_val);
|
|
484
|
+
return JS_ThrowTypeError(ctx, "bind: port must be a number");
|
|
485
|
+
}
|
|
486
|
+
JS_FreeValue(ctx, port_val);
|
|
487
|
+
|
|
488
|
+
/* family: 'ipv4' | 'ipv6' (optional, default dual) */
|
|
489
|
+
JSValue family_val = JS_GetPropertyStr(ctx, opts, "family");
|
|
490
|
+
int family = AF_INET6;
|
|
491
|
+
bool dual = true;
|
|
492
|
+
if (JS_IsString(family_val)) {
|
|
493
|
+
const char* fs = JS_ToCString(ctx, family_val);
|
|
494
|
+
if (fs && strcmp(fs, "ipv4") == 0) {
|
|
495
|
+
family = AF_INET;
|
|
496
|
+
dual = false;
|
|
497
|
+
} else if (fs && strcmp(fs, "ipv6") == 0) {
|
|
498
|
+
family = AF_INET6;
|
|
499
|
+
dual = false;
|
|
500
|
+
}
|
|
501
|
+
if (fs) JS_FreeCString(ctx, fs);
|
|
502
|
+
}
|
|
503
|
+
JS_FreeValue(ctx, family_val);
|
|
504
|
+
|
|
505
|
+
/* address: string (optional) */
|
|
506
|
+
JSValue address_val = JS_GetPropertyStr(ctx, opts, "address");
|
|
507
|
+
const char* address = nullptr;
|
|
508
|
+
if (JS_IsString(address_val)) {
|
|
509
|
+
address = JS_ToCString(ctx, address_val);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/* recvQueue: number (optional, default 8) */
|
|
513
|
+
JSValue rq_val = JS_GetPropertyStr(ctx, opts, "recvQueue");
|
|
514
|
+
int32_t recv_queue = 8;
|
|
515
|
+
if (JS_IsNumber(rq_val)) {
|
|
516
|
+
JS_ToInt32(ctx, &recv_queue, rq_val);
|
|
517
|
+
}
|
|
518
|
+
JS_FreeValue(ctx, rq_val);
|
|
519
|
+
if (recv_queue < 1) recv_queue = 1;
|
|
520
|
+
|
|
521
|
+
/* Create socket */
|
|
522
|
+
int fd = socket(family, SOCK_DGRAM, 0);
|
|
523
|
+
if (fd < 0) {
|
|
524
|
+
if (address) JS_FreeCString(ctx, address);
|
|
525
|
+
JS_FreeValue(ctx, address_val);
|
|
526
|
+
JSValue err = mik__udp_err(ctx, "BindFailed", "socket: %s", strerror(errno));
|
|
527
|
+
return MIK_NewResolvedPromise(ctx, 1, &err);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/* Set non-blocking */
|
|
531
|
+
int flags = fcntl(fd, F_GETFL, 0);
|
|
532
|
+
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
|
533
|
+
|
|
534
|
+
/* Configure dual-stack on v6 */
|
|
535
|
+
if (family == AF_INET6) {
|
|
536
|
+
int v6only = dual ? 0 : 1;
|
|
537
|
+
setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/* Bind. Route through mik__parse_addr so the same scope-id handling
|
|
541
|
+
* (`fe80::1%2`) used on send applies on bind too. */
|
|
542
|
+
struct sockaddr_storage local;
|
|
543
|
+
socklen_t local_len;
|
|
544
|
+
memset(&local, 0, sizeof(local));
|
|
545
|
+
if (family == AF_INET) {
|
|
546
|
+
struct sockaddr_in* sin = (struct sockaddr_in*)&local;
|
|
547
|
+
sin->sin_family = AF_INET;
|
|
548
|
+
sin->sin_port = htons((uint16_t)port);
|
|
549
|
+
if (address) {
|
|
550
|
+
if (mik__parse_addr(address, port, AF_INET, &local, &local_len) != 0) {
|
|
551
|
+
close(fd);
|
|
552
|
+
JS_FreeCString(ctx, address);
|
|
553
|
+
JS_FreeValue(ctx, address_val);
|
|
554
|
+
JSValue err = mik__udp_err(ctx, "BindFailed", "invalid address");
|
|
555
|
+
return MIK_NewResolvedPromise(ctx, 1, &err);
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
sin->sin_addr.s_addr = htonl(INADDR_ANY);
|
|
559
|
+
local_len = sizeof(*sin);
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&local;
|
|
563
|
+
sin6->sin6_family = AF_INET6;
|
|
564
|
+
sin6->sin6_port = htons((uint16_t)port);
|
|
565
|
+
if (address) {
|
|
566
|
+
if (mik__parse_addr(address, port, AF_INET6, &local, &local_len) != 0) {
|
|
567
|
+
close(fd);
|
|
568
|
+
JS_FreeCString(ctx, address);
|
|
569
|
+
JS_FreeValue(ctx, address_val);
|
|
570
|
+
JSValue err = mik__udp_err(ctx, "BindFailed", "invalid address");
|
|
571
|
+
return MIK_NewResolvedPromise(ctx, 1, &err);
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
/* in6addr_any */
|
|
575
|
+
local_len = sizeof(*sin6);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (address) JS_FreeCString(ctx, address);
|
|
580
|
+
JS_FreeValue(ctx, address_val);
|
|
581
|
+
|
|
582
|
+
if (bind(fd, (struct sockaddr*)&local, local_len) < 0) {
|
|
583
|
+
const char* err_name = (errno == EADDRINUSE) ? "AddressInUse" : "BindFailed";
|
|
584
|
+
int saved_errno = errno;
|
|
585
|
+
close(fd);
|
|
586
|
+
JSValue err = mik__udp_err(ctx, err_name, "bind: %s", strerror(saved_errno));
|
|
587
|
+
return MIK_NewResolvedPromise(ctx, 1, &err);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/* Read back actual port (handles port 0 / ephemeral) */
|
|
591
|
+
struct sockaddr_storage actual;
|
|
592
|
+
socklen_t actual_len = sizeof(actual);
|
|
593
|
+
int actual_port = port;
|
|
594
|
+
if (getsockname(fd, (struct sockaddr*)&actual, &actual_len) == 0) {
|
|
595
|
+
if (actual.ss_family == AF_INET) {
|
|
596
|
+
actual_port = ntohs(((struct sockaddr_in*)&actual)->sin_port);
|
|
597
|
+
} else if (actual.ss_family == AF_INET6) {
|
|
598
|
+
actual_port = ntohs(((struct sockaddr_in6*)&actual)->sin6_port);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/* Build state + JS object */
|
|
603
|
+
UdpSocketState* s = new UdpSocketState();
|
|
604
|
+
s->ctx = ctx;
|
|
605
|
+
s->fd = fd;
|
|
606
|
+
s->family = family;
|
|
607
|
+
s->dual = dual;
|
|
608
|
+
s->port = actual_port;
|
|
609
|
+
s->on_message = JS_UNDEFINED;
|
|
610
|
+
s->dropped = 0;
|
|
611
|
+
s->recv_queue = recv_queue;
|
|
612
|
+
s->closed = false;
|
|
613
|
+
s->dead = false;
|
|
614
|
+
|
|
615
|
+
JSValue obj = JS_NewObjectClass(ctx, udp_socket_class_id);
|
|
616
|
+
if (JS_IsException(obj)) {
|
|
617
|
+
close(fd);
|
|
618
|
+
delete s;
|
|
619
|
+
return obj;
|
|
620
|
+
}
|
|
621
|
+
JS_SetOpaque(obj, s);
|
|
622
|
+
|
|
623
|
+
/* Install methods and properties directly on the object */
|
|
624
|
+
mik__udp_install_methods(ctx, obj, s);
|
|
625
|
+
|
|
626
|
+
/* Register loop consumer once per runtime; first bind triggers it. */
|
|
627
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
628
|
+
if (mik_rt) {
|
|
629
|
+
bool already = false;
|
|
630
|
+
for (auto* rt : g_consumer_registered_runtimes) {
|
|
631
|
+
if (rt == mik_rt) {
|
|
632
|
+
already = true;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (!already) {
|
|
637
|
+
g_consumer_registered_runtimes.push_back(mik_rt);
|
|
638
|
+
MIK_RegisterLoopConsumer(mik_rt, mik__udp_consume, mik__udp_destroy);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
g_open_sockets.push_back(s);
|
|
643
|
+
|
|
644
|
+
JSValue ok = mik__result_ok(ctx, obj);
|
|
645
|
+
return MIK_NewResolvedPromise(ctx, 1, &ok);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/* ── Loop consumer ───────────────────────────────────────────────────*/
|
|
649
|
+
|
|
650
|
+
static void mik__udp_consume(JSContext* ctx) {
|
|
651
|
+
g_iteration_depth++;
|
|
652
|
+
|
|
653
|
+
/* Walk by current size each iteration: callbacks may bind new sockets
|
|
654
|
+
* (push_back at the end — drained next tick) and finalize others
|
|
655
|
+
* (marked dead, swept after the loop). Indexed access stays valid
|
|
656
|
+
* because nothing is erased while g_iteration_depth > 0. */
|
|
657
|
+
for (size_t i = 0; i < g_open_sockets.size(); i++) {
|
|
658
|
+
UdpSocketState* s = g_open_sockets[i];
|
|
659
|
+
if (!s || s->dead) continue;
|
|
660
|
+
if (s->ctx != ctx) continue;
|
|
661
|
+
if (s->closed) continue;
|
|
662
|
+
|
|
663
|
+
for (int drained = 0; drained < s->recv_queue; drained++) {
|
|
664
|
+
/* 1500 bytes covers the unfragmented Ethernet MTU, which is
|
|
665
|
+
* the realistic ceiling for the listed targets (CoAP, mDNS,
|
|
666
|
+
* SNTP, DNS). MSG_TRUNC reports the original length so larger
|
|
667
|
+
* datagrams are observable as drops rather than silent
|
|
668
|
+
* truncation. */
|
|
669
|
+
uint8_t buf[1500];
|
|
670
|
+
struct sockaddr_storage src;
|
|
671
|
+
socklen_t src_len = sizeof(src);
|
|
672
|
+
ssize_t r = recvfrom(s->fd, buf, sizeof(buf), MSG_TRUNC,
|
|
673
|
+
(struct sockaddr*)&src, &src_len);
|
|
674
|
+
if (r < 0) {
|
|
675
|
+
/* EAGAIN/EWOULDBLOCK — buffer empty, move on */
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
if ((size_t)r > sizeof(buf)) {
|
|
679
|
+
/* Datagram larger than our recv buffer; drop it rather
|
|
680
|
+
* than deliver a truncated payload. */
|
|
681
|
+
s->dropped++;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (!JS_IsFunction(ctx, s->on_message)) {
|
|
686
|
+
s->dropped++;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
char addr_str[80];
|
|
691
|
+
int port;
|
|
692
|
+
const char* family = mik__format_addr(&src, addr_str, sizeof(addr_str), &port);
|
|
693
|
+
|
|
694
|
+
/* Allocate fresh Uint8Array per packet (no buffer reuse) */
|
|
695
|
+
uint8_t* data = (uint8_t*)js_malloc(ctx, (size_t)r);
|
|
696
|
+
if (!data) {
|
|
697
|
+
s->dropped++;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
memcpy(data, buf, (size_t)r);
|
|
701
|
+
|
|
702
|
+
JSValue msg = MIK_NewUint8Array(ctx, data, (size_t)r);
|
|
703
|
+
JSValue peer = mik__make_peer(ctx, addr_str, port, family);
|
|
704
|
+
|
|
705
|
+
JSValue args[2] = {msg, peer};
|
|
706
|
+
JSValue ret = JS_Call(ctx, s->on_message, JS_UNDEFINED, 2, args);
|
|
707
|
+
JS_FreeValue(ctx, msg);
|
|
708
|
+
JS_FreeValue(ctx, peer);
|
|
709
|
+
if (JS_IsException(ret)) {
|
|
710
|
+
/* Clear the pending exception so it doesn't leak into the
|
|
711
|
+
* next callback / native call, and surface it via the
|
|
712
|
+
* runtime's error path. One bad callback shouldn't tear
|
|
713
|
+
* the loop down. */
|
|
714
|
+
mik_dump_error(ctx);
|
|
715
|
+
}
|
|
716
|
+
JS_FreeValue(ctx, ret);
|
|
717
|
+
|
|
718
|
+
/* The callback may have closed or finalized this socket. */
|
|
719
|
+
if (s->dead || s->closed || s->fd < 0) break;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
g_iteration_depth--;
|
|
724
|
+
|
|
725
|
+
/* Sweep entries marked dead during this (outermost) iteration. */
|
|
726
|
+
if (g_iteration_depth == 0) {
|
|
727
|
+
for (auto it = g_open_sockets.begin(); it != g_open_sockets.end();) {
|
|
728
|
+
if ((*it)->dead) {
|
|
729
|
+
delete *it;
|
|
730
|
+
it = g_open_sockets.erase(it);
|
|
731
|
+
} else {
|
|
732
|
+
++it;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
static void mik__udp_destroy(JSContext* ctx) {
|
|
739
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
740
|
+
if (mik_rt) {
|
|
741
|
+
for (auto it = g_consumer_registered_runtimes.begin();
|
|
742
|
+
it != g_consumer_registered_runtimes.end(); ++it) {
|
|
743
|
+
if (*it == mik_rt) {
|
|
744
|
+
g_consumer_registered_runtimes.erase(it);
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/* Close OS fds for sockets belonging to this runtime. The JS objects
|
|
751
|
+
* are about to be freed by GC, which calls mik__udp_finalizer for each;
|
|
752
|
+
* the finalizer also closes if not already closed, so this is just to
|
|
753
|
+
* release fds promptly during shutdown. */
|
|
754
|
+
for (auto* s : g_open_sockets) {
|
|
755
|
+
if (s->ctx != ctx) continue;
|
|
756
|
+
if (!s->closed && s->fd >= 0) {
|
|
757
|
+
close(s->fd);
|
|
758
|
+
s->fd = -1;
|
|
759
|
+
s->closed = true;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/* ── Module init ─────────────────────────────────────────────────────*/
|
|
765
|
+
|
|
766
|
+
static int mik__udp_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
767
|
+
JS_SetModuleExport(ctx, m, "bind", JS_NewCFunction(ctx, mik__udp_bind, "bind", 1));
|
|
768
|
+
return 0;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
JSModuleDef* mik__udp_init(JSContext* ctx) {
|
|
772
|
+
JSRuntime* rt = JS_GetRuntime(ctx);
|
|
773
|
+
|
|
774
|
+
/* The class only carries the opaque pointer, finalizer, and gc_mark.
|
|
775
|
+
* Methods/properties go directly on each socket object (see
|
|
776
|
+
* mik__udp_install_methods). No class prototype is set. */
|
|
777
|
+
JS_NewClassID(rt, &udp_socket_class_id);
|
|
778
|
+
JS_NewClass(rt, udp_socket_class_id, &mik__udp_socket_class);
|
|
779
|
+
|
|
780
|
+
JSModuleDef* m = JS_NewCModule(ctx, "native:udp", mik__udp_module_init);
|
|
781
|
+
if (!m) return nullptr;
|
|
782
|
+
JS_AddModuleExport(ctx, m, "bind");
|
|
783
|
+
return m;
|
|
784
|
+
}
|
package/src/mikrojs.cpp
CHANGED
|
@@ -304,6 +304,7 @@ MIKRuntime* MIK_NewRuntimeInternal(MIKRunOptions* options) {
|
|
|
304
304
|
* rt->result_proto being set. */
|
|
305
305
|
mik__result_init(ctx);
|
|
306
306
|
mik__cbor_init(ctx);
|
|
307
|
+
mik__udp_init(ctx);
|
|
307
308
|
|
|
308
309
|
/* Native mikrojs modules (replace bytecode builtins) */
|
|
309
310
|
mik__inspect_register(ctx);
|