@live-codes/odin-wasm 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +180 -2
- package/THIRD-PARTY-NOTICES.md +41 -0
- package/assets/odin-src.pack.gz +0 -0
- package/assets/odin.wasm +0 -0
- package/bin/copy-assets.mjs +47 -0
- package/dist/odin-wasm.global.js +6 -0
- package/package.json +49 -4
- package/scripts/build-iife.mjs +33 -0
- package/scripts/check-package.mjs +43 -0
- package/scripts/sync-assets.mjs +55 -0
- package/src/api.js +117 -0
- package/src/asset-receipts.js +12 -0
- package/src/assets.js +149 -0
- package/src/engine.js +198 -0
- package/src/host-js.js +112 -0
- package/src/index.js +9 -0
- package/src/index.node.js +9 -0
- package/src/pack.js +40 -0
- package/src/packaged.node.js +13 -0
- package/src/runtime.js +40 -0
- package/src/wasi-host.js +564 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Receipts for the assets this package ships in `assets/`, keyed by their path under it.
|
|
2
|
+
//
|
|
3
|
+
// Generated by scripts/sync-assets.mjs — do not edit by hand.
|
|
4
|
+
//
|
|
5
|
+
// Every read is checked against one of these before the bytes are used, so a truncated download or
|
|
6
|
+
// a host serving something else is an error here rather than a wasm instantiation failure a step
|
|
7
|
+
// later.
|
|
8
|
+
|
|
9
|
+
export const ASSET_RECEIPTS = Object.freeze({
|
|
10
|
+
'odin.wasm': Object.freeze({ bytes: 3763439, sha256: '5044e383aa2415927cd518a4d3e6fdbcc6c99154ac90d212e154f18c092deef3' }),
|
|
11
|
+
'odin-src.pack.gz': Object.freeze({ bytes: 2502169, sha256: '567b1c0c1b50d3e557d119927ade3373dda91364acae3302f2cf01950a8dd8f9' })
|
|
12
|
+
});
|
package/src/assets.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Where the assets come from, and how they are read.
|
|
2
|
+
//
|
|
3
|
+
// Two sources behind one shape, because everything downstream only needs "give
|
|
4
|
+
// me this asset, verified":
|
|
5
|
+
//
|
|
6
|
+
// hosted - a base URL. The page fetches from a server you control.
|
|
7
|
+
// packaged - the files that ship inside this package, on disk. Only reachable
|
|
8
|
+
// in Node, which is the only place there is a filesystem.
|
|
9
|
+
//
|
|
10
|
+
// Every asset is checked against a pinned SHA-256 receipt before it is used, so
|
|
11
|
+
// a truncated download or a host serving something else is an error here rather
|
|
12
|
+
// than a wasm instantiation failure a step later.
|
|
13
|
+
|
|
14
|
+
import { ASSET_RECEIPTS } from './asset-receipts.js';
|
|
15
|
+
|
|
16
|
+
/** The assets the package carries, in the order they are loaded. */
|
|
17
|
+
export const ASSET_NAMES = Object.freeze(Object.keys(ASSET_RECEIPTS));
|
|
18
|
+
|
|
19
|
+
const BASE_URL_REQUIRED =
|
|
20
|
+
'baseUrl is required here. The assets that ship in this package can only be read where there is a ' +
|
|
21
|
+
'filesystem, and a browser cannot reach a file inside an npm package — copy them somewhere your ' +
|
|
22
|
+
'page can fetch with `npx --package @live-codes/odin-wasm odin-wasm-copy-assets <dir>` and pass ' +
|
|
23
|
+
'that directory as baseUrl.';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} options - the options `createCompiler` was given
|
|
27
|
+
* @param {object|null} packaged - the packaged-asset reader, or null where there is no filesystem
|
|
28
|
+
*/
|
|
29
|
+
export function resolveAssetSource(options, packaged) {
|
|
30
|
+
if (options.baseUrl != null && options.baseUrl !== '') return createHostedSource(options);
|
|
31
|
+
if (!packaged) throw new Error(BASE_URL_REQUIRED);
|
|
32
|
+
return createPackagedSource(packaged);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createHostedSource(options) {
|
|
36
|
+
const baseUrl = resolveBaseUrl(options.baseUrl);
|
|
37
|
+
return {
|
|
38
|
+
kind: 'hosted',
|
|
39
|
+
key: `hosted\u0000${baseUrl}`,
|
|
40
|
+
description: baseUrl,
|
|
41
|
+
baseUrl,
|
|
42
|
+
async readAsset(name, onProgress) {
|
|
43
|
+
const url = new URL(name, baseUrl);
|
|
44
|
+
const response = await fetch(url);
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`Failed to load the runtime asset ${url}: ${response.status}`);
|
|
47
|
+
}
|
|
48
|
+
return verifyReceipt(name, await readBody(response, onProgress));
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createPackagedSource(packaged) {
|
|
54
|
+
return {
|
|
55
|
+
kind: 'packaged',
|
|
56
|
+
key: `packaged\u0000${packaged.root.href}`,
|
|
57
|
+
description: `the assets packaged with this library (${packaged.root.href})`,
|
|
58
|
+
async readAsset(name) {
|
|
59
|
+
let bytes;
|
|
60
|
+
try {
|
|
61
|
+
bytes = await packaged.readFile(name);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Failed to read the packaged asset ${name} from ${packaged.root.href}: ${error.message}`,
|
|
65
|
+
{ cause: error }
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return verifyReceipt(name, bytes);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Absolute http(s); in a browser a relative URL resolves against the page.
|
|
74
|
+
function resolveBaseUrl(value) {
|
|
75
|
+
let url;
|
|
76
|
+
try {
|
|
77
|
+
url = value instanceof URL ? value : new URL(value, globalThis.document?.baseURI);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`baseUrl must be an absolute http(s) URL, or relative to the page in a browser: ${error.message}`,
|
|
81
|
+
{ cause: error }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`baseUrl must be an absolute http(s) URL, or relative to the page in a browser: got "${value}"`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return url.href.endsWith('/') ? url.href : `${url.href}/`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readBody(response, onProgress) {
|
|
93
|
+
if (!onProgress || !response.body) {
|
|
94
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
95
|
+
}
|
|
96
|
+
const total = Number(response.headers.get('content-length')) || 0;
|
|
97
|
+
const reader = response.body.getReader();
|
|
98
|
+
const chunks = [];
|
|
99
|
+
let loaded = 0;
|
|
100
|
+
onProgress(0, total);
|
|
101
|
+
for (;;) {
|
|
102
|
+
const { done, value } = await reader.read();
|
|
103
|
+
if (done) break;
|
|
104
|
+
chunks.push(value);
|
|
105
|
+
loaded += value.length;
|
|
106
|
+
onProgress(loaded, total);
|
|
107
|
+
}
|
|
108
|
+
const bytes = new Uint8Array(loaded);
|
|
109
|
+
let offset = 0;
|
|
110
|
+
for (const chunk of chunks) {
|
|
111
|
+
bytes.set(chunk, offset);
|
|
112
|
+
offset += chunk.length;
|
|
113
|
+
}
|
|
114
|
+
return bytes;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function verifyReceipt(name, bytes) {
|
|
118
|
+
const receipt = ASSET_RECEIPTS[name];
|
|
119
|
+
if (!receipt) throw new Error(`No pinned receipt for the runtime asset ${name}`);
|
|
120
|
+
if (bytes.byteLength !== receipt.bytes) {
|
|
121
|
+
throw new Error(`The runtime asset ${name} is ${bytes.byteLength} bytes, expected ${receipt.bytes}`);
|
|
122
|
+
}
|
|
123
|
+
const digest = await sha256Hex(bytes);
|
|
124
|
+
if (digest !== receipt.sha256) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`The runtime asset ${name} failed SHA-256 verification: expected ${receipt.sha256}, got ${digest}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return bytes;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function sha256Hex(bytes) {
|
|
133
|
+
const subtle = globalThis.crypto?.subtle;
|
|
134
|
+
if (!subtle) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
'Verifying the runtime assets needs crypto.subtle: a secure context in the browser, or Node 20 and later.'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const digest = await subtle.digest('SHA-256', bytes);
|
|
140
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function inflateGzip(bytes) {
|
|
144
|
+
if (typeof DecompressionStream !== 'function') {
|
|
145
|
+
throw new Error('Inflating the standard library pack needs DecompressionStream');
|
|
146
|
+
}
|
|
147
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'));
|
|
148
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
149
|
+
}
|
package/src/engine.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// The compile → run engine. It has no opinion about where the compiler module
|
|
2
|
+
// and the standard library came from; `assets.js` reads them, this runs them.
|
|
3
|
+
//
|
|
4
|
+
// The compiler is a WASI command module, so every compile is a fresh instance
|
|
5
|
+
// over a fresh in-memory file system: the standard library, the user's file,
|
|
6
|
+
// and an `out/` to write into. The module it emits then runs on one of two
|
|
7
|
+
// hosts, depending on the target it was built for.
|
|
8
|
+
|
|
9
|
+
import { WasiFileSystem, WasiExit, createWasi, runWasi } from './wasi-host.js';
|
|
10
|
+
import { createJsHost, JsExit } from './host-js.js';
|
|
11
|
+
|
|
12
|
+
// Where the compiler looks for base/, core/ and vendor/.
|
|
13
|
+
const ODIN_ROOT = '/odin';
|
|
14
|
+
const PROGRAM = '/out/main.wasm';
|
|
15
|
+
|
|
16
|
+
/** The targets the compiler can emit, and the page can run. */
|
|
17
|
+
export const TARGETS = ['wasi_wasm32', 'js_wasm32'];
|
|
18
|
+
|
|
19
|
+
const encoder = new TextEncoder();
|
|
20
|
+
|
|
21
|
+
const capture = () => {
|
|
22
|
+
const stdoutParts = [];
|
|
23
|
+
const stderrParts = [];
|
|
24
|
+
const order = [];
|
|
25
|
+
return {
|
|
26
|
+
stdout: (text) => {
|
|
27
|
+
stdoutParts.push(text);
|
|
28
|
+
order.push(text);
|
|
29
|
+
},
|
|
30
|
+
stderr: (text) => {
|
|
31
|
+
stderrParts.push(text);
|
|
32
|
+
order.push(text);
|
|
33
|
+
},
|
|
34
|
+
stdoutText: () => stdoutParts.join(''),
|
|
35
|
+
stderrText: () => stderrParts.join(''),
|
|
36
|
+
outputText: () => order.join(''),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** The lines a caller reads as diagnostics, with the empty ones dropped. */
|
|
41
|
+
const diagnosticLines = (text) =>
|
|
42
|
+
text
|
|
43
|
+
.split('\n')
|
|
44
|
+
.map((line) => line.trimEnd())
|
|
45
|
+
.filter((line) => line.trim() !== '');
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {object} assets
|
|
49
|
+
* @param {WebAssembly.Module} assets.compilerModule - the Odin compiler
|
|
50
|
+
* @param {[string, Uint8Array][]} assets.libraryEntries - the packed standard library
|
|
51
|
+
*/
|
|
52
|
+
export function createEngine({ compilerModule, libraryEntries }) {
|
|
53
|
+
/** A fresh file system with the standard library under /odin. */
|
|
54
|
+
const libraryFs = () => {
|
|
55
|
+
const fs = new WasiFileSystem();
|
|
56
|
+
for (const [path, data] of libraryEntries) fs.writeFile(`${ODIN_ROOT}/${path}`, data);
|
|
57
|
+
// The compiler registers its collections (base, core, vendor) from
|
|
58
|
+
// directories and requires all three to exist; the pack ships no vendor
|
|
59
|
+
// packages, so the directory is empty.
|
|
60
|
+
fs.mkdirAll(`${ODIN_ROOT}/vendor`);
|
|
61
|
+
return fs;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const compile = ({ code, target, compileArgs, fileName }) => {
|
|
65
|
+
const fs = libraryFs();
|
|
66
|
+
fs.writeFile(`/src/${fileName}`, encoder.encode(code));
|
|
67
|
+
fs.mkdirAll('/out');
|
|
68
|
+
|
|
69
|
+
const compilerOut = [];
|
|
70
|
+
const compilerErr = [];
|
|
71
|
+
const args = [
|
|
72
|
+
'odin',
|
|
73
|
+
'build',
|
|
74
|
+
'/src',
|
|
75
|
+
`-out:${PROGRAM}`,
|
|
76
|
+
'-backend:wasm',
|
|
77
|
+
`-target:${target}`,
|
|
78
|
+
...compileArgs,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
let exitCode = 1;
|
|
82
|
+
let crash = '';
|
|
83
|
+
try {
|
|
84
|
+
exitCode = runWasi(compilerModule, createWasi(fs, {
|
|
85
|
+
args,
|
|
86
|
+
env: [`ODIN_ROOT=${ODIN_ROOT}`],
|
|
87
|
+
stdout: (text) => compilerOut.push(text),
|
|
88
|
+
stderr: (text) => compilerErr.push(text),
|
|
89
|
+
}));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
crash = error?.message ?? String(error);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
exitCode,
|
|
96
|
+
artifact: exitCode === 0 ? fs.readFile(PROGRAM) : null,
|
|
97
|
+
diagnostics: diagnosticLines(compilerOut.join('') + compilerErr.join('')),
|
|
98
|
+
crash,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const runWasiTarget = (artifact, { input, args }) => {
|
|
103
|
+
const captured = capture();
|
|
104
|
+
let exitCode = 0;
|
|
105
|
+
try {
|
|
106
|
+
exitCode = runWasi(new WebAssembly.Module(artifact.slice()), createWasi(new WasiFileSystem(), {
|
|
107
|
+
args: ['main.wasm', ...args],
|
|
108
|
+
stdin: input,
|
|
109
|
+
stdout: captured.stdout,
|
|
110
|
+
stderr: captured.stderr,
|
|
111
|
+
}));
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error instanceof WasiExit) exitCode = error.code;
|
|
114
|
+
else {
|
|
115
|
+
captured.stderr(`The program trapped: ${error?.message ?? error}\n`);
|
|
116
|
+
exitCode = 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { exitCode, ...capturedText(captured) };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const runJsTarget = async (artifact, { input, args }) => {
|
|
123
|
+
const captured = capture();
|
|
124
|
+
const host = createJsHost({ input, args: ['main.wasm', ...args], stdout: captured.stdout, stderr: captured.stderr });
|
|
125
|
+
let exitCode = 0;
|
|
126
|
+
try {
|
|
127
|
+
const { instance } = await WebAssembly.instantiate(artifact.slice(), host.imports);
|
|
128
|
+
host.setMemory(instance.exports.memory);
|
|
129
|
+
if (instance.exports._start) instance.exports._start();
|
|
130
|
+
if (instance.exports._end) instance.exports._end();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error instanceof JsExit) exitCode = error.code;
|
|
133
|
+
else {
|
|
134
|
+
captured.stderr(`The program trapped: ${error?.message ?? error}\n`);
|
|
135
|
+
exitCode = 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { exitCode, ...capturedText(captured) };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Compile and run one program.
|
|
143
|
+
*
|
|
144
|
+
* @param {object} params
|
|
145
|
+
* @param {string} params.code
|
|
146
|
+
* @param {string|Uint8Array} params.input
|
|
147
|
+
* @param {string} params.target
|
|
148
|
+
* @param {string[]} params.args
|
|
149
|
+
* @param {string[]} params.compileArgs
|
|
150
|
+
* @param {string} params.fileName
|
|
151
|
+
*/
|
|
152
|
+
const compileAndRun = async ({ code, input = '', target, args = [], compileArgs = [], fileName = 'main.odin' }) => {
|
|
153
|
+
const compileStarted = now();
|
|
154
|
+
const compiled = compile({ code, target, compileArgs: [...compileArgs], fileName });
|
|
155
|
+
const compileMs = Math.round(now() - compileStarted);
|
|
156
|
+
|
|
157
|
+
if (compiled.artifact === null || compiled.artifact.length === 0) {
|
|
158
|
+
const errors = compiled.crash
|
|
159
|
+
? [compiled.crash]
|
|
160
|
+
: compiled.diagnostics.length > 0
|
|
161
|
+
? compiled.diagnostics
|
|
162
|
+
: [`The compiler produced no module (exit code ${compiled.exitCode}).`];
|
|
163
|
+
return { stdout: '', stderr: '', output: '', errors, exitCode: null, compileMs, runMs: null };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const runStarted = now();
|
|
167
|
+
let ran;
|
|
168
|
+
try {
|
|
169
|
+
ran = target === 'js_wasm32'
|
|
170
|
+
? await runJsTarget(compiled.artifact, { input, args })
|
|
171
|
+
: runWasiTarget(compiled.artifact, { input, args });
|
|
172
|
+
} catch (error) {
|
|
173
|
+
ran = { exitCode: 1, stdout: '', stderr: `The program could not run: ${error?.message ?? error}\n`, output: '', };
|
|
174
|
+
ran.output = ran.stderr;
|
|
175
|
+
}
|
|
176
|
+
const runMs = Math.round(now() - runStarted);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
stdout: ran.stdout,
|
|
180
|
+
stderr: ran.stderr,
|
|
181
|
+
output: ran.output,
|
|
182
|
+
errors: [],
|
|
183
|
+
exitCode: ran.exitCode,
|
|
184
|
+
compileMs,
|
|
185
|
+
runMs,
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
return { compileAndRun };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const capturedText = (captured) => ({
|
|
193
|
+
stdout: captured.stdoutText(),
|
|
194
|
+
stderr: captured.stderrText(),
|
|
195
|
+
output: captured.outputText(),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const now = () => (globalThis.performance?.now?.() ?? Date.now());
|
package/src/host-js.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// The host a `js_wasm32` program runs on, outside a page.
|
|
2
|
+
//
|
|
3
|
+
// A js_wasm32 module imports `odin_env` (the console, stdin, `evaluate`, the
|
|
4
|
+
// math intrinsics), `odin_dom` (elements and events), `webgl` and `webgl2`.
|
|
5
|
+
// Only `odin_env` can do anything without a DOM, so the other three are stubs
|
|
6
|
+
// that accept any call and return 0 — a program that reaches for the DOM runs
|
|
7
|
+
// and does nothing rather than failing to instantiate. A page that wants the
|
|
8
|
+
// DOM and the canvas supplies its own host; see the repository's page, which
|
|
9
|
+
// uses Odin's own `odin.js`.
|
|
10
|
+
|
|
11
|
+
/** Thrown by `trap`/`abort`, so an exit is not confused with a real crash. */
|
|
12
|
+
export class JsExit extends Error {
|
|
13
|
+
constructor(code) {
|
|
14
|
+
super(`The program exited (code ${code}).`);
|
|
15
|
+
this.name = 'JsExit';
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const encoder = new TextEncoder();
|
|
21
|
+
const decoder = new TextDecoder();
|
|
22
|
+
|
|
23
|
+
// Any import the host does not define resolves to a no-op returning 0, so a
|
|
24
|
+
// module written against a different set of host functions still instantiates.
|
|
25
|
+
const withFallback = (target) =>
|
|
26
|
+
new Proxy(target, {
|
|
27
|
+
get: (object, property) =>
|
|
28
|
+
typeof property === 'string' && !(property in object) ? () => 0 : object[property],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} options
|
|
33
|
+
* @param {string|Uint8Array} [options.input] - the program's stdin, handed out once
|
|
34
|
+
* @param {string[]} [options.args] - argv, including argv[0]
|
|
35
|
+
* @param {(text: string) => void} [options.stdout]
|
|
36
|
+
* @param {(text: string) => void} [options.stderr]
|
|
37
|
+
*/
|
|
38
|
+
export function createJsHost({ input = '', args = [], stdout = () => {}, stderr = () => {} } = {}) {
|
|
39
|
+
const inputBytes = typeof input === 'string' ? encoder.encode(input) : new Uint8Array(input);
|
|
40
|
+
let inputOffset = 0;
|
|
41
|
+
|
|
42
|
+
let memory = null;
|
|
43
|
+
const bytes = () => new Uint8Array(memory.buffer);
|
|
44
|
+
const readString = (ptr, len) => decoder.decode(bytes().subarray(ptr, ptr + len));
|
|
45
|
+
|
|
46
|
+
// Any call to a DOM or WebGL import is accepted and returns 0.
|
|
47
|
+
const inertNamespace = new Proxy(
|
|
48
|
+
{},
|
|
49
|
+
{ get: (_target, property) => (typeof property === 'string' ? () => 0 : undefined) }
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const odin_env = {
|
|
53
|
+
write(fd, ptr, len) {
|
|
54
|
+
const text = readString(ptr, len);
|
|
55
|
+
(fd === 2 ? stderr : stdout)(text);
|
|
56
|
+
},
|
|
57
|
+
// Standard input, for the standard library's js streams — see the
|
|
58
|
+
// repository's patches/core/os/file_js.odin, which is in the shipped pack.
|
|
59
|
+
read(fd, ptr, len) {
|
|
60
|
+
if (fd !== 0) return 0;
|
|
61
|
+
const count = Math.max(0, Math.min(len, inputBytes.length - inputOffset));
|
|
62
|
+
if (count > 0) {
|
|
63
|
+
bytes().set(inputBytes.subarray(inputOffset, inputOffset + count), ptr);
|
|
64
|
+
inputOffset += count;
|
|
65
|
+
}
|
|
66
|
+
return count;
|
|
67
|
+
},
|
|
68
|
+
trap() {
|
|
69
|
+
throw new JsExit(1);
|
|
70
|
+
},
|
|
71
|
+
abort() {
|
|
72
|
+
throw new JsExit(1);
|
|
73
|
+
},
|
|
74
|
+
alert() {},
|
|
75
|
+
// The one interop call that works without a DOM: it evaluates in the global
|
|
76
|
+
// scope, so a page that embeds this can draw through it.
|
|
77
|
+
evaluate(ptr, len) {
|
|
78
|
+
const source = readString(ptr, len);
|
|
79
|
+
(0, globalThis.eval)(source);
|
|
80
|
+
},
|
|
81
|
+
open() {},
|
|
82
|
+
time_now: () => BigInt(Date.now()),
|
|
83
|
+
tick_now: () => globalThis.performance?.now?.() ?? Date.now(),
|
|
84
|
+
time_sleep() {},
|
|
85
|
+
sqrt: Math.sqrt,
|
|
86
|
+
sin: Math.sin,
|
|
87
|
+
cos: Math.cos,
|
|
88
|
+
pow: Math.pow,
|
|
89
|
+
fmuladd: (x, y, z) => x * y + z,
|
|
90
|
+
ln: Math.log,
|
|
91
|
+
exp: Math.exp,
|
|
92
|
+
ldexp: (x, exponent) => x * 2 ** exponent,
|
|
93
|
+
rand_bytes(ptr, len) {
|
|
94
|
+
bytes().set(globalThis.crypto.getRandomValues(new Uint8Array(len)), ptr);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
imports: {
|
|
100
|
+
env: withFallback({}),
|
|
101
|
+
odin_env: withFallback(odin_env),
|
|
102
|
+
odin_dom: inertNamespace,
|
|
103
|
+
webgl: inertNamespace,
|
|
104
|
+
webgl2: inertNamespace,
|
|
105
|
+
},
|
|
106
|
+
/** The module exports its own memory, so it is set after instantiation. */
|
|
107
|
+
setMemory(mem) {
|
|
108
|
+
memory = mem;
|
|
109
|
+
},
|
|
110
|
+
args,
|
|
111
|
+
};
|
|
112
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The entry every environment gets unless something more specific matches, so
|
|
2
|
+
// it has to work without a filesystem: no `node:fs` here, and `baseUrl` is
|
|
3
|
+
// required because a browser cannot read a file that lives inside an npm package.
|
|
4
|
+
import { createApi } from './api.js';
|
|
5
|
+
|
|
6
|
+
const api = createApi({ packaged: null });
|
|
7
|
+
|
|
8
|
+
export const createCompiler = api.createCompiler;
|
|
9
|
+
export const { TARGETS } = api;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Node, where the assets that ship in this package can be read off disk, so
|
|
2
|
+
// `baseUrl` becomes optional and `createCompiler()` is enough on its own.
|
|
3
|
+
import { createApi } from './api.js';
|
|
4
|
+
import { packagedAssets } from './packaged.node.js';
|
|
5
|
+
|
|
6
|
+
const api = createApi({ packaged: packagedAssets });
|
|
7
|
+
|
|
8
|
+
export const createCompiler = api.createCompiler;
|
|
9
|
+
export const { TARGETS } = api;
|
package/src/pack.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The packed standard library: Odin's `base/` and `core/`, in the container the
|
|
2
|
+
// repository's packer writes.
|
|
3
|
+
//
|
|
4
|
+
// The compiler reads its standard library from files, so a page must give it
|
|
5
|
+
// one. A flat, self-describing container is smaller than a tar (no per-entry
|
|
6
|
+
// headers) and inflates with the one decompressor a browser and Node both have.
|
|
7
|
+
//
|
|
8
|
+
// Format (before gzip):
|
|
9
|
+
// "ODINPOC1" 8 bytes
|
|
10
|
+
// count u32 little-endian
|
|
11
|
+
// count × { pathLen:u32, path:utf8, dataLen:u32, data:bytes }
|
|
12
|
+
|
|
13
|
+
export const PACK_MAGIC = 'ODINPOC1';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {ArrayBuffer|Uint8Array} buffer - the inflated container
|
|
17
|
+
* @returns {[string, Uint8Array][]} `base/…`, `core/…` and their bytes
|
|
18
|
+
*/
|
|
19
|
+
export function unpackLibrary(buffer) {
|
|
20
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
21
|
+
if (new TextDecoder().decode(bytes.subarray(0, 8)) !== PACK_MAGIC) {
|
|
22
|
+
throw new Error(`The standard library pack is not a ${PACK_MAGIC} container.`);
|
|
23
|
+
}
|
|
24
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
25
|
+
const decoder = new TextDecoder();
|
|
26
|
+
const count = view.getUint32(8, true);
|
|
27
|
+
const entries = [];
|
|
28
|
+
let pos = 12;
|
|
29
|
+
for (let i = 0; i < count; i++) {
|
|
30
|
+
const pathLen = view.getUint32(pos, true);
|
|
31
|
+
pos += 4;
|
|
32
|
+
const path = decoder.decode(bytes.subarray(pos, pos + pathLen));
|
|
33
|
+
pos += pathLen;
|
|
34
|
+
const dataLen = view.getUint32(pos, true);
|
|
35
|
+
pos += 4;
|
|
36
|
+
entries.push([path, bytes.subarray(pos, pos + dataLen)]);
|
|
37
|
+
pos += dataLen;
|
|
38
|
+
}
|
|
39
|
+
return entries;
|
|
40
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The Node half of the packaged-asset source: reading the files that ship in
|
|
2
|
+
// this package.
|
|
3
|
+
//
|
|
4
|
+
// This module is only reachable through the `node` condition in package.json,
|
|
5
|
+
// so a browser bundle never includes it and never trips over `node:fs`.
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
|
|
8
|
+
export const PACKAGED_ROOT = new URL('../assets/', import.meta.url);
|
|
9
|
+
|
|
10
|
+
export const packagedAssets = {
|
|
11
|
+
root: PACKAGED_ROOT,
|
|
12
|
+
readFile: async (name) => new Uint8Array(await readFile(new URL(name, PACKAGED_ROOT))),
|
|
13
|
+
};
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The engines, one per asset source.
|
|
2
|
+
//
|
|
3
|
+
// A source is keyed by where its bytes came from, so creating two compilers
|
|
4
|
+
// against the same `baseUrl` — or the same package — compiles the compiler
|
|
5
|
+
// module and unpacks the standard library once. Every compiler holds a
|
|
6
|
+
// reference; the entry is dropped when the last one is disposed.
|
|
7
|
+
|
|
8
|
+
import { inflateGzip } from './assets.js';
|
|
9
|
+
import { createEngine } from './engine.js';
|
|
10
|
+
import { unpackLibrary } from './pack.js';
|
|
11
|
+
|
|
12
|
+
const engines = new Map();
|
|
13
|
+
|
|
14
|
+
async function loadEngine(source, onProgress) {
|
|
15
|
+
const report = (name) => (loaded, total) => onProgress?.({ name, loaded, total });
|
|
16
|
+
const wasmBytes = await source.readAsset('odin.wasm', report('odin.wasm'));
|
|
17
|
+
const packBytes = await source.readAsset('odin-src.pack.gz', report('odin-src.pack.gz'));
|
|
18
|
+
const compilerModule = await WebAssembly.compile(wasmBytes);
|
|
19
|
+
const libraryEntries = unpackLibrary(await inflateGzip(packBytes));
|
|
20
|
+
return createEngine({ compilerModule, libraryEntries });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @returns {{ promise: Promise<object>, refs: number, key: string }} */
|
|
24
|
+
export function acquireEngine(source, onProgress) {
|
|
25
|
+
let entry = engines.get(source.key);
|
|
26
|
+
if (entry === undefined) {
|
|
27
|
+
entry = { key: source.key, refs: 0, promise: loadEngine(source, onProgress) };
|
|
28
|
+
engines.set(source.key, entry);
|
|
29
|
+
// A failed load must not be cached, or every later attempt reports the same
|
|
30
|
+
// stale failure.
|
|
31
|
+
entry.promise.catch(() => engines.delete(source.key));
|
|
32
|
+
}
|
|
33
|
+
entry.refs += 1;
|
|
34
|
+
return entry;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function releaseEngine(entry) {
|
|
38
|
+
entry.refs -= 1;
|
|
39
|
+
if (entry.refs <= 0) engines.delete(entry.key);
|
|
40
|
+
}
|