@velox0/cerver 0.3.1 → 0.4.1
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/.github/workflows/publish.yml +7 -8
- package/lib/assets/embed.js +2 -1
- package/lib/codegen/dispatch_gen.js +189 -0
- package/lib/codegen/generator.js +12 -1
- package/lib/compiler/compile.js +29 -2
- package/package.json +1 -1
- package/runtime/cerver.h +171 -100
- package/runtime/http_parser.c +156 -152
- package/runtime/http_writer.c +138 -97
- package/runtime/mime.c +48 -49
- package/runtime/router.c +121 -99
- package/runtime/server.c +662 -405
- package/runtime/static.c +185 -127
- package/templates/cerver.config.js +1 -0
- package/test/run.js +1 -1
|
@@ -2,13 +2,12 @@ name: Publish package
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
types: [published]
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
8
7
|
workflow_dispatch:
|
|
9
8
|
|
|
10
9
|
permissions:
|
|
11
|
-
contents:
|
|
10
|
+
contents: write
|
|
12
11
|
id-token: write
|
|
13
12
|
|
|
14
13
|
jobs:
|
|
@@ -18,18 +17,18 @@ jobs:
|
|
|
18
17
|
|
|
19
18
|
steps:
|
|
20
19
|
- name: Checkout
|
|
21
|
-
uses: actions/checkout@
|
|
20
|
+
uses: actions/checkout@v6
|
|
22
21
|
|
|
23
22
|
- name: Setup pnpm
|
|
24
|
-
uses: pnpm/action-setup@
|
|
23
|
+
uses: pnpm/action-setup@v6
|
|
25
24
|
with:
|
|
26
25
|
version: 10
|
|
27
26
|
run_install: false
|
|
28
27
|
|
|
29
28
|
- name: Setup Node.js
|
|
30
|
-
uses: actions/setup-node@
|
|
29
|
+
uses: actions/setup-node@v6
|
|
31
30
|
with:
|
|
32
|
-
node-version:
|
|
31
|
+
node-version: 24
|
|
33
32
|
registry-url: https://registry.npmjs.org
|
|
34
33
|
cache: pnpm
|
|
35
34
|
|
package/lib/assets/embed.js
CHANGED
|
@@ -179,7 +179,8 @@ async function generateEmbeddedAssets(assets, shouldMinify, compression) {
|
|
|
179
179
|
` { "${entry.servePath}", "${entry.mime}", ` +
|
|
180
180
|
`${entry.name}, ${entry.name}_len, ` +
|
|
181
181
|
`${entry.gzName}, ${entry.gzLen}, ` +
|
|
182
|
-
`${entry.brName}, ${entry.brLen}
|
|
182
|
+
`${entry.brName}, ${entry.brLen}, ` +
|
|
183
|
+
`NULL, 0 },`
|
|
183
184
|
);
|
|
184
185
|
}
|
|
185
186
|
lines.push("};");
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { cString, handlerName } = require("./emit");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate a compile-time dispatch function that avoids the generic router.
|
|
7
|
+
*
|
|
8
|
+
* For static routes: method check → length check → memcmp (known lengths).
|
|
9
|
+
* For dynamic routes: segment-count check → per-segment inline comparisons.
|
|
10
|
+
*
|
|
11
|
+
* Falls through to generic router for unmatched paths.
|
|
12
|
+
*
|
|
13
|
+
* @param {IRRoute[]} routes - All IR routes
|
|
14
|
+
* @returns {string} - C source for cerver_generated_dispatch()
|
|
15
|
+
*/
|
|
16
|
+
function generateDispatch(routes) {
|
|
17
|
+
const lines = [];
|
|
18
|
+
|
|
19
|
+
/* Separate static and dynamic routes */
|
|
20
|
+
const staticRoutes = routes.filter((r) => !r.urlPath.includes(":"));
|
|
21
|
+
const dynamicRoutes = routes.filter((r) => r.urlPath.includes(":"));
|
|
22
|
+
|
|
23
|
+
/* Forward declarations for handler functions */
|
|
24
|
+
lines.push("/* ---- Generated Fast Dispatch ---- */");
|
|
25
|
+
lines.push("");
|
|
26
|
+
|
|
27
|
+
/* Group static routes by method */
|
|
28
|
+
const byMethod = {};
|
|
29
|
+
for (const route of staticRoutes) {
|
|
30
|
+
if (!byMethod[route.method]) byMethod[route.method] = [];
|
|
31
|
+
byMethod[route.method].push(route);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Group dynamic routes by method */
|
|
35
|
+
const dynByMethod = {};
|
|
36
|
+
for (const route of dynamicRoutes) {
|
|
37
|
+
if (!dynByMethod[route.method]) dynByMethod[route.method] = [];
|
|
38
|
+
dynByMethod[route.method].push(route);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Generate the dispatch function */
|
|
42
|
+
lines.push(
|
|
43
|
+
"static cerver_handler_fn cerver_generated_dispatch(cerver_request_t *req) {"
|
|
44
|
+
);
|
|
45
|
+
lines.push(" const char *path = req->path;");
|
|
46
|
+
lines.push(" size_t path_len = 0;");
|
|
47
|
+
lines.push(" { const char *p = path; while (*p) { path_len++; p++; } }");
|
|
48
|
+
lines.push("");
|
|
49
|
+
|
|
50
|
+
const allMethods = new Set([
|
|
51
|
+
...Object.keys(byMethod),
|
|
52
|
+
...Object.keys(dynByMethod),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
let firstMethod = true;
|
|
56
|
+
for (const method of allMethods) {
|
|
57
|
+
const methodStatic = byMethod[method] || [];
|
|
58
|
+
const methodDynamic = dynByMethod[method] || [];
|
|
59
|
+
|
|
60
|
+
/* Method check */
|
|
61
|
+
const methodLen = method.length;
|
|
62
|
+
lines.push(
|
|
63
|
+
` ${firstMethod ? "" : "} else "}if (req->method[0] == '${method[0]}' && memcmp(req->method, ${cString(method)}, ${methodLen + 1}) == 0) {`
|
|
64
|
+
);
|
|
65
|
+
firstMethod = false;
|
|
66
|
+
|
|
67
|
+
/* ---- Static routes: group by path length for fast rejection ---- */
|
|
68
|
+
if (methodStatic.length > 0) {
|
|
69
|
+
/* Group by length */
|
|
70
|
+
const byLength = {};
|
|
71
|
+
for (const route of methodStatic) {
|
|
72
|
+
const len = route.urlPath.length;
|
|
73
|
+
if (!byLength[len]) byLength[len] = [];
|
|
74
|
+
byLength[len].push(route);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
lines.push(" /* Static routes */");
|
|
78
|
+
|
|
79
|
+
const lengths = Object.keys(byLength)
|
|
80
|
+
.map(Number)
|
|
81
|
+
.sort((a, b) => a - b);
|
|
82
|
+
|
|
83
|
+
for (const len of lengths) {
|
|
84
|
+
const routesAtLen = byLength[len];
|
|
85
|
+
|
|
86
|
+
lines.push(` if (path_len == ${len}) {`);
|
|
87
|
+
|
|
88
|
+
for (const route of routesAtLen) {
|
|
89
|
+
const name = handlerName(route.method, route.urlPath);
|
|
90
|
+
if (route.urlPath === "/") {
|
|
91
|
+
lines.push(
|
|
92
|
+
` if (path[0] == '/' && path[1] == '\\0') return ${name};`
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
lines.push(
|
|
96
|
+
` if (memcmp(path, ${cString(route.urlPath)}, ${len}) == 0) return ${name};`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
lines.push(" }");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ---- Dynamic routes: inline segment matching ---- */
|
|
106
|
+
if (methodDynamic.length > 0) {
|
|
107
|
+
lines.push("");
|
|
108
|
+
lines.push(" /* Dynamic routes */");
|
|
109
|
+
|
|
110
|
+
for (const route of methodDynamic) {
|
|
111
|
+
const segments = route.urlPath.split("/").filter(Boolean);
|
|
112
|
+
const name = handlerName(route.method, route.urlPath);
|
|
113
|
+
|
|
114
|
+
lines.push(" {");
|
|
115
|
+
lines.push(` /* ${route.urlPath} */`);
|
|
116
|
+
lines.push(` const char *p = path;`);
|
|
117
|
+
lines.push(` if (*p == '/') p++;`);
|
|
118
|
+
lines.push(` int match = 1;`);
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < segments.length; i++) {
|
|
121
|
+
const seg = segments[i];
|
|
122
|
+
const isLast = i === segments.length - 1;
|
|
123
|
+
|
|
124
|
+
if (seg.startsWith(":")) {
|
|
125
|
+
const paramName = seg.slice(1);
|
|
126
|
+
/* Dynamic segment: find the segment boundaries */
|
|
127
|
+
lines.push(` const char *seg${i}_start = p;`);
|
|
128
|
+
lines.push(` while (*p && *p != '/') p++;`);
|
|
129
|
+
lines.push(` size_t seg${i}_len = (size_t)(p - seg${i}_start);`);
|
|
130
|
+
lines.push(` if (seg${i}_len == 0) match = 0;`);
|
|
131
|
+
|
|
132
|
+
if (!isLast) {
|
|
133
|
+
lines.push(` if (match && *p == '/') p++; else if (!${isLast}) match = 0;`);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
/* Static segment: compare directly */
|
|
137
|
+
const segLen = seg.length;
|
|
138
|
+
lines.push(
|
|
139
|
+
` if (match && memcmp(p, ${cString(seg)}, ${segLen}) == 0 && (p[${segLen}] == '/' || p[${segLen}] == '\\0')) {`
|
|
140
|
+
);
|
|
141
|
+
lines.push(` p += ${segLen};`);
|
|
142
|
+
lines.push(` if (*p == '/') p++;`);
|
|
143
|
+
lines.push(` } else { match = 0; }`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Ensure path is fully consumed */
|
|
148
|
+
lines.push(` if (match && *p != '\\0') match = 0;`);
|
|
149
|
+
|
|
150
|
+
/* Extract params on match */
|
|
151
|
+
lines.push(` if (match) {`);
|
|
152
|
+
|
|
153
|
+
/* Re-iterate to set params */
|
|
154
|
+
let paramIdx = 0;
|
|
155
|
+
for (let i = 0; i < segments.length; i++) {
|
|
156
|
+
const seg = segments[i];
|
|
157
|
+
if (seg.startsWith(":")) {
|
|
158
|
+
const paramName = seg.slice(1);
|
|
159
|
+
lines.push(
|
|
160
|
+
` req->params[req->params_count].key = ${cString(paramName)};`
|
|
161
|
+
);
|
|
162
|
+
lines.push(
|
|
163
|
+
` req->params[req->params_count].value = seg${i}_start;`
|
|
164
|
+
);
|
|
165
|
+
lines.push(` req->params_count++;`);
|
|
166
|
+
paramIdx++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
lines.push(` return ${name};`);
|
|
171
|
+
lines.push(` }`);
|
|
172
|
+
lines.push(` }`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!firstMethod) {
|
|
178
|
+
lines.push(" }");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
lines.push("");
|
|
182
|
+
lines.push(" return NULL; /* No match — fall through to generic router */");
|
|
183
|
+
lines.push("}");
|
|
184
|
+
lines.push("");
|
|
185
|
+
|
|
186
|
+
return lines.join("\n");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { generateDispatch };
|
package/lib/codegen/generator.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { handlerName, emitStatement } = require("./emit");
|
|
4
4
|
const { generateRouteTable } = require("./route_table");
|
|
5
|
+
const { generateDispatch } = require("./dispatch_gen");
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Generate the complete C server source file from IR routes.
|
|
@@ -70,6 +71,11 @@ function generateServer(routes, config, assetsCode) {
|
|
|
70
71
|
lines.push("");
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/* ---- Generated fast dispatch ---- */
|
|
75
|
+
lines.push("/* ---- Generated Fast Dispatch (compile-time optimized) ---- */");
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push(generateDispatch(routes));
|
|
78
|
+
|
|
73
79
|
/* ---- Main function ---- */
|
|
74
80
|
lines.push("/* ---- Entry Point ---- */");
|
|
75
81
|
lines.push("");
|
|
@@ -81,7 +87,7 @@ function generateServer(routes, config, assetsCode) {
|
|
|
81
87
|
lines.push("");
|
|
82
88
|
lines.push(" /* Allow PORT env override */");
|
|
83
89
|
lines.push(' const char *env_port = getenv("CERVER_PORT");');
|
|
84
|
-
lines.push(
|
|
90
|
+
lines.push(' if (!env_port) env_port = getenv("PORT");');
|
|
85
91
|
lines.push(" if (env_port) port = atoi(env_port);");
|
|
86
92
|
lines.push("");
|
|
87
93
|
lines.push(" cerver_server_t srv;");
|
|
@@ -92,6 +98,11 @@ function generateServer(routes, config, assetsCode) {
|
|
|
92
98
|
);
|
|
93
99
|
lines.push("");
|
|
94
100
|
|
|
101
|
+
/* Wire up generated dispatch */
|
|
102
|
+
lines.push(" /* Use generated compile-time dispatch for fast routing */");
|
|
103
|
+
lines.push(" cerver_set_dispatch(&srv, cerver_generated_dispatch);");
|
|
104
|
+
lines.push("");
|
|
105
|
+
|
|
95
106
|
if (assetsCode) {
|
|
96
107
|
lines.push(
|
|
97
108
|
" cerver_set_assets(&srv, cerver_embedded_assets, cerver_embedded_asset_count);"
|
package/lib/compiler/compile.js
CHANGED
|
@@ -22,6 +22,20 @@ function detectCompiler() {
|
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Check if a compiler supports a given flag.
|
|
27
|
+
*/
|
|
28
|
+
function supportsFlag(cc, flag) {
|
|
29
|
+
try {
|
|
30
|
+
execSync(`echo 'int main(){}' | ${cc} ${flag} -x c -o /dev/null - 2>/dev/null`, {
|
|
31
|
+
stdio: "ignore",
|
|
32
|
+
});
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
25
39
|
/**
|
|
26
40
|
* Compile the generated C source into a binary.
|
|
27
41
|
*
|
|
@@ -46,9 +60,9 @@ function compile(distDir, runtimeDir, opts) {
|
|
|
46
60
|
.filter((f) => f.endsWith(".c"))
|
|
47
61
|
.map((f) => path.join(runtimeDir, f));
|
|
48
62
|
|
|
49
|
-
/* Build the compiler command */
|
|
63
|
+
/* Build the compiler command with aggressive optimization */
|
|
50
64
|
const args = [
|
|
51
|
-
"-
|
|
65
|
+
"-O3",
|
|
52
66
|
"-Wall",
|
|
53
67
|
"-Wextra",
|
|
54
68
|
"-Wno-unused-parameter",
|
|
@@ -60,10 +74,23 @@ function compile(distDir, runtimeDir, opts) {
|
|
|
60
74
|
"-lpthread",
|
|
61
75
|
];
|
|
62
76
|
|
|
77
|
+
/* Add LTO if supported */
|
|
78
|
+
if (supportsFlag(cc, "-flto")) {
|
|
79
|
+
args.splice(1, 0, "-flto");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Add march=native if supported (for SIMD/hardware-specific opts) */
|
|
83
|
+
if (supportsFlag(cc, "-march=native")) {
|
|
84
|
+
args.push("-march=native");
|
|
85
|
+
}
|
|
86
|
+
|
|
63
87
|
if (opts && opts.static) {
|
|
64
88
|
args.push("-static");
|
|
65
89
|
}
|
|
66
90
|
|
|
91
|
+
/* On macOS, we need to define _GNU_SOURCE for some functions */
|
|
92
|
+
args.push("-D_GNU_SOURCE");
|
|
93
|
+
|
|
67
94
|
console.log(` compiling with ${cc}...`);
|
|
68
95
|
console.log(` ${cc} ${args.join(" ")}`);
|
|
69
96
|
|