Skip to content

Commit c0c173c

Browse files
authored
Merge pull request #123 from Raiden1411/evm_wasm
wasm: make the evm interpreter compile to wasm
2 parents e210f2d + a5d819a commit c0c173c

File tree

15 files changed

+371
-91
lines changed

15 files changed

+371
-91
lines changed

build.zig

Lines changed: 73 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -233,27 +233,50 @@ pub fn build(b: *std.Build) void {
233233
zabi_utils.addImport("zabi-types", zabi_types);
234234
}
235235

236+
// Runs the tests or coverage steps.
237+
buildTestOrCoverage(b, target, optimize, zabi);
238+
239+
// Runs the benchmark
240+
buildBenchmark(b, target, optimize, zabi);
241+
242+
// Build and generate docs for zabi. Uses the `doc_comments` spread across the codebase.
243+
// Always build in `ReleaseFast`.
244+
buildDocs(b, target);
245+
246+
// Build the wasm file. Always build in `ReleaseSmall` on `wasm32-freestanding`.
247+
buildWasm(b, zabi);
248+
}
249+
250+
// Builds and runs the main tests of zabi or the coverage from kcov.
251+
fn buildTestOrCoverage(
252+
b: *std.Build,
253+
target: std.Build.ResolvedTarget,
254+
optimize: std.builtin.OptimizeMode,
255+
module: *std.Build.Module,
256+
) void {
236257
const load_variables = b.option(bool, "load_variables", "Load enviroment variables from a \"env\" file.") orelse false;
237258
const env_file_path = b.option([]const u8, "env_file_path", "Specify the location of a env variables file") orelse ".env";
238259

239260
// Builds and runs the main tests of zabi.
240-
const lib_unit_tests = b.addTest(.{
241-
.name = "zabi-tests",
242-
.root_source_file = b.path("tests/root.zig"),
243-
.target = target,
244-
.optimize = optimize,
245-
.test_runner = b.path("build/test_runner.zig"),
246-
});
247-
lib_unit_tests.root_module.addImport("zabi", zabi);
248-
addDependencies(b, &lib_unit_tests.root_module, target, optimize);
261+
{
262+
const lib_unit_tests = b.addTest(.{
263+
.name = "zabi-tests",
264+
.root_source_file = b.path("tests/root.zig"),
265+
.target = target,
266+
.optimize = optimize,
267+
.test_runner = b.path("build/test_runner.zig"),
268+
});
269+
lib_unit_tests.root_module.addImport("zabi", module);
270+
addDependencies(b, &lib_unit_tests.root_module, target, optimize);
249271

250-
var run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
272+
var run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
251273

252-
const test_step = b.step("test", "Run unit tests");
253-
test_step.dependOn(&run_lib_unit_tests.step);
274+
const test_step = b.step("test", "Run unit tests");
275+
test_step.dependOn(&run_lib_unit_tests.step);
254276

255-
if (load_variables)
256-
loadVariables(b, env_file_path, run_lib_unit_tests);
277+
if (load_variables)
278+
loadVariables(b, env_file_path, run_lib_unit_tests);
279+
}
257280

258281
// Build and run coverage test runner if `zig build coverage` was ran
259282
{
@@ -264,7 +287,7 @@ pub fn build(b: *std.Build) void {
264287
.optimize = optimize,
265288
.test_runner = b.path("build/test_runner.zig"),
266289
});
267-
coverage_lib_unit_tests.root_module.addImport("zabi", zabi);
290+
coverage_lib_unit_tests.root_module.addImport("zabi", module);
268291
const test_step_coverage = b.step("coverage", "Run unit tests with kcov coverage");
269292

270293
const kcov_collect = std.Build.Step.Run.create(b, "collect coverage");
@@ -289,16 +312,44 @@ pub fn build(b: *std.Build) void {
289312
});
290313
test_step_coverage.dependOn(&install_coverage.step);
291314
}
315+
}
292316

293-
// Runs the benchmark
294-
buildBenchmark(b, target, optimize, zabi);
317+
/// Build the wasm binary.
318+
fn buildWasm(b: *std.Build, module: *std.Build.Module) void {
319+
const wasm_crosstarget: std.Target.Query = .{
320+
.cpu_arch = .wasm32,
321+
.os_tag = .freestanding,
322+
.cpu_model = .{ .explicit = &std.Target.wasm.cpu.mvp },
323+
.cpu_features_add = std.Target.wasm.featureSet(&.{
324+
.atomics,
325+
.bulk_memory,
326+
.reference_types,
327+
.sign_ext,
328+
}),
329+
};
295330

296-
// Build and generate docs for zabi. Uses the `doc_comments` spread across the codebase.
297-
// Always build in `ReleaseFast`.
298-
buildDocs(b, target);
331+
const wasm = b.addExecutable(.{
332+
.name = "zabi_wasm",
333+
.root_source_file = b.path("src/root_wasm.zig"),
334+
.target = b.resolveTargetQuery(wasm_crosstarget),
335+
.optimize = .ReleaseSmall,
336+
});
337+
wasm.root_module.addImport("zabi", module);
299338

300-
// Build the wasm file. Always build in `ReleaseSmall` on `wasm32-freestanding.
301-
buildWasm(b);
339+
// Browser target
340+
wasm.entry = .disabled;
341+
wasm.rdynamic = true;
342+
343+
// Memory defaults.
344+
wasm.initial_memory = 65536 * 32;
345+
wasm.max_memory = 65536 * 65336;
346+
347+
wasm.root_module.stack_protector = true;
348+
349+
const wasm_install = b.addInstallArtifact(wasm, .{});
350+
const wasm_step = b.step("wasm", "Build wasm library");
351+
352+
wasm_step.dependOn(&wasm_install.step);
302353
}
303354

304355
/// Adds zabi project dependencies.
@@ -354,49 +405,6 @@ fn buildDocs(b: *std.Build, target: std.Build.ResolvedTarget) void {
354405
const docs_step = b.step("docs", "Generate documentation based on the source code.");
355406
docs_step.dependOn(&docs_run.step);
356407
}
357-
/// Builds for wasm32-freestanding target.
358-
fn buildWasm(b: *std.Build) void {
359-
const wasm_crosstarget: std.Target.Query = .{
360-
.cpu_arch = .wasm32,
361-
.os_tag = .freestanding,
362-
.cpu_model = .{ .explicit = &std.Target.wasm.cpu.mvp },
363-
.cpu_features_add = std.Target.wasm.featureSet(&.{
364-
// We use this to explicitly request shared memory.
365-
.atomics,
366-
367-
// Not explicitly used but compiler could use them if they want.
368-
.bulk_memory,
369-
.reference_types,
370-
.sign_ext,
371-
}),
372-
};
373-
374-
const wasm = b.addExecutable(.{
375-
.name = "zabi_wasm",
376-
.root_source_file = b.path("src/root_wasm.zig"),
377-
.target = b.resolveTargetQuery(wasm_crosstarget),
378-
.optimize = .ReleaseSmall,
379-
.link_libc = true,
380-
});
381-
382-
// Browser target
383-
wasm.entry = .disabled;
384-
wasm.rdynamic = true;
385-
386-
// Memory defaults.
387-
wasm.initial_memory = 65536 * 32;
388-
wasm.max_memory = 65536 * 65336;
389-
390-
wasm.root_module.stack_protector = true;
391-
392-
addDependencies(b, &wasm.root_module, b.resolveTargetQuery(wasm_crosstarget), .ReleaseSmall);
393-
394-
const wasm_install = b.addInstallArtifact(wasm, .{});
395-
const wasm_step = b.step("wasm", "Build wasm library");
396-
397-
wasm_step.dependOn(&wasm_install.step);
398-
}
399-
400408
/// Loads enviroment variables from a `.env` file in case they aren't already present.
401409
fn loadVariables(b: *std.Build, env_path: []const u8, exe: *std.Build.Step.Run) void {
402410
var file = std.fs.cwd().openFile(env_path, .{}) catch |err|

examples/wasm/index.html

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>Zig WASM Test</title>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<style type="text/css">
8+
body {
9+
font-family: system-ui, -apple-system, Roboto, "Segoe UI", sans-serif;
10+
color: #000000;
11+
}
12+
13+
.hidden {
14+
display: none;
15+
}
16+
17+
.run {
18+
display: flex;
19+
flex-direction: column;
20+
gap: 8px;
21+
}
22+
23+
.contract_code_area {
24+
display: flex;
25+
flex-direction: column;
26+
gap: 8px;
27+
}
28+
29+
label {
30+
font-size: 14px;
31+
font-weight: bold;
32+
}
33+
34+
textarea {
35+
width: 50%;
36+
padding: 10px;
37+
font-size: 16px;
38+
border: 1px solid #ccc;
39+
border-radius: 4px;
40+
resize: vertical;
41+
}
42+
43+
@media (prefers-color-scheme: dark) {
44+
body {
45+
background-color: #111;
46+
color: #bbb;
47+
}
48+
}
49+
</style>
50+
</head>
51+
<title>ZIG WASM INTERPRETER</title>
52+
<body>
53+
<h1 class="interpreter">Interpreter</h1>
54+
<div class="contract_code_area">
55+
<label for="code_label">Contract Code:</label>
56+
<textarea id="contract_code" name="message" rows="4"></textarea>
57+
<label for="calldata_label">Calldata:</label>
58+
<textarea id="calldata"></textarea>
59+
</div>
60+
<br>
61+
<button type="button" id="run">Run</button>
62+
<h2>Output:</h2>
63+
<h3 id="result"></h3>
64+
<script src="./main.js"></script>
65+
</body>
66+
</html>

examples/wasm/main.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
(function () {
2+
const contract_code = document.getElementById("contract_code");
3+
const calldata = document.getElementById("calldata");
4+
5+
const run = document.getElementById("run");
6+
run.onclick = runInterpreter;
7+
8+
let wasm_promise = fetch("./zabi_wasm.wasm");
9+
var wasm_exports = null;
10+
11+
const text_decoder = new TextDecoder();
12+
const text_encoder = new TextEncoder();
13+
14+
// Convenience function to prepare a typed byte array
15+
// from a pointer and a length into WASM memory.
16+
function getView(ptr, len) {
17+
return new Uint8Array(wasm_exports.memory.buffer, ptr, len);
18+
}
19+
20+
// JS strings are UTF-16 and have to be encoded into an
21+
// UTF-8 typed byte array in WASM memory.
22+
function encodeString(str) {
23+
const capacity = str.length * 2 + 5; // As per MDN
24+
const ptr = wasm_exports.malloc(capacity);
25+
const { written } = text_encoder.encodeInto(str, getView(ptr, capacity));
26+
return [ptr, written, capacity];
27+
}
28+
29+
// Decodes the string type produced from zig.
30+
function decodeString(ptr, len) {
31+
if (len === 0) return "";
32+
return text_decoder.decode(
33+
new Uint8Array(wasm_exports.memory.buffer, ptr, len),
34+
);
35+
}
36+
37+
// Unwraps the string type produced from zig.
38+
function unwrapHexString(bigint) {
39+
const ptr = Number(bigint & 0xffffffffn);
40+
const len = Number(bigint >> 32n);
41+
const buffer = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
42+
43+
return byteToHex(buffer);
44+
}
45+
46+
const byteToHex = (byte) => {
47+
const key = "0123456789abcdef";
48+
let bytes = new Uint8Array(byte);
49+
let newHex = "";
50+
let currentChar = 0;
51+
for (let i = 0; i < bytes.length; i++) {
52+
currentChar = bytes[i] >> 4;
53+
newHex += key[currentChar];
54+
currentChar = bytes[i] & 15;
55+
newHex += key[currentChar];
56+
}
57+
return newHex;
58+
};
59+
60+
function runInterpreter() {
61+
const code = contract_code.value;
62+
const call = calldata.value;
63+
64+
console.log(`Calldata file: ${call}`);
65+
const [ptr, len] = encodeString(code);
66+
const [pointer, length] = encodeString(call ? call : "");
67+
68+
const contract = wasm_exports.instanciateContract(
69+
pointer,
70+
length,
71+
ptr,
72+
len,
73+
);
74+
const host = wasm_exports.generateHost(wasm_exports.getPlainHost());
75+
76+
const result = wasm_exports.runCode(contract, host);
77+
const str = unwrapHexString(result);
78+
document.getElementById("result").textContent = str;
79+
80+
wasm_exports.free(ptr);
81+
wasm_exports.free(pointer);
82+
}
83+
84+
// Instantiate WASM module and run our test code.
85+
WebAssembly.instantiateStreaming(wasm_promise, {
86+
env: {
87+
// We export this function to WASM land.
88+
log: (ptr, len) => {
89+
const msg = decodeString(ptr, len);
90+
console.log(msg);
91+
},
92+
},
93+
}).then((wasm_binary) => {
94+
wasm_exports = wasm_binary.instance.exports;
95+
window.wasm = wasm_binary; // for debugging
96+
});
97+
})();

src/evm/Interpreter.zig

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ pub const InterpreterRunErrors = AllInstructionErrors || error{
4444
CreateCodeSizeLimit,
4545
};
4646

47+
/// Set of possible errors that can be returned depending on the interpreter's current state.
48+
pub const InterpreterStatusErrors = error{
49+
OpcodeNotFound,
50+
CallWithValueNotAllowedInStaticCall,
51+
InvalidInstructionOpcode,
52+
InterpreterReverted,
53+
CreateCodeSizeLimit,
54+
InvalidOffset,
55+
InvalidJump,
56+
};
57+
4758
/// The set of next interpreter actions.
4859
pub const InterpreterActions = union(enum) {
4960
/// Call action.
@@ -110,7 +121,7 @@ memory: Memory,
110121
/// The next interpreter action.
111122
next_action: InterpreterActions,
112123
/// The interpreter's counter.
113-
program_counter: u64,
124+
program_counter: usize,
114125
/// The spec for this interpreter.
115126
spec: SpecId,
116127
/// The stack of the interpreter with 1024 max size.
@@ -228,10 +239,9 @@ pub fn runInstruction(self: *Interpreter) AllInstructionErrors!void {
228239
/// const result = try interpreter.run();
229240
/// defer result.deinit(testing.allocator);
230241
/// ```
231-
pub fn run(self: *Interpreter) !InterpreterActions {
232-
while (self.status == .running) : (self.advanceProgramCounter()) {
242+
pub fn run(self: *Interpreter) (AllInstructionErrors || InterpreterStatusErrors)!InterpreterActions {
243+
while (self.status == .running) : (self.advanceProgramCounter())
233244
try self.runInstruction();
234-
}
235245

236246
// Handles the different status of the interperter after it's finished
237247
switch (self.status) {
@@ -256,7 +266,7 @@ pub fn run(self: *Interpreter) !InterpreterActions {
256266
}
257267
/// Resizes the inner memory size. Adds gas expansion cost to
258268
/// the gas tracker.
259-
pub fn resize(self: *Interpreter, new_size: u64) (Allocator.Error || GasTracker.Error || Memory.Error)!void {
269+
pub fn resize(self: *Interpreter, new_size: usize) (Allocator.Error || GasTracker.Error || Memory.Error)!void {
260270
if (new_size > self.memory.getCurrentMemorySize()) {
261271
const count = mem.availableWords(new_size);
262272
const mem_cost = gas.calculateMemoryCost(count);

0 commit comments

Comments
 (0)