Skip to content

Commit 29834ed

Browse files
authored
Add JS2Wasm (#33)
1 parent c242241 commit 29834ed

30 files changed

+4371
-283
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@
1010
[submodule "c2wasm-api/clang/includes"]
1111
path = c2wasm-api/clang/includes
1212
url = https://github.com/XRPLF/hook-macros
13+
[submodule "quickjslite"]
14+
path = quickjslite
15+
url = https://github.com/RichardAH/quickjslite

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ unzip bin.zip
5656
```
5757

5858
- CD to docker folder `cd docker`
59-
- Run `make c2wasm-api && make clangd && make wasi-sdk && make hook-cleaner`
59+
- Run `make c2wasm-api && make clangd && make wasi-sdk && make hook-cleaner && make qjsc`
6060
- Run `docker-compose build`
6161
- Run `docker-compose up` or `docker-compose up -d`
6262
- This should start server at port `:9000`, the actual compiling endpoint is this: [http://localhost:9000/api/build](localhost:9000/api/build). Note that it takes a while to start.

c2wasm-api/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# API that compiles C code to WASM
1+
# API that compiles C / JS code to WASM
22

3-
This directory contains a webserver for a C to WASM compiler and
3+
This directory contains a webserver for a C / JS to WASM compiler and
44
Language Server (i.e. interactive linting). Server is built with
55
[Fastify](https://www.fastify.io/), fast and low overhead framework
66
for Node.js

c2wasm-api/src/chooks.ts

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
import fastify from 'fastify';
2+
import fs from 'fs';
3+
import { mkdirSync, writeFileSync, existsSync, openSync, closeSync, readFileSync, renameSync, rmSync, unlinkSync } from "fs";
4+
import { deflateSync } from "zlib";
5+
import { execSync } from "child_process";
6+
import { z } from 'zod';
7+
import fastifyCors from 'fastify-cors';
8+
import fastifyWebSocket from 'fastify-websocket';
9+
10+
const server = fastify();
11+
12+
server.register(fastifyCors, {
13+
// put your options here
14+
origin: '*'
15+
})
16+
server.register(fastifyWebSocket);
17+
18+
// Compilation code
19+
const llvmDir = process.cwd() + "/clang/wasi-sdk";
20+
const tempDir = "/tmp";
21+
const sysroot = llvmDir + "/share/wasi-sysroot";
22+
23+
export interface ResponseData {
24+
success: boolean;
25+
message: string;
26+
output: string;
27+
tasks: Task[];
28+
}
29+
30+
export interface Task {
31+
name: string;
32+
file?: string;
33+
success?: boolean;
34+
console?: string;
35+
output?: string;
36+
}
37+
38+
const requestBodySchema = z.object({
39+
output: z.enum(['wasm']),
40+
files: z.array(z.object({
41+
type: z.string(),
42+
name: z.string(),
43+
options: z.string().optional(),
44+
src: z.string()
45+
})),
46+
link_options: z.string().optional(),
47+
compress: z.boolean().optional(),
48+
strip: z.boolean().optional()
49+
});
50+
51+
type RequestBody = z.infer<typeof requestBodySchema>;
52+
53+
// Input: JSON in the following format
54+
// {
55+
// output: "wasm",
56+
// files: [
57+
// {
58+
// type: "c",
59+
// name: "file.c",
60+
// options: "-O3 -std=c99",
61+
// src: "#include..."
62+
// }
63+
// ],
64+
// link_options: "--import-memory"
65+
// }
66+
// Output: JSON in the following format
67+
// {
68+
// success: true,
69+
// message: "Success",
70+
// output: "AGFzbQE.... =",
71+
// tasks: [
72+
// {
73+
// name: "building wasm",
74+
// success: true,
75+
// console: ""
76+
// }
77+
// ]
78+
// }
79+
80+
function sanitize_shell_output<T>(out: T): T {
81+
return out; // FIXME
82+
}
83+
84+
function shell_exec(cmd: string, cwd: string) {
85+
const out = openSync(cwd + '/out.log', 'w');
86+
let error = '';
87+
try {
88+
execSync(cmd, { cwd, stdio: [null, out, out], });
89+
} catch (ex: unknown) {
90+
if (ex instanceof Error) {
91+
error = ex?.message;
92+
}
93+
} finally {
94+
closeSync(out);
95+
}
96+
const result = readFileSync(cwd + '/out.log').toString() || error;
97+
return result;
98+
}
99+
100+
function get_optimization_options(options: string) {
101+
const optimization_options = [
102+
/* default '-O0' not included */ '-O1', '-O2', '-O3', '-O4', '-Os'
103+
];
104+
105+
let safe_options = '';
106+
for (let o of optimization_options) {
107+
if (options.includes(o)) {
108+
safe_options += ' ' + o;
109+
}
110+
}
111+
112+
return safe_options;
113+
}
114+
115+
function get_clang_options(options: string) {
116+
const clang_flags = `--sysroot=${sysroot} -xc -I/app/clang/includes -fdiagnostics-print-source-range-info -Werror=implicit-function-declaration`;
117+
const miscellaneous_options = [
118+
'-ffast-math', '-fno-inline', '-std=c99', '-std=c89'
119+
];
120+
121+
let safe_options = '';
122+
for (let o of miscellaneous_options) {
123+
if (options.includes(o)) {
124+
safe_options += ' ' + o;
125+
} else if (o.includes('-std=') && options.toLowerCase().includes(o)) {
126+
safe_options += ' ' + o;
127+
}
128+
}
129+
130+
return clang_flags + safe_options;
131+
}
132+
133+
function get_lld_options(options: string) {
134+
// --sysroot=${sysroot} is already included in compiler options
135+
const clang_flags = `--no-standard-libraries -nostartfiles -Wl,--allow-undefined,--no-entry,--export-all`;
136+
if (!options) {
137+
return clang_flags;
138+
}
139+
const available_options = ['--import-memory', '-g'];
140+
let safe_options = '';
141+
for (let o of available_options) {
142+
if (options.includes(o)) {
143+
safe_options += ' -Wl,' + o;
144+
}
145+
}
146+
return clang_flags + safe_options;
147+
}
148+
149+
function serialize_file_data(filename: string, compress: boolean) {
150+
let content = readFileSync(filename);
151+
if (compress) {
152+
content = deflateSync(content);
153+
}
154+
return content.toString("base64");
155+
}
156+
157+
function validate_filename(name: string) {
158+
if (!/^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/.test(name)) {
159+
return false;
160+
}
161+
const parts = name.split(/\//g);
162+
for (let p of parts) {
163+
if (p == '.' || p == '..') {
164+
return false;
165+
}
166+
}
167+
return parts;
168+
}
169+
170+
function link_c_files(source_files: string[], compile_options: string, link_options: string, cwd: string, output: string, result_obj: Task) {
171+
const files = source_files.join(' ');
172+
const clang = llvmDir + '/bin/clang';
173+
const cmd = clang + ' ' + get_clang_options(compile_options) + ' ' + get_lld_options(link_options) + ' ' + files + ' -o ' + output;
174+
const out = shell_exec(cmd, cwd);
175+
result_obj.console = sanitize_shell_output(out);
176+
if (!existsSync(output)) {
177+
result_obj.success = false;
178+
return false;
179+
}
180+
result_obj.success = true;
181+
return true;
182+
}
183+
184+
function optimize_wasm(cwd: string, inplace: string, opt_options: string, result_obj: Task) {
185+
const unopt = cwd + '/unopt.wasm';
186+
const cmd = 'wasm-opt ' + opt_options + ' -o ' + inplace + ' ' + unopt;
187+
const out = openSync(cwd + '/opt.log', 'w');
188+
let error = '';
189+
let success = true;
190+
try {
191+
renameSync(inplace, unopt);
192+
execSync(cmd, { cwd, stdio: [null, out, out], });
193+
} catch (ex: unknown) {
194+
success = false;
195+
if (ex instanceof Error) {
196+
error = ex?.message;
197+
}
198+
} finally {
199+
closeSync(out);
200+
}
201+
const out_msg = readFileSync(cwd + '/opt.log').toString() || error;
202+
result_obj.console = sanitize_shell_output(out_msg);
203+
result_obj.success = success;
204+
return success;
205+
}
206+
207+
function clean_wasm(cwd: string, inplace: string, result_obj: Task) {
208+
const cmd = 'hook-cleaner ' + inplace;
209+
const out = openSync(cwd + '/cleanout.log', 'w');
210+
let error = '';
211+
let success = true;
212+
try {
213+
execSync(cmd, { cwd, stdio: [null, out, out], });
214+
} catch (ex: unknown) {
215+
success = false;
216+
if (ex instanceof Error) {
217+
error = ex?.message;
218+
}
219+
} finally {
220+
closeSync(out);
221+
}
222+
const out_msg = readFileSync(cwd + '/cleanout.log').toString() || error;
223+
result_obj.console = sanitize_shell_output(out_msg);
224+
result_obj.success = success;
225+
return success;
226+
}
227+
228+
export function build_project(project: RequestBody, base: string) {
229+
const output = project.output;
230+
const compress = project.compress;
231+
const strip = project.strip;
232+
let build_result: ResponseData = {
233+
success: false,
234+
message: '',
235+
output: '',
236+
tasks: [],
237+
};
238+
const dir = base + '.$';
239+
const result = base + '.wasm';
240+
241+
const complete = (success: boolean, message: string) => {
242+
rmSync(dir, { recursive: true });
243+
if (existsSync(result)) {
244+
unlinkSync(result);
245+
}
246+
247+
build_result.success = success;
248+
build_result.message = message;
249+
return build_result;
250+
};
251+
252+
if (output != 'wasm') {
253+
return complete(false, 'Invalid output type ' + output);
254+
}
255+
256+
build_result.tasks = [];
257+
const files = project.files;
258+
if (!files.length) {
259+
return complete(false, 'No source files');
260+
}
261+
262+
if (!existsSync(dir)) {
263+
mkdirSync(dir);
264+
}
265+
266+
const sources = [];
267+
let options;
268+
for (let file of files) {
269+
const name = file.name;
270+
if (!validate_filename(name)) {
271+
return complete(false, 'Invalid filename ' + name);
272+
}
273+
const fileName = dir + '/' + name;
274+
sources.push(fileName);
275+
if (!options) {
276+
options = file.options;
277+
} else {
278+
if (file.options && (file.options != options)) {
279+
return complete(false, 'Per-file compilation options not supported');
280+
}
281+
}
282+
283+
const src = file.src;
284+
if (!src) {
285+
return complete(false, 'Source file ' + name + ' is empty');
286+
}
287+
288+
writeFileSync(fileName, src);
289+
}
290+
const link_options = project.link_options;
291+
const link_result_obj = {
292+
name: 'building wasm'
293+
};
294+
build_result.tasks.push(link_result_obj);
295+
if (!link_c_files(sources, options || '', link_options || '', dir, result, link_result_obj)) {
296+
return complete(false, 'Build error');
297+
}
298+
299+
const opt_options = get_optimization_options(options || '');
300+
if (opt_options) {
301+
const opt_obj = {
302+
name: 'optimizing wasm'
303+
};
304+
build_result.tasks.push(opt_obj);
305+
if (!optimize_wasm(dir, result, opt_options, opt_obj)) {
306+
return complete(false, 'Optimization error');
307+
}
308+
}
309+
310+
if (strip) {
311+
const clean_obj = {
312+
name: 'cleaning wasm'
313+
};
314+
build_result.tasks.push(clean_obj);
315+
if (!clean_wasm(dir, result, clean_obj)) {
316+
return complete(false, 'Post-build error');
317+
}
318+
}
319+
320+
build_result.output = serialize_file_data(result, compress || false);
321+
322+
return complete(true, 'Success');
323+
}
324+
// END Compile code

0 commit comments

Comments
 (0)