@velox0/cerver 0.4.2 → 0.5.0
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 +37 -10
- package/lib/assets/embed.js +33 -8
- package/lib/codegen/dispatch_gen.js +4 -1
- package/lib/codegen/generator.js +90 -55
- package/lib/codegen/route_table.js +2 -2
- package/lib/commands/build.js +31 -8
- package/lib/commands/dev.js +9 -2
- package/lib/commands/new.js +667 -258
- package/lib/commands/run.js +8 -1
- package/lib/compiler/compile.js +9 -4
- package/lib/config.js +20 -1
- package/lib/parser/discover.js +6 -6
- package/package.json +2 -2
- package/runtime/cerver.h +10 -0
- package/runtime/http_writer.c +134 -27
- package/runtime/router.c +194 -10
- package/runtime/server.c +40 -6
- package/runtime/static.c +80 -64
- package/runtime/tests/runtime_tests.c +142 -13
- package/.github/workflows/ci.yml +0 -35
- package/.github/workflows/publish.yml +0 -50
- package/test/run.js +0 -355
package/lib/commands/run.js
CHANGED
|
@@ -3,12 +3,19 @@
|
|
|
3
3
|
const { execFileSync } = require("child_process");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const fs = require("fs");
|
|
6
|
+
const { findProjectRoot } = require("../config");
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Run the compiled binary from dist/.
|
|
9
10
|
*/
|
|
10
11
|
function run(opts) {
|
|
11
|
-
const projectDir =
|
|
12
|
+
const projectDir = findProjectRoot();
|
|
13
|
+
if (!projectDir) {
|
|
14
|
+
console.log("Not a cerver project");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
process.chdir(projectDir);
|
|
18
|
+
|
|
12
19
|
const binaryPath = path.join(projectDir, "dist", "server");
|
|
13
20
|
|
|
14
21
|
if (!fs.existsSync(binaryPath)) {
|
package/lib/compiler/compile.js
CHANGED
|
@@ -45,12 +45,17 @@ function supportsFlag(cc, flag) {
|
|
|
45
45
|
*/
|
|
46
46
|
function compile(distDir, runtimeDir, opts) {
|
|
47
47
|
const cc = detectCompiler();
|
|
48
|
-
const serverC = path.join(distDir, "server.c");
|
|
49
48
|
const outputBin = path.join(distDir, "server");
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
/* Collect all generated source files from distDir */
|
|
51
|
+
const generatedSources = fs
|
|
52
|
+
.readdirSync(distDir)
|
|
53
|
+
.filter((f) => f.endsWith(".c"))
|
|
54
|
+
.map((f) => path.join(distDir, f));
|
|
55
|
+
|
|
56
|
+
if (generatedSources.length === 0) {
|
|
52
57
|
throw new Error(
|
|
53
|
-
`cerver: generated source
|
|
58
|
+
`cerver: no generated C source files found in ${distDir}`
|
|
54
59
|
);
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -68,7 +73,7 @@ function compile(distDir, runtimeDir, opts) {
|
|
|
68
73
|
"-Wno-unused-parameter",
|
|
69
74
|
"-o",
|
|
70
75
|
outputBin,
|
|
71
|
-
|
|
76
|
+
...generatedSources,
|
|
72
77
|
...runtimeSources,
|
|
73
78
|
`-I${runtimeDir}`,
|
|
74
79
|
"-lpthread",
|
package/lib/config.js
CHANGED
|
@@ -53,4 +53,23 @@ function loadConfig(projectDir) {
|
|
|
53
53
|
return config;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
57
|
+
let currentDir = startDir;
|
|
58
|
+
|
|
59
|
+
while (true) {
|
|
60
|
+
if (fs.existsSync(path.join(currentDir, "cerver.config.js"))) {
|
|
61
|
+
return currentDir;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const parentDir = path.dirname(currentDir);
|
|
65
|
+
if (parentDir === currentDir) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
currentDir = parentDir;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { loadConfig, findProjectRoot, DEFAULTS };
|
|
75
|
+
|
package/lib/parser/discover.js
CHANGED
|
@@ -4,15 +4,15 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Discover route files under
|
|
7
|
+
* Discover route files under routes/ and map them to URL paths.
|
|
8
8
|
*
|
|
9
9
|
* File-based routing convention:
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* routes/index.js → /
|
|
11
|
+
* routes/page.js → /page
|
|
12
|
+
* routes/group/item.js → /group/item
|
|
13
|
+
* routes/item/[id].js → /item/:id
|
|
14
14
|
*
|
|
15
|
-
* @param {string} routesDir - Absolute path to
|
|
15
|
+
* @param {string} routesDir - Absolute path to routes/
|
|
16
16
|
* @returns {Array<{ filePath: string, urlPath: string }>}
|
|
17
17
|
*/
|
|
18
18
|
function discoverRoutes(routesDir) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velox0/cerver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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,10 @@ 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,
|
|
273
|
+
cerver_handler_fn handler);
|
|
274
|
+
void cerver_trie_free(void* trie);
|
|
265
275
|
|
|
266
276
|
/* ------------------------------------------------------------------ */
|
|
267
277
|
/* 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 "";
|
|
@@ -101,7 +108,9 @@ int cerver_route_match(const cerver_route_t* route, cerver_request_t* req) {
|
|
|
101
108
|
const char* pp = pattern; /* pattern pointer */
|
|
102
109
|
const char* rp = path; /* request path pointer */
|
|
103
110
|
|
|
104
|
-
int
|
|
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,168 @@ 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,
|
|
221
|
+
cerver_handler_fn handler) {
|
|
222
|
+
if (!trie) return;
|
|
223
|
+
trie_node_t* curr = (trie_node_t*)trie;
|
|
224
|
+
const char* p = pattern;
|
|
225
|
+
while (*p == '/') p++;
|
|
226
|
+
|
|
227
|
+
while (*p) {
|
|
228
|
+
const char* seg_start = p;
|
|
229
|
+
while (*p && *p != '/') p++;
|
|
230
|
+
size_t len = (size_t)(p - seg_start);
|
|
231
|
+
if (len == 0) {
|
|
232
|
+
while (*p == '/') p++;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Find if child exists
|
|
237
|
+
trie_node_t* child = NULL;
|
|
238
|
+
for (int i = 0; i < curr->children_count; i++) {
|
|
239
|
+
trie_node_t* c = curr->children[i];
|
|
240
|
+
if (strlen(c->segment) == len && memcmp(c->segment, seg_start, len) == 0) {
|
|
241
|
+
child = c;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!child) {
|
|
247
|
+
child = trie_create_node(seg_start, len);
|
|
248
|
+
if (curr->children_count >= curr->children_cap) {
|
|
249
|
+
curr->children_cap = curr->children_cap == 0 ? 4 : curr->children_cap * 2;
|
|
250
|
+
curr->children = realloc(curr->children, curr->children_cap * sizeof(trie_node_t*));
|
|
251
|
+
}
|
|
252
|
+
curr->children[curr->children_count++] = child;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
curr = child;
|
|
256
|
+
while (*p == '/') p++;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Add handler to leaf
|
|
260
|
+
if (curr->handler_count < 16) {
|
|
261
|
+
curr->handlers[curr->handler_count].method = method;
|
|
262
|
+
curr->handlers[curr->handler_count].handler = handler;
|
|
263
|
+
curr->handler_count++;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
void cerver_trie_free(void* trie) {
|
|
268
|
+
if (!trie) return;
|
|
269
|
+
trie_node_t* node = (trie_node_t*)trie;
|
|
270
|
+
for (int i = 0; i < node->children_count; i++) {
|
|
271
|
+
cerver_trie_free(node->children[i]);
|
|
272
|
+
}
|
|
273
|
+
free(node->children);
|
|
274
|
+
free(node->segment);
|
|
275
|
+
free(node->param_name);
|
|
276
|
+
free(node);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
static int trie_match_recursive(trie_node_t* node, const char* path, cerver_request_t* req,
|
|
280
|
+
cerver_handler_fn* out_handler, int param_start_idx) {
|
|
281
|
+
// Skip leading slashes
|
|
282
|
+
while (*path == '/') path++;
|
|
283
|
+
|
|
284
|
+
if (*path == '\0') {
|
|
285
|
+
// Check if node has a handler for req->method
|
|
286
|
+
for (int i = 0; i < node->handler_count; i++) {
|
|
287
|
+
if (strcmp(node->handlers[i].method, req->method) == 0) {
|
|
288
|
+
*out_handler = node->handlers[i].handler;
|
|
289
|
+
req->params_count = param_start_idx;
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Extract next segment from path
|
|
297
|
+
const char* seg_start = path;
|
|
298
|
+
while (*path && *path != '/') path++;
|
|
299
|
+
size_t seg_len = (size_t)(path - seg_start);
|
|
300
|
+
|
|
301
|
+
// Try static children first
|
|
302
|
+
for (int i = 0; i < node->children_count; i++) {
|
|
303
|
+
trie_node_t* child = node->children[i];
|
|
304
|
+
if (!child->is_param) {
|
|
305
|
+
if (strlen(child->segment) == seg_len && memcmp(child->segment, seg_start, seg_len) == 0) {
|
|
306
|
+
if (trie_match_recursive(child, path, req, out_handler, param_start_idx)) {
|
|
307
|
+
return 1;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Try parameter/dynamic children next
|
|
314
|
+
for (int i = 0; i < node->children_count; i++) {
|
|
315
|
+
trie_node_t* child = node->children[i];
|
|
316
|
+
if (child->is_param) {
|
|
317
|
+
if (param_start_idx < CERVER_MAX_PARAMS) {
|
|
318
|
+
req->params[param_start_idx].key = child->param_name;
|
|
319
|
+
req->params[param_start_idx].value = seg_start;
|
|
320
|
+
}
|
|
321
|
+
if (trie_match_recursive(child, path, req, out_handler, param_start_idx + 1)) {
|
|
322
|
+
return 1;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* ------------------------------------------------------------------ */
|
|
331
|
+
/* Dispatch: find and return the handler for a request */
|
|
332
|
+
/* ------------------------------------------------------------------ */
|
|
333
|
+
|
|
156
334
|
cerver_handler_fn cerver_dispatch(cerver_server_t* srv, cerver_request_t* req) {
|
|
157
335
|
/* Try the generated compile-time dispatch first */
|
|
158
336
|
if (srv->dispatch_override) {
|
|
@@ -160,13 +338,19 @@ cerver_handler_fn cerver_dispatch(cerver_server_t* srv, cerver_request_t* req) {
|
|
|
160
338
|
if (h) return h;
|
|
161
339
|
}
|
|
162
340
|
|
|
163
|
-
/* Fall back to generic route table scan */
|
|
164
|
-
if (!srv->
|
|
341
|
+
/* Fall back to generic route table scan via Trie */
|
|
342
|
+
if (!srv->route_trie) return NULL;
|
|
165
343
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
344
|
+
cerver_handler_fn handler = NULL;
|
|
345
|
+
req->params_count = 0;
|
|
346
|
+
if (trie_match_recursive((trie_node_t*)srv->route_trie, req->path, req, &handler, 0)) {
|
|
347
|
+
// NUL-terminate extracted values in-place inside req->path
|
|
348
|
+
for (int i = 0; i < req->params_count; i++) {
|
|
349
|
+
char* val = (char*)req->params[i].value;
|
|
350
|
+
while (*val && *val != '/') val++;
|
|
351
|
+
if (*val == '/') *val = '\0';
|
|
169
352
|
}
|
|
353
|
+
return handler;
|
|
170
354
|
}
|
|
171
355
|
|
|
172
356
|
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
|
|
@@ -758,6 +771,22 @@ int cerver_listen(cerver_server_t* srv) {
|
|
|
758
771
|
printf("cerver: %d acceptor(s), %d connection workers, keep-alive max %d req/conn\n",
|
|
759
772
|
acceptor_count, pool_size, CERVER_KEEPALIVE_MAX);
|
|
760
773
|
|
|
774
|
+
for (int i = 0; i < srv->route_count; i++) {
|
|
775
|
+
const char* method = srv->routes[i].method;
|
|
776
|
+
const char* color = "\x1B[35m";
|
|
777
|
+
if (strcmp(method, "GET") == 0) {
|
|
778
|
+
color = "\x1B[32m";
|
|
779
|
+
} else if (strcmp(method, "POST") == 0) {
|
|
780
|
+
color = "\x1B[33m";
|
|
781
|
+
} else if (strcmp(method, "PUT") == 0) {
|
|
782
|
+
color = "\x1B[36m";
|
|
783
|
+
} else if (strcmp(method, "DELETE") == 0) {
|
|
784
|
+
color = "\x1B[31m";
|
|
785
|
+
}
|
|
786
|
+
printf(" → Mapped {%s, %s%s\x1B[0m} route\n", srv->routes[i].pattern, color, method);
|
|
787
|
+
}
|
|
788
|
+
fflush(stdout);
|
|
789
|
+
|
|
761
790
|
release_acceptors();
|
|
762
791
|
|
|
763
792
|
pthread_attr_destroy(&attr);
|
|
@@ -791,7 +820,7 @@ void cerver_shutdown(cerver_server_t* srv) {
|
|
|
791
820
|
|
|
792
821
|
if (srv->workers) {
|
|
793
822
|
for (int i = 0; i < srv->worker_count; i++) {
|
|
794
|
-
#
|
|
823
|
+
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
|
|
795
824
|
if (srv->workers[i].listen_fd != srv->sock_fd && srv->workers[i].listen_fd >= 0)
|
|
796
825
|
close(srv->workers[i].listen_fd);
|
|
797
826
|
#endif
|
|
@@ -801,6 +830,11 @@ void cerver_shutdown(cerver_server_t* srv) {
|
|
|
801
830
|
srv->workers = NULL;
|
|
802
831
|
}
|
|
803
832
|
|
|
833
|
+
if (srv->route_trie) {
|
|
834
|
+
cerver_trie_free(srv->route_trie);
|
|
835
|
+
srv->route_trie = NULL;
|
|
836
|
+
}
|
|
837
|
+
|
|
804
838
|
if (srv->sock_fd >= 0) {
|
|
805
839
|
close(srv->sock_fd);
|
|
806
840
|
srv->sock_fd = -1;
|