virtual-machine 0.0.0-rc1
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/.yarn/install-state.gz +0 -0
- package/.yarnrc.yml +1 -0
- package/Cargo.toml +48 -0
- package/build/cli.js +904 -0
- package/build/index.d.ts +127 -0
- package/build/index.js +723 -0
- package/build/index.mjs +17 -0
- package/build/riscv_vm-CYH5SWIJ.mjs +672 -0
- package/build.sh +26 -0
- package/cli.ts +251 -0
- package/index.ts +15 -0
- package/package.json +30 -0
- package/src/bus.rs +558 -0
- package/src/clint.rs +132 -0
- package/src/console.rs +83 -0
- package/src/cpu.rs +1913 -0
- package/src/csr.rs +67 -0
- package/src/decoder.rs +789 -0
- package/src/dram.rs +146 -0
- package/src/emulator.rs +603 -0
- package/src/lib.rs +270 -0
- package/src/main.rs +363 -0
- package/src/mmu.rs +331 -0
- package/src/net.rs +121 -0
- package/src/net_tap.rs +164 -0
- package/src/net_webtransport.rs +446 -0
- package/src/net_ws.rs +396 -0
- package/src/plic.rs +261 -0
- package/src/uart.rs +233 -0
- package/src/virtio.rs +1074 -0
- package/tsconfig.json +19 -0
- package/tsup/index.ts +80 -0
- package/tsup/tsup.cli.ts +8 -0
- package/tsup/tsup.core.cjs.ts +7 -0
- package/tsup/tsup.core.esm.ts +8 -0
package/cli.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
/**
|
|
4
|
+
* RISC-V VM CLI
|
|
5
|
+
*
|
|
6
|
+
* This CLI mirrors the browser VM wiring in `useVM`:
|
|
7
|
+
* - loads a kernel image (ELF or raw binary)
|
|
8
|
+
* - optionally loads a VirtIO block disk image (e.g. xv6 `fs.img`)
|
|
9
|
+
* - can optionally connect to a network relay (WebTransport/WebSocket)
|
|
10
|
+
* - runs the VM in a tight loop
|
|
11
|
+
* - connects stdin → UART input and UART output → stdout
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import yargs from 'yargs';
|
|
17
|
+
import { hideBin } from 'yargs/helpers';
|
|
18
|
+
|
|
19
|
+
// Default relay server URL and cert hash, mirroring the React hook.
|
|
20
|
+
const DEFAULT_RELAY_URL =
|
|
21
|
+
process.env.RELAY_URL || 'https://localhost:4433';
|
|
22
|
+
const DEFAULT_CERT_HASH =
|
|
23
|
+
process.env.RELAY_CERT_HASH || '';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create and initialize a Wasm VM instance, mirroring the React `useVM` hook:
|
|
27
|
+
* - initializes the WASM module once via `WasmInternal`
|
|
28
|
+
* - constructs `WasmVm` with the kernel bytes
|
|
29
|
+
* - optionally attaches a VirtIO block device from a disk image
|
|
30
|
+
* - optionally connects to a network relay (WebTransport/WebSocket)
|
|
31
|
+
*/
|
|
32
|
+
async function createVm(
|
|
33
|
+
kernelPath: string,
|
|
34
|
+
diskPath?: string,
|
|
35
|
+
options?: {
|
|
36
|
+
network?: boolean;
|
|
37
|
+
relayUrl?: string;
|
|
38
|
+
certHash?: string;
|
|
39
|
+
},
|
|
40
|
+
) {
|
|
41
|
+
const resolvedKernel = path.resolve(kernelPath);
|
|
42
|
+
const kernelBuf = fs.readFileSync(resolvedKernel);
|
|
43
|
+
const kernelBytes = new Uint8Array(kernelBuf);
|
|
44
|
+
|
|
45
|
+
const { WasmInternal } = await import('./');
|
|
46
|
+
const wasm = await WasmInternal();
|
|
47
|
+
const VmCtor = wasm.WasmVm;
|
|
48
|
+
if (!VmCtor) {
|
|
49
|
+
throw new Error('WasmVm class not found in wasm module');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const vm = new VmCtor(kernelBytes);
|
|
53
|
+
|
|
54
|
+
if (diskPath) {
|
|
55
|
+
const resolvedDisk = path.resolve(diskPath);
|
|
56
|
+
const diskBuf = fs.readFileSync(resolvedDisk);
|
|
57
|
+
const diskBytes = new Uint8Array(diskBuf);
|
|
58
|
+
|
|
59
|
+
if (typeof vm.load_disk === 'function') {
|
|
60
|
+
vm.load_disk(diskBytes);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Optional network setup (pre-boot), mirroring `useVM.startVM`.
|
|
65
|
+
if (options?.network) {
|
|
66
|
+
const relayUrl = options.relayUrl || DEFAULT_RELAY_URL;
|
|
67
|
+
const certHash = options.certHash || DEFAULT_CERT_HASH || undefined;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
if (typeof (vm as any).connect_webtransport === 'function') {
|
|
71
|
+
(vm as any).connect_webtransport(relayUrl, certHash);
|
|
72
|
+
console.error(
|
|
73
|
+
`[Network] Initiating WebTransport connection to ${relayUrl}`,
|
|
74
|
+
);
|
|
75
|
+
} else if (typeof (vm as any).connect_network === 'function') {
|
|
76
|
+
(vm as any).connect_network(relayUrl);
|
|
77
|
+
console.error(
|
|
78
|
+
`[Network] Initiating WebSocket connection to ${relayUrl}`,
|
|
79
|
+
);
|
|
80
|
+
} else {
|
|
81
|
+
console.error(
|
|
82
|
+
'[Network] No network methods available on VM (rebuild WASM with networking)',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error('[Network] Pre-boot connection failed:', err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return vm;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Run the VM in a loop and wire stdin/stdout to the UART, similar to the browser loop:
|
|
95
|
+
* - executes a fixed number of instructions per tick
|
|
96
|
+
* - drains the UART output buffer and writes to stdout
|
|
97
|
+
* - feeds raw stdin bytes into the VM's UART input
|
|
98
|
+
*/
|
|
99
|
+
function runVmLoop(vm: any) {
|
|
100
|
+
let running = true;
|
|
101
|
+
|
|
102
|
+
const shutdown = (code: number) => {
|
|
103
|
+
if (!running) return;
|
|
104
|
+
running = false;
|
|
105
|
+
|
|
106
|
+
if (process.stdin.isTTY && (process.stdin as any).setRawMode) {
|
|
107
|
+
(process.stdin as any).setRawMode(false);
|
|
108
|
+
}
|
|
109
|
+
process.stdin.pause();
|
|
110
|
+
|
|
111
|
+
process.exit(code);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Handle Ctrl+C via signal as a fallback
|
|
115
|
+
process.on('SIGINT', () => {
|
|
116
|
+
shutdown(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Configure stdin → VM UART input
|
|
120
|
+
if (process.stdin.isTTY && (process.stdin as any).setRawMode) {
|
|
121
|
+
(process.stdin as any).setRawMode(true);
|
|
122
|
+
}
|
|
123
|
+
process.stdin.resume();
|
|
124
|
+
|
|
125
|
+
process.stdin.on('data', (chunk) => {
|
|
126
|
+
// In raw mode `chunk` is typically a Buffer; iterate its bytes.
|
|
127
|
+
for (const byte of chunk as any as Uint8Array) {
|
|
128
|
+
// Ctrl+C (ETX) – terminate the VM and exit
|
|
129
|
+
if (byte === 3) {
|
|
130
|
+
shutdown(0);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Map CR to LF as in the React hook
|
|
135
|
+
if (byte === 13) {
|
|
136
|
+
vm.input(10);
|
|
137
|
+
} else if (byte === 127 || byte === 8) {
|
|
138
|
+
// Backspace
|
|
139
|
+
vm.input(8);
|
|
140
|
+
} else {
|
|
141
|
+
vm.input(byte);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const INSTRUCTIONS_PER_TICK = 100_000;
|
|
147
|
+
|
|
148
|
+
const loop = () => {
|
|
149
|
+
if (!running) return;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// Execute a batch of instructions
|
|
153
|
+
for (let i = 0; i < INSTRUCTIONS_PER_TICK; i++) {
|
|
154
|
+
vm.step();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Drain UART output buffer, similar to `useVM`
|
|
158
|
+
const outChunks: string[] = [];
|
|
159
|
+
let limit = 2000;
|
|
160
|
+
let code = typeof vm.get_output === 'function' ? vm.get_output() : undefined;
|
|
161
|
+
|
|
162
|
+
while (code !== undefined && limit-- > 0) {
|
|
163
|
+
const c = Number(code);
|
|
164
|
+
|
|
165
|
+
if (c === 8) {
|
|
166
|
+
// Backspace – move cursor back, erase, move back
|
|
167
|
+
outChunks.push('\b \b');
|
|
168
|
+
} else if (c === 10 || c === 13) {
|
|
169
|
+
outChunks.push('\n');
|
|
170
|
+
} else if (c >= 32 && c <= 126) {
|
|
171
|
+
outChunks.push(String.fromCharCode(c));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
code = vm.get_output();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (outChunks.length) {
|
|
178
|
+
process.stdout.write(outChunks.join(''));
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.error('\n[VM] Error while executing:', err);
|
|
182
|
+
shutdown(1);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Schedule the next tick; run as fast as the event loop allows.
|
|
187
|
+
setImmediate(loop);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
loop();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
(yargs(hideBin(process.argv)) as any)
|
|
194
|
+
.command(
|
|
195
|
+
'run <kernel>',
|
|
196
|
+
'Runs a RISC-V kernel inside the virtual machine',
|
|
197
|
+
(y: any) =>
|
|
198
|
+
y
|
|
199
|
+
.positional('kernel', {
|
|
200
|
+
type: 'string',
|
|
201
|
+
describe: 'Path to the RISC-V kernel (ELF or raw binary)',
|
|
202
|
+
demandOption: true,
|
|
203
|
+
})
|
|
204
|
+
.option('disk', {
|
|
205
|
+
alias: 'd',
|
|
206
|
+
type: 'string',
|
|
207
|
+
describe: 'Optional path to a VirtIO block disk image (e.g. xv6 fs.img)',
|
|
208
|
+
})
|
|
209
|
+
.option('network', {
|
|
210
|
+
alias: 'n',
|
|
211
|
+
type: 'boolean',
|
|
212
|
+
describe:
|
|
213
|
+
'Enable network and connect to relay at boot (uses WebTransport/WebSocket)',
|
|
214
|
+
})
|
|
215
|
+
.option('relay-url', {
|
|
216
|
+
alias: 'r',
|
|
217
|
+
type: 'string',
|
|
218
|
+
describe: `Relay URL for WebTransport/WebSocket (default: ${DEFAULT_RELAY_URL})`,
|
|
219
|
+
})
|
|
220
|
+
.option('cert-hash', {
|
|
221
|
+
alias: 'c',
|
|
222
|
+
type: 'string',
|
|
223
|
+
describe:
|
|
224
|
+
'Optional certificate SHA-256 hash for self-signed TLS (used with WebTransport)',
|
|
225
|
+
}),
|
|
226
|
+
async (argv: any) => {
|
|
227
|
+
const kernelPath = argv.kernel as string;
|
|
228
|
+
const diskPath = (argv.disk ?? undefined) as string | undefined;
|
|
229
|
+
const relayUrlArg = (argv['relay-url'] ?? undefined) as string | undefined;
|
|
230
|
+
const certHashArg = (argv['cert-hash'] ?? undefined) as string | undefined;
|
|
231
|
+
// If user explicitly passes --network or a relay URL, enable networking.
|
|
232
|
+
const enableNetwork =
|
|
233
|
+
(argv.network as boolean | undefined) ?? !!relayUrlArg;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const vm = await createVm(kernelPath, diskPath, {
|
|
237
|
+
network: enableNetwork,
|
|
238
|
+
relayUrl: relayUrlArg,
|
|
239
|
+
certHash: certHashArg,
|
|
240
|
+
});
|
|
241
|
+
runVmLoop(vm);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error('[CLI] Failed to start VM:', err);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
.demandCommand(1, 'You need to specify a command')
|
|
249
|
+
.strict()
|
|
250
|
+
.help()
|
|
251
|
+
.parse();
|
package/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import wasmBuffer from "./pkg/riscv_vm_bg.wasm";
|
|
2
|
+
|
|
3
|
+
let loaded: typeof import("./pkg/riscv_vm") | undefined;
|
|
4
|
+
|
|
5
|
+
export async function WasmInternal() {
|
|
6
|
+
if (!loaded) {
|
|
7
|
+
const module = await import("./pkg/riscv_vm");
|
|
8
|
+
const wasmInstance = module.initSync(wasmBuffer);
|
|
9
|
+
await module.default(wasmInstance);
|
|
10
|
+
loaded = module;
|
|
11
|
+
}
|
|
12
|
+
return loaded;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "virtual-machine",
|
|
3
|
+
"version": "0.0.0-rc1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"bin": {
|
|
6
|
+
"uaito-cli": "build/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"main": "./build/index.js",
|
|
12
|
+
"module": "./build/index.mjs",
|
|
13
|
+
"types": "./build/index.d.ts",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "rm -rf build && sh build.sh"
|
|
16
|
+
},
|
|
17
|
+
"author": "elribonazo@gmail.com",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@trust0/ridb-build": "^0.0.21",
|
|
21
|
+
"tsup": "^8.4.0",
|
|
22
|
+
"typescript": "^5.8.3",
|
|
23
|
+
"yargs": "^18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"packageManager": "yarn@4.11.0+sha512.4e54aeace9141df2f0177c266b05ec50dc044638157dae128c471ba65994ac802122d7ab35bcd9e81641228b7dcf24867d28e750e0bcae8a05277d600008ad54",
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@esbuild-plugins/node-resolve": "^0.2.2",
|
|
28
|
+
"@types/node": "^24.10.1"
|
|
29
|
+
}
|
|
30
|
+
}
|