@velox0/cerver 0.4.2 → 0.4.3
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/README.md +22 -5
- package/lib/codegen/dispatch_gen.js +3 -0
- package/package.json +2 -2
- package/runtime/cerver.h +9 -0
- package/runtime/http_writer.c +134 -27
- package/runtime/router.c +191 -9
- package/runtime/server.c +24 -6
- package/runtime/static.c +12 -52
- package/runtime/tests/runtime_tests.c +60 -10
- package/test/run.js +11 -0
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<img src="templates/cerver.png" alt="Cerver Logo" width="120" />
|
|
3
|
-
</div>
|
|
4
|
-
|
|
5
1
|
# Cerver
|
|
6
2
|
|
|
3
|
+
[](https://github.com/velox0/cerver/actions/workflows/publish.yml)
|
|
4
|
+
[](https://github.com/velox0/cerver/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/@velox0/cerver)
|
|
6
|
+
|
|
7
|
+
<img src="templates/cerver.png" alt="Cerver Logo" width="200px" align="right" />
|
|
8
|
+
|
|
7
9
|
A lightweight, compile-time web framework that transpiles restricted JavaScript server logic into highly optimized native C HTTP server binaries.
|
|
8
10
|
|
|
9
11
|
Cerver takes a Next.js-style file-based routing structure (written in a strict subset of JavaScript), parses it, generates equivalent C code, embeds your static assets, and compiles it all into a single, standalone executable that runs with zero Node.js dependency.
|
|
@@ -11,11 +13,26 @@ Cerver takes a Next.js-style file-based routing structure (written in a strict s
|
|
|
11
13
|
## Features
|
|
12
14
|
|
|
13
15
|
- **Compile-Time Framework**: Your JavaScript is parsed and compiled to native C. There is no JavaScript engine (like V8) or interpreter included in the final binary.
|
|
14
|
-
- **Microscopic Footprint**: Generated executables are
|
|
16
|
+
- **Microscopic Footprint**: Generated executables are tiny and start in milliseconds.
|
|
15
17
|
- **Single-Binary Deployment**: Static assets (HTML, CSS, JS, images) are automatically minified and embedded directly into the executable as C byte arrays.
|
|
16
18
|
- **Native Performance**: Uses `kqueue` (macOS) or `epoll` (Linux) event loops for high-performance non-blocking I/O.
|
|
17
19
|
- **File-Based Routing**: Intuitive `app/routes/` directory structure, supporting dynamic segments (e.g., `/item/[id].js`).
|
|
18
20
|
|
|
21
|
+
## Benchmarks (Autocannon)
|
|
22
|
+
|
|
23
|
+
Local loopback runs against `localhost`, 20s per run.
|
|
24
|
+
|
|
25
|
+
Note: timeouts only appear at the highest concurrency (240 connections). On a single machine, autocannon and the server compete for CPU and kernel resources; the timeouts are likely client/loopback saturation rather than server errors.
|
|
26
|
+
|
|
27
|
+
| Connections | Pipelining | Avg req/s | Avg latency | p99 latency | Total read | Errors (timeouts) |
|
|
28
|
+
| ----------- | ---------- | --------- | ----------- | ----------- | ---------- | ----------------- |
|
|
29
|
+
| 60 | 1 | 123,005 | 0.01 ms | 0 ms | 21.0 GB | 0 |
|
|
30
|
+
| 60 | 10 | 125,973 | 4.26 ms | 10 ms | 21.5 GB | 0 |
|
|
31
|
+
| 120 | 1 | 124,214 | 0.15 ms | 1 ms | 21.2 GB | 0 |
|
|
32
|
+
| 120 | 10 | 131,245 | 8.64 ms | 15 ms | 22.4 GB | 0 |
|
|
33
|
+
| 240 | 1 | 124,890 | 0.54 ms | 1 ms | 21.3 GB | 118 (timeout) |
|
|
34
|
+
| 240 | 10 | 123,677 | 9.87 ms | 14 ms | 21.1 GB | 1540 (timeout) |
|
|
35
|
+
|
|
19
36
|
## Getting Started
|
|
20
37
|
|
|
21
38
|
1. Install globally (requires `gcc` or `clang` on your system):
|
|
@@ -162,6 +162,9 @@ function generateDispatch(routes) {
|
|
|
162
162
|
lines.push(
|
|
163
163
|
` req->params[req->params_count].value = seg${i}_start;`
|
|
164
164
|
);
|
|
165
|
+
lines.push(
|
|
166
|
+
` ((char*)seg${i}_start)[seg${i}_len] = '\\0';`
|
|
167
|
+
);
|
|
165
168
|
lines.push(` req->params_count++;`);
|
|
166
169
|
paramIdx++;
|
|
167
170
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velox0/cerver",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Compile restricted JavaScript server logic into optimized native C binaries",
|
|
5
5
|
"main": "bin/cerver.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,4 +30,4 @@
|
|
|
30
30
|
"chokidar": "^3.6.0",
|
|
31
31
|
"commander": "^13.1.0"
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
package/runtime/cerver.h
CHANGED
|
@@ -119,6 +119,9 @@ typedef struct {
|
|
|
119
119
|
|
|
120
120
|
/* Keep-alive control: set to 1 to force close after response */
|
|
121
121
|
int _force_close;
|
|
122
|
+
|
|
123
|
+
/* Internal file descriptor for sendfile serving */
|
|
124
|
+
int _file_fd;
|
|
122
125
|
} cerver_response_t;
|
|
123
126
|
|
|
124
127
|
/* Response helpers — called by generated handler code */
|
|
@@ -233,6 +236,9 @@ struct cerver_server {
|
|
|
233
236
|
/* Worker pool */
|
|
234
237
|
int worker_count;
|
|
235
238
|
cerver_worker_t* workers;
|
|
239
|
+
|
|
240
|
+
/* Route trie for radix/trie-based routing */
|
|
241
|
+
void* route_trie;
|
|
236
242
|
};
|
|
237
243
|
|
|
238
244
|
/* Server lifecycle */
|
|
@@ -262,6 +268,9 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive);
|
|
|
262
268
|
|
|
263
269
|
int cerver_route_match(const cerver_route_t* route, cerver_request_t* req);
|
|
264
270
|
cerver_handler_fn cerver_dispatch(cerver_server_t* srv, cerver_request_t* req);
|
|
271
|
+
void* cerver_trie_create(void);
|
|
272
|
+
void cerver_trie_insert(void* trie, const char* pattern, const char* method, cerver_handler_fn handler);
|
|
273
|
+
void cerver_trie_free(void* trie);
|
|
265
274
|
|
|
266
275
|
/* ------------------------------------------------------------------ */
|
|
267
276
|
/* MIME (internal) */
|
package/runtime/http_writer.c
CHANGED
|
@@ -12,6 +12,64 @@
|
|
|
12
12
|
#include <string.h>
|
|
13
13
|
#include <unistd.h>
|
|
14
14
|
#include <sys/uio.h>
|
|
15
|
+
#include <errno.h>
|
|
16
|
+
|
|
17
|
+
#ifdef __linux__
|
|
18
|
+
#include <sys/sendfile.h>
|
|
19
|
+
static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count) {
|
|
20
|
+
off_t off = offset;
|
|
21
|
+
return sendfile(out_fd, in_fd, &off, count);
|
|
22
|
+
}
|
|
23
|
+
#elif defined(__APPLE__)
|
|
24
|
+
#include <sys/types.h>
|
|
25
|
+
#include <sys/socket.h>
|
|
26
|
+
static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count) {
|
|
27
|
+
off_t len = (off_t)count;
|
|
28
|
+
int res = sendfile(in_fd, out_fd, offset, &len, NULL, 0);
|
|
29
|
+
if (res == 0) {
|
|
30
|
+
return (ssize_t)len;
|
|
31
|
+
}
|
|
32
|
+
if (len > 0) {
|
|
33
|
+
return (ssize_t)len;
|
|
34
|
+
}
|
|
35
|
+
/* Fallback to read-write copy if not a socket or unsupported on this descriptor type */
|
|
36
|
+
char buf[8192];
|
|
37
|
+
if (lseek(in_fd, offset, SEEK_SET) == -1) return -1;
|
|
38
|
+
size_t to_read = count > sizeof(buf) ? sizeof(buf) : count;
|
|
39
|
+
ssize_t n_read = read(in_fd, buf, to_read);
|
|
40
|
+
if (n_read <= 0) return n_read;
|
|
41
|
+
|
|
42
|
+
size_t written = 0;
|
|
43
|
+
while (written < (size_t)n_read) {
|
|
44
|
+
ssize_t n_write = write(out_fd, buf + written, (size_t)n_read - written);
|
|
45
|
+
if (n_write < 0) {
|
|
46
|
+
if (errno == EINTR) continue;
|
|
47
|
+
return -1;
|
|
48
|
+
}
|
|
49
|
+
written += (size_t)n_write;
|
|
50
|
+
}
|
|
51
|
+
return (ssize_t)written;
|
|
52
|
+
}
|
|
53
|
+
#else
|
|
54
|
+
static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count) {
|
|
55
|
+
char buf[8192];
|
|
56
|
+
if (lseek(in_fd, offset, SEEK_SET) == -1) return -1;
|
|
57
|
+
size_t to_read = count > sizeof(buf) ? sizeof(buf) : count;
|
|
58
|
+
ssize_t n_read = read(in_fd, buf, to_read);
|
|
59
|
+
if (n_read <= 0) return n_read;
|
|
60
|
+
|
|
61
|
+
size_t written = 0;
|
|
62
|
+
while (written < (size_t)n_read) {
|
|
63
|
+
ssize_t n_write = write(out_fd, buf + written, (size_t)n_read - written);
|
|
64
|
+
if (n_write < 0) {
|
|
65
|
+
if (errno == EINTR) continue;
|
|
66
|
+
return -1;
|
|
67
|
+
}
|
|
68
|
+
written += (size_t)n_write;
|
|
69
|
+
}
|
|
70
|
+
return (ssize_t)written;
|
|
71
|
+
}
|
|
72
|
+
#endif
|
|
15
73
|
|
|
16
74
|
/* ------------------------------------------------------------------ */
|
|
17
75
|
/* Status text lookup */
|
|
@@ -93,40 +151,89 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive) {
|
|
|
93
151
|
hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen, "\r\n");
|
|
94
152
|
|
|
95
153
|
/*
|
|
96
|
-
* Use writev() to send
|
|
97
|
-
*
|
|
154
|
+
* Use writev() or sendfile() to send response, or copy to contiguous
|
|
155
|
+
* buffer if body is small to avoid writev round-trips.
|
|
98
156
|
*/
|
|
99
|
-
if (res->
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
157
|
+
if (res->_body_owned == 3) {
|
|
158
|
+
/* Send header first */
|
|
159
|
+
size_t header_total = (size_t)hlen;
|
|
160
|
+
size_t header_written = 0;
|
|
161
|
+
while (header_written < header_total) {
|
|
162
|
+
ssize_t n = write(fd, header + header_written, header_total - header_written);
|
|
163
|
+
if (n < 0) {
|
|
164
|
+
if (errno == EINTR) continue;
|
|
165
|
+
return -1;
|
|
166
|
+
}
|
|
167
|
+
header_written += (size_t)n;
|
|
168
|
+
}
|
|
108
169
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
170
|
+
/* Zero-copy body sending via sendfile(2) */
|
|
171
|
+
size_t body_total = res->body_len;
|
|
172
|
+
size_t body_sent = 0;
|
|
173
|
+
while (body_sent < body_total) {
|
|
174
|
+
ssize_t n = cerver_sendfile(fd, res->_file_fd, (off_t)body_sent, body_total - body_sent);
|
|
175
|
+
if (n < 0) {
|
|
176
|
+
if (errno == EINTR) continue;
|
|
177
|
+
return -1;
|
|
178
|
+
}
|
|
179
|
+
if (n == 0) break; /* EOF */
|
|
180
|
+
body_sent += (size_t)n;
|
|
181
|
+
}
|
|
182
|
+
} else if (res->body && res->body_len > 0) {
|
|
183
|
+
if ((size_t)hlen + res->body_len <= sizeof(header)) {
|
|
184
|
+
/* Small response optimization: copy body into header buffer and send in one syscall */
|
|
185
|
+
memcpy(header + hlen, res->body, res->body_len);
|
|
186
|
+
size_t total = (size_t)hlen + res->body_len;
|
|
187
|
+
size_t written = 0;
|
|
188
|
+
while (written < total) {
|
|
189
|
+
ssize_t n = write(fd, header + written, total - written);
|
|
190
|
+
if (n < 0) {
|
|
191
|
+
if (errno == EINTR) continue;
|
|
192
|
+
return -1;
|
|
193
|
+
}
|
|
194
|
+
written += (size_t)n;
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
/* Large response: use writev to send header + body */
|
|
198
|
+
struct iovec iov[2];
|
|
199
|
+
iov[0].iov_base = header;
|
|
200
|
+
iov[0].iov_len = (size_t)hlen;
|
|
201
|
+
iov[1].iov_base = (void*)res->body;
|
|
202
|
+
iov[1].iov_len = res->body_len;
|
|
203
|
+
|
|
204
|
+
size_t total = iov[0].iov_len + iov[1].iov_len;
|
|
205
|
+
size_t written = 0;
|
|
113
206
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
/*
|
|
120
|
-
size_t
|
|
121
|
-
iov[0].iov_len
|
|
122
|
-
|
|
123
|
-
|
|
207
|
+
while (written < total) {
|
|
208
|
+
ssize_t n = writev(fd, iov, 2);
|
|
209
|
+
if (n < 0) return -1;
|
|
210
|
+
written += (size_t)n;
|
|
211
|
+
|
|
212
|
+
/* Adjust iov for partial writes */
|
|
213
|
+
size_t to_consume = (size_t)n;
|
|
214
|
+
if (to_consume < iov[0].iov_len) {
|
|
215
|
+
iov[0].iov_base = (char*)iov[0].iov_base + to_consume;
|
|
216
|
+
iov[0].iov_len -= to_consume;
|
|
217
|
+
} else {
|
|
218
|
+
to_consume -= iov[0].iov_len;
|
|
219
|
+
iov[0].iov_len = 0;
|
|
220
|
+
iov[1].iov_base = (char*)iov[1].iov_base + to_consume;
|
|
221
|
+
iov[1].iov_len -= to_consume;
|
|
222
|
+
}
|
|
124
223
|
}
|
|
125
224
|
}
|
|
126
225
|
} else {
|
|
127
226
|
/* No body — just send header */
|
|
128
|
-
|
|
129
|
-
|
|
227
|
+
size_t total = (size_t)hlen;
|
|
228
|
+
size_t written = 0;
|
|
229
|
+
while (written < total) {
|
|
230
|
+
ssize_t n = write(fd, header + written, total - written);
|
|
231
|
+
if (n < 0) {
|
|
232
|
+
if (errno == EINTR) continue;
|
|
233
|
+
return -1;
|
|
234
|
+
}
|
|
235
|
+
written += (size_t)n;
|
|
236
|
+
}
|
|
130
237
|
}
|
|
131
238
|
|
|
132
239
|
return 0;
|
package/runtime/router.c
CHANGED
|
@@ -19,8 +19,15 @@
|
|
|
19
19
|
|
|
20
20
|
const char* cerver_req_param(const cerver_request_t* req, const char* key) {
|
|
21
21
|
for (int i = 0; i < req->params_count; i++) {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const char* pkey = req->params[i].key;
|
|
23
|
+
if (pkey && pkey[0] == key[0]) {
|
|
24
|
+
int j = 0;
|
|
25
|
+
while (key[j] && pkey[j] == key[j]) {
|
|
26
|
+
j++;
|
|
27
|
+
}
|
|
28
|
+
if (key[j] == '\0' && (pkey[j] == '\0' || pkey[j] == '/')) {
|
|
29
|
+
return req->params[i].value;
|
|
30
|
+
}
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
33
|
return "";
|
|
@@ -102,6 +109,8 @@ int cerver_route_match(const cerver_route_t* route, cerver_request_t* req) {
|
|
|
102
109
|
const char* rp = path; /* request path pointer */
|
|
103
110
|
|
|
104
111
|
int saved_params = req->params_count;
|
|
112
|
+
char* param_slashes[CERVER_MAX_PARAMS];
|
|
113
|
+
int param_slash_count = 0;
|
|
105
114
|
|
|
106
115
|
/* Skip leading '/' */
|
|
107
116
|
if (*pp == '/') pp++;
|
|
@@ -122,10 +131,12 @@ int cerver_route_match(const cerver_route_t* route, cerver_request_t* req) {
|
|
|
122
131
|
/* Dynamic segment — extract parameter */
|
|
123
132
|
if (req->params_count < CERVER_MAX_PARAMS) {
|
|
124
133
|
req->params[req->params_count].key = pp_seg + 1;
|
|
125
|
-
/*
|
|
126
|
-
/* The key points into the route pattern (static/const) */
|
|
134
|
+
/* The key points into the route pattern (static/const, not NUL-terminated) */
|
|
127
135
|
req->params[req->params_count].value = rp_seg;
|
|
128
136
|
req->params_count++;
|
|
137
|
+
if (*rp == '/') {
|
|
138
|
+
param_slashes[param_slash_count++] = (char*)rp;
|
|
139
|
+
}
|
|
129
140
|
}
|
|
130
141
|
} else {
|
|
131
142
|
/* Static segment — must match exactly */
|
|
@@ -146,6 +157,11 @@ int cerver_route_match(const cerver_route_t* route, cerver_request_t* req) {
|
|
|
146
157
|
return 0;
|
|
147
158
|
}
|
|
148
159
|
|
|
160
|
+
/* Match succeeded, NUL-terminate extracted values in-place inside req->path */
|
|
161
|
+
for (int i = 0; i < param_slash_count; i++) {
|
|
162
|
+
*param_slashes[i] = '\0';
|
|
163
|
+
}
|
|
164
|
+
|
|
149
165
|
return 1;
|
|
150
166
|
}
|
|
151
167
|
|
|
@@ -153,6 +169,166 @@ int cerver_route_match(const cerver_route_t* route, cerver_request_t* req) {
|
|
|
153
169
|
/* Dispatch: find and return the handler for a request */
|
|
154
170
|
/* ------------------------------------------------------------------ */
|
|
155
171
|
|
|
172
|
+
/* ------------------------------------------------------------------ */
|
|
173
|
+
/* Trie/Radix Route Router */
|
|
174
|
+
/* ------------------------------------------------------------------ */
|
|
175
|
+
|
|
176
|
+
static char* trie_strndup(const char* s, size_t n) {
|
|
177
|
+
char* p = malloc(n + 1);
|
|
178
|
+
if (p) {
|
|
179
|
+
memcpy(p, s, n);
|
|
180
|
+
p[n] = '\0';
|
|
181
|
+
}
|
|
182
|
+
return p;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
typedef struct trie_node trie_node_t;
|
|
186
|
+
|
|
187
|
+
struct trie_node {
|
|
188
|
+
char* segment;
|
|
189
|
+
int is_param;
|
|
190
|
+
char* param_name;
|
|
191
|
+
|
|
192
|
+
struct {
|
|
193
|
+
const char* method;
|
|
194
|
+
cerver_handler_fn handler;
|
|
195
|
+
} handlers[16];
|
|
196
|
+
int handler_count;
|
|
197
|
+
|
|
198
|
+
trie_node_t** children;
|
|
199
|
+
int children_count;
|
|
200
|
+
int children_cap;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
void* cerver_trie_create(void) {
|
|
204
|
+
trie_node_t* node = calloc(1, sizeof(trie_node_t));
|
|
205
|
+
return node;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
static trie_node_t* trie_create_node(const char* segment, size_t len) {
|
|
209
|
+
trie_node_t* node = calloc(1, sizeof(trie_node_t));
|
|
210
|
+
if (node && segment) {
|
|
211
|
+
node->segment = trie_strndup(segment, len);
|
|
212
|
+
if (node->segment[0] == ':') {
|
|
213
|
+
node->is_param = 1;
|
|
214
|
+
node->param_name = trie_strndup(node->segment + 1, len - 1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return node;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
void cerver_trie_insert(void* trie, const char* pattern, const char* method, cerver_handler_fn handler) {
|
|
221
|
+
if (!trie) return;
|
|
222
|
+
trie_node_t* curr = (trie_node_t*)trie;
|
|
223
|
+
const char* p = pattern;
|
|
224
|
+
while (*p == '/') p++;
|
|
225
|
+
|
|
226
|
+
while (*p) {
|
|
227
|
+
const char* seg_start = p;
|
|
228
|
+
while (*p && *p != '/') p++;
|
|
229
|
+
size_t len = (size_t)(p - seg_start);
|
|
230
|
+
if (len == 0) {
|
|
231
|
+
while (*p == '/') p++;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Find if child exists
|
|
236
|
+
trie_node_t* child = NULL;
|
|
237
|
+
for (int i = 0; i < curr->children_count; i++) {
|
|
238
|
+
trie_node_t* c = curr->children[i];
|
|
239
|
+
if (strlen(c->segment) == len && memcmp(c->segment, seg_start, len) == 0) {
|
|
240
|
+
child = c;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!child) {
|
|
246
|
+
child = trie_create_node(seg_start, len);
|
|
247
|
+
if (curr->children_count >= curr->children_cap) {
|
|
248
|
+
curr->children_cap = curr->children_cap == 0 ? 4 : curr->children_cap * 2;
|
|
249
|
+
curr->children = realloc(curr->children, curr->children_cap * sizeof(trie_node_t*));
|
|
250
|
+
}
|
|
251
|
+
curr->children[curr->children_count++] = child;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
curr = child;
|
|
255
|
+
while (*p == '/') p++;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Add handler to leaf
|
|
259
|
+
if (curr->handler_count < 16) {
|
|
260
|
+
curr->handlers[curr->handler_count].method = method;
|
|
261
|
+
curr->handlers[curr->handler_count].handler = handler;
|
|
262
|
+
curr->handler_count++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
void cerver_trie_free(void* trie) {
|
|
267
|
+
if (!trie) return;
|
|
268
|
+
trie_node_t* node = (trie_node_t*)trie;
|
|
269
|
+
for (int i = 0; i < node->children_count; i++) {
|
|
270
|
+
cerver_trie_free(node->children[i]);
|
|
271
|
+
}
|
|
272
|
+
free(node->children);
|
|
273
|
+
free(node->segment);
|
|
274
|
+
free(node->param_name);
|
|
275
|
+
free(node);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
static int trie_match_recursive(trie_node_t* node, const char* path, cerver_request_t* req, cerver_handler_fn* out_handler, int param_start_idx) {
|
|
279
|
+
// Skip leading slashes
|
|
280
|
+
while (*path == '/') path++;
|
|
281
|
+
|
|
282
|
+
if (*path == '\0') {
|
|
283
|
+
// Check if node has a handler for req->method
|
|
284
|
+
for (int i = 0; i < node->handler_count; i++) {
|
|
285
|
+
if (strcmp(node->handlers[i].method, req->method) == 0) {
|
|
286
|
+
*out_handler = node->handlers[i].handler;
|
|
287
|
+
req->params_count = param_start_idx;
|
|
288
|
+
return 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Extract next segment from path
|
|
295
|
+
const char* seg_start = path;
|
|
296
|
+
while (*path && *path != '/') path++;
|
|
297
|
+
size_t seg_len = (size_t)(path - seg_start);
|
|
298
|
+
|
|
299
|
+
// Try static children first
|
|
300
|
+
for (int i = 0; i < node->children_count; i++) {
|
|
301
|
+
trie_node_t* child = node->children[i];
|
|
302
|
+
if (!child->is_param) {
|
|
303
|
+
if (strlen(child->segment) == seg_len && memcmp(child->segment, seg_start, seg_len) == 0) {
|
|
304
|
+
if (trie_match_recursive(child, path, req, out_handler, param_start_idx)) {
|
|
305
|
+
return 1;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Try parameter/dynamic children next
|
|
312
|
+
for (int i = 0; i < node->children_count; i++) {
|
|
313
|
+
trie_node_t* child = node->children[i];
|
|
314
|
+
if (child->is_param) {
|
|
315
|
+
if (param_start_idx < CERVER_MAX_PARAMS) {
|
|
316
|
+
req->params[param_start_idx].key = child->param_name;
|
|
317
|
+
req->params[param_start_idx].value = seg_start;
|
|
318
|
+
}
|
|
319
|
+
if (trie_match_recursive(child, path, req, out_handler, param_start_idx + 1)) {
|
|
320
|
+
return 1;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* ------------------------------------------------------------------ */
|
|
329
|
+
/* Dispatch: find and return the handler for a request */
|
|
330
|
+
/* ------------------------------------------------------------------ */
|
|
331
|
+
|
|
156
332
|
cerver_handler_fn cerver_dispatch(cerver_server_t* srv, cerver_request_t* req) {
|
|
157
333
|
/* Try the generated compile-time dispatch first */
|
|
158
334
|
if (srv->dispatch_override) {
|
|
@@ -160,13 +336,19 @@ cerver_handler_fn cerver_dispatch(cerver_server_t* srv, cerver_request_t* req) {
|
|
|
160
336
|
if (h) return h;
|
|
161
337
|
}
|
|
162
338
|
|
|
163
|
-
/* Fall back to generic route table scan */
|
|
164
|
-
if (!srv->
|
|
339
|
+
/* Fall back to generic route table scan via Trie */
|
|
340
|
+
if (!srv->route_trie) return NULL;
|
|
165
341
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
342
|
+
cerver_handler_fn handler = NULL;
|
|
343
|
+
req->params_count = 0;
|
|
344
|
+
if (trie_match_recursive((trie_node_t*)srv->route_trie, req->path, req, &handler, 0)) {
|
|
345
|
+
// NUL-terminate extracted values in-place inside req->path
|
|
346
|
+
for (int i = 0; i < req->params_count; i++) {
|
|
347
|
+
char* val = (char*)req->params[i].value;
|
|
348
|
+
while (*val && *val != '/') val++;
|
|
349
|
+
if (*val == '/') *val = '\0';
|
|
169
350
|
}
|
|
351
|
+
return handler;
|
|
170
352
|
}
|
|
171
353
|
|
|
172
354
|
return NULL;
|
package/runtime/server.c
CHANGED
|
@@ -278,6 +278,8 @@ static void handle_connection(cerver_server_t* srv, int client_fd) {
|
|
|
278
278
|
free((void*)res.body);
|
|
279
279
|
else if (res._body_owned == 2 && res.body)
|
|
280
280
|
munmap((void*)res.body, res.body_len);
|
|
281
|
+
else if (res._body_owned == 3 && res._file_fd >= 0)
|
|
282
|
+
close(res._file_fd);
|
|
281
283
|
|
|
282
284
|
free(buf);
|
|
283
285
|
if (write_err < 0) break;
|
|
@@ -348,7 +350,7 @@ static int create_listener(int port, int reuseport) {
|
|
|
348
350
|
int opt = 1;
|
|
349
351
|
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
|
350
352
|
|
|
351
|
-
#
|
|
353
|
+
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
|
|
352
354
|
if (reuseport) setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
|
|
353
355
|
#else
|
|
354
356
|
(void)reuseport;
|
|
@@ -621,6 +623,13 @@ int cerver_init(cerver_server_t* srv, int port, int threads) {
|
|
|
621
623
|
int cerver_add_routes(cerver_server_t* srv, cerver_route_t* routes, int count) {
|
|
622
624
|
srv->routes = routes;
|
|
623
625
|
srv->route_count = count;
|
|
626
|
+
|
|
627
|
+
srv->route_trie = cerver_trie_create();
|
|
628
|
+
if (srv->route_trie) {
|
|
629
|
+
for (int i = 0; i < count; i++) {
|
|
630
|
+
cerver_trie_insert(srv->route_trie, routes[i].pattern, routes[i].method, routes[i].handler);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
624
633
|
return 0;
|
|
625
634
|
}
|
|
626
635
|
|
|
@@ -702,7 +711,7 @@ int cerver_listen(cerver_server_t* srv) {
|
|
|
702
711
|
return -1;
|
|
703
712
|
}
|
|
704
713
|
|
|
705
|
-
srv->sock_fd = create_listener(srv->port,
|
|
714
|
+
srv->sock_fd = create_listener(srv->port, 1);
|
|
706
715
|
if (srv->sock_fd < 0) {
|
|
707
716
|
srv->running = 0;
|
|
708
717
|
for (int i = 0; i < pool_size; i++) pthread_join(pool_threads[i], NULL);
|
|
@@ -730,9 +739,13 @@ int cerver_listen(cerver_server_t* srv) {
|
|
|
730
739
|
w->id = i;
|
|
731
740
|
w->srv = srv;
|
|
732
741
|
w->event_fd = -1;
|
|
733
|
-
#
|
|
734
|
-
|
|
735
|
-
|
|
742
|
+
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
|
|
743
|
+
if (i == 0) {
|
|
744
|
+
w->listen_fd = srv->sock_fd;
|
|
745
|
+
} else {
|
|
746
|
+
w->listen_fd = create_listener(srv->port, 1);
|
|
747
|
+
if (w->listen_fd < 0) w->listen_fd = srv->sock_fd;
|
|
748
|
+
}
|
|
736
749
|
#else
|
|
737
750
|
w->listen_fd = srv->sock_fd;
|
|
738
751
|
#endif
|
|
@@ -791,7 +804,7 @@ void cerver_shutdown(cerver_server_t* srv) {
|
|
|
791
804
|
|
|
792
805
|
if (srv->workers) {
|
|
793
806
|
for (int i = 0; i < srv->worker_count; i++) {
|
|
794
|
-
#
|
|
807
|
+
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
|
|
795
808
|
if (srv->workers[i].listen_fd != srv->sock_fd && srv->workers[i].listen_fd >= 0)
|
|
796
809
|
close(srv->workers[i].listen_fd);
|
|
797
810
|
#endif
|
|
@@ -801,6 +814,11 @@ void cerver_shutdown(cerver_server_t* srv) {
|
|
|
801
814
|
srv->workers = NULL;
|
|
802
815
|
}
|
|
803
816
|
|
|
817
|
+
if (srv->route_trie) {
|
|
818
|
+
cerver_trie_free(srv->route_trie);
|
|
819
|
+
srv->route_trie = NULL;
|
|
820
|
+
}
|
|
821
|
+
|
|
804
822
|
if (srv->sock_fd >= 0) {
|
|
805
823
|
close(srv->sock_fd);
|
|
806
824
|
srv->sock_fd = -1;
|
package/runtime/static.c
CHANGED
|
@@ -189,59 +189,19 @@ static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_
|
|
|
189
189
|
const char* mime = cerver_mime_from_path(full_path);
|
|
190
190
|
|
|
191
191
|
/*
|
|
192
|
-
* Use
|
|
193
|
-
* The
|
|
194
|
-
*
|
|
195
|
-
* but for simplicity we'll use read() for small files and mmap for large.
|
|
192
|
+
* Use sendfile for zero-copy filesystem static serving.
|
|
193
|
+
* The file is opened and its descriptor is stored in the response structure
|
|
194
|
+
* for streaming directly to the client socket in the writer.
|
|
196
195
|
*/
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
/* Advise the kernel we'll read sequentially */
|
|
208
|
-
madvise(mapped, file_size, MADV_SEQUENTIAL);
|
|
209
|
-
|
|
210
|
-
res->status = 200;
|
|
211
|
-
res->content_type = mime;
|
|
212
|
-
res->body = (const char*)mapped;
|
|
213
|
-
res->body_len = file_size;
|
|
214
|
-
res->_body_owned = 2; /* Special flag: needs munmap, not free */
|
|
215
|
-
} else {
|
|
216
|
-
/* Small files: read into buffer (avoids mmap overhead) */
|
|
217
|
-
int fd = open(full_path, O_RDONLY);
|
|
218
|
-
if (fd < 0) return -1;
|
|
219
|
-
|
|
220
|
-
char* file_data = malloc(file_size);
|
|
221
|
-
if (!file_data) {
|
|
222
|
-
close(fd);
|
|
223
|
-
return -1;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
size_t total = 0;
|
|
227
|
-
while (total < file_size) {
|
|
228
|
-
ssize_t n = read(fd, file_data + total, file_size - total);
|
|
229
|
-
if (n <= 0) break;
|
|
230
|
-
total += (size_t)n;
|
|
231
|
-
}
|
|
232
|
-
close(fd);
|
|
233
|
-
|
|
234
|
-
if (total != file_size) {
|
|
235
|
-
free(file_data);
|
|
236
|
-
return -1;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
res->status = 200;
|
|
240
|
-
res->content_type = mime;
|
|
241
|
-
res->body = file_data;
|
|
242
|
-
res->body_len = file_size;
|
|
243
|
-
res->_body_owned = 1; /* malloc'd */
|
|
244
|
-
}
|
|
196
|
+
int fd = open(full_path, O_RDONLY);
|
|
197
|
+
if (fd < 0) return -1;
|
|
198
|
+
|
|
199
|
+
res->status = 200;
|
|
200
|
+
res->content_type = mime;
|
|
201
|
+
res->body = NULL;
|
|
202
|
+
res->body_len = file_size;
|
|
203
|
+
res->_body_owned = 3; /* Special flag: sendfile, close fd */
|
|
204
|
+
res->_file_fd = fd;
|
|
245
205
|
|
|
246
206
|
add_cache_headers(res, path);
|
|
247
207
|
|
|
@@ -224,6 +224,22 @@ static void test_route_match_mismatch_resets_params(void) {
|
|
|
224
224
|
MU_ASSERT_EQ_INT(0, req.params_count);
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
static void test_route_match_multi_segment(void) {
|
|
228
|
+
cerver_route_t route;
|
|
229
|
+
route.method = "GET";
|
|
230
|
+
route.pattern = "/users/:id/profile";
|
|
231
|
+
route.handler = handler_a;
|
|
232
|
+
|
|
233
|
+
cerver_request_t req;
|
|
234
|
+
memset(&req, 0, sizeof(req));
|
|
235
|
+
strcpy(req.method, "GET");
|
|
236
|
+
strcpy(req.path, "/users/123/profile");
|
|
237
|
+
|
|
238
|
+
MU_ASSERT(cerver_route_match(&route, &req) == 1);
|
|
239
|
+
MU_ASSERT_EQ_INT(1, req.params_count);
|
|
240
|
+
MU_ASSERT_STREQ("123", cerver_req_param(&req, "id"));
|
|
241
|
+
}
|
|
242
|
+
|
|
227
243
|
static void test_request_header_helpers(void) {
|
|
228
244
|
cerver_request_t req;
|
|
229
245
|
memset(&req, 0, sizeof(req));
|
|
@@ -318,10 +334,27 @@ static void test_static_filesystem_small(void) {
|
|
|
318
334
|
|
|
319
335
|
MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
|
|
320
336
|
MU_ASSERT_EQ_SIZE(5, res.body_len);
|
|
321
|
-
MU_ASSERT(
|
|
322
|
-
MU_ASSERT_EQ_INT(
|
|
337
|
+
MU_ASSERT(res.body == NULL);
|
|
338
|
+
MU_ASSERT_EQ_INT(3, res._body_owned);
|
|
339
|
+
MU_ASSERT(res._file_fd >= 0);
|
|
340
|
+
|
|
341
|
+
int fds[2];
|
|
342
|
+
MU_ASSERT(pipe(fds) == 0);
|
|
343
|
+
MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
|
|
344
|
+
close(fds[1]);
|
|
323
345
|
|
|
324
|
-
|
|
346
|
+
char out[1024];
|
|
347
|
+
ssize_t n = read_all(fds[0], out, sizeof(out));
|
|
348
|
+
MU_ASSERT(n > 0);
|
|
349
|
+
close(fds[0]);
|
|
350
|
+
|
|
351
|
+
MU_ASSERT(strstr(out, "HTTP/1.1 200 OK\r\n") != NULL);
|
|
352
|
+
MU_ASSERT(strstr(out, "Content-Length: 5\r\n") != NULL);
|
|
353
|
+
MU_ASSERT(strstr(out, "\r\nsmall") != NULL);
|
|
354
|
+
|
|
355
|
+
if (res._body_owned == 3 && res._file_fd >= 0) {
|
|
356
|
+
close(res._file_fd);
|
|
357
|
+
}
|
|
325
358
|
unlink(file_path);
|
|
326
359
|
rmdir(dir);
|
|
327
360
|
}
|
|
@@ -334,10 +367,10 @@ static void test_static_filesystem_large(void) {
|
|
|
334
367
|
char file_path[PATH_MAX];
|
|
335
368
|
snprintf(file_path, sizeof(file_path), "%s/large.bin", dir);
|
|
336
369
|
|
|
337
|
-
char* payload = (char*)malloc(
|
|
370
|
+
char* payload = (char*)malloc(32000);
|
|
338
371
|
MU_ASSERT(payload != NULL);
|
|
339
|
-
memset(payload, 'a',
|
|
340
|
-
MU_ASSERT_EQ_INT(0, write_file(file_path, payload,
|
|
372
|
+
memset(payload, 'a', 32000);
|
|
373
|
+
MU_ASSERT_EQ_INT(0, write_file(file_path, payload, 32000));
|
|
341
374
|
free(payload);
|
|
342
375
|
|
|
343
376
|
cerver_server_t srv;
|
|
@@ -352,11 +385,27 @@ static void test_static_filesystem_large(void) {
|
|
|
352
385
|
strcpy(req.path, "/large.bin");
|
|
353
386
|
|
|
354
387
|
MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
|
|
355
|
-
MU_ASSERT_EQ_SIZE(
|
|
356
|
-
|
|
357
|
-
|
|
388
|
+
MU_ASSERT_EQ_SIZE(32000, res.body_len);
|
|
389
|
+
MU_ASSERT(res.body == NULL);
|
|
390
|
+
MU_ASSERT_EQ_INT(3, res._body_owned);
|
|
391
|
+
MU_ASSERT(res._file_fd >= 0);
|
|
358
392
|
|
|
359
|
-
|
|
393
|
+
int fds[2];
|
|
394
|
+
MU_ASSERT(pipe(fds) == 0);
|
|
395
|
+
MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
|
|
396
|
+
close(fds[1]);
|
|
397
|
+
|
|
398
|
+
char out[35000];
|
|
399
|
+
ssize_t n = read_all(fds[0], out, sizeof(out));
|
|
400
|
+
MU_ASSERT(n > 0);
|
|
401
|
+
close(fds[0]);
|
|
402
|
+
|
|
403
|
+
MU_ASSERT(strstr(out, "HTTP/1.1 200 OK\r\n") != NULL);
|
|
404
|
+
MU_ASSERT(strstr(out, "Content-Length: 32000\r\n") != NULL);
|
|
405
|
+
|
|
406
|
+
if (res._body_owned == 3 && res._file_fd >= 0) {
|
|
407
|
+
close(res._file_fd);
|
|
408
|
+
}
|
|
360
409
|
unlink(file_path);
|
|
361
410
|
rmdir(dir);
|
|
362
411
|
}
|
|
@@ -402,6 +451,7 @@ int main(void) {
|
|
|
402
451
|
mu_run("write_response_force_close", test_write_response_force_close);
|
|
403
452
|
mu_run("route_match_and_dispatch", test_route_match_and_dispatch);
|
|
404
453
|
mu_run("route_match_mismatch_resets_params", test_route_match_mismatch_resets_params);
|
|
454
|
+
mu_run("route_match_multi_segment", test_route_match_multi_segment);
|
|
405
455
|
mu_run("request_header_helpers", test_request_header_helpers);
|
|
406
456
|
mu_run("static_embedded_prefers_br", test_static_embedded_prefers_br);
|
|
407
457
|
mu_run("static_embedded_index_fallback", test_static_embedded_index_fallback);
|
package/test/run.js
CHANGED
|
@@ -190,6 +190,17 @@ test("generateRouteTable emits forward declarations, entries, and count", () =>
|
|
|
190
190
|
assert.match(code, /static const int cerver_route_count = 2;/);
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
test("generateDispatch generates correct parameter extraction and termination", () => {
|
|
194
|
+
const { generateDispatch } = require("../lib/codegen/dispatch_gen");
|
|
195
|
+
const routes = [
|
|
196
|
+
IR.IRRoute("GET", "/users/:id/profile", ["id"], IR.IRHandler([], [])),
|
|
197
|
+
];
|
|
198
|
+
const code = generateDispatch(routes);
|
|
199
|
+
assert.match(code, /req->params\[req->params_count\]\.key = "id";/);
|
|
200
|
+
assert.match(code, /req->params\[req->params_count\]\.value = seg1_start;/);
|
|
201
|
+
assert.match(code, /\(\(char\*\)seg1_start\)\[seg1_len\] = '\\0';/);
|
|
202
|
+
});
|
|
203
|
+
|
|
193
204
|
test("loadConfig merges defaults and supports export default configs", () => {
|
|
194
205
|
const dir = tempDir();
|
|
195
206
|
try {
|