authorgravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2020-08-18 18:30:48+02:00
committergravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2020-08-19 02:05:01+02:00
logfe3aa4ccd0bc157e1dd9d84b7c57b0cb2f0a77a9
tree9412eed471ec13ed1649ee3348f6d62e802202a9
parent741fb8d30675f85316f7df100801e7cf8c2b3194
signaturelock-open Commit is signed but in an unrecognized format.

stage2/wasm: do incremental compilation in-memory

Before this commit the wasm backend worked similarly to elf. As functions were generated they were written directly to the output file and existing code was shifted around in the file as necessary. This approach had several disadvantages: - Large amounts of padding in the output were necessary to avoid expensive copying of data within the file. - Function/type/global/etc indexes were required to be known at the time of preforming codegen, which severely limited the flexibility of where code could be placed in the binary - Significant complexity to track the state of the output file through incremental updates This commit takes things in a different direction. Code is incrementally compiled into in-memory buffers and the entire binary is rewritten using these buffers on flush. This has several advantages: - Significantly smaller resulting binaries - More performant resulting binaries due to lack of indirection - Significantly simpler compiler code - Indexes no longer need to be known before codegen. We can track where Decls must be referenced by index insert the proper indexes while writing the code in the flush() function. This is not yet implemented but is planned for the next commit. The main disadvantage is of course increased memory usage in order to store these buffers of generated code.

1 files changed, 135 insertions(+), 361 deletions(-)

src-self-hosted/link/Wasm.zig+135-361
...@@ -32,19 +32,19 @@ const spec = struct {...@@ -32,19 +32,19 @@ const spec = struct {
32pub const base_tag = link.File.Tag.wasm;32pub const base_tag = link.File.Tag.wasm;
3333
34pub const FnData = struct {34pub const FnData = struct {
35 funcidx: u32,35 /// Generated code for the type of the function
36 functype: std.ArrayListUnmanaged(u8) = .{},
37 /// Generated code for the body of the function
38 code: std.ArrayListUnmanaged(u8) = .{},
36};39};
3740
38base: link.File,41base: link.File,
3942
40types: Types,43/// List of all function Decls to be written to the output file. The index of
41funcs: Funcs,44/// each Decl in this list at the time of writing the binary is used as the
42exports: Exports,45/// function index.
4346/// TODO: can/should we access some data structure in Module directly?
44/// Array over the section structs used in the various sections above to47funcs: std.ArrayListUnmanaged(*Module.Decl) = .{},
45/// allow iteration when shifting sections to make space.
46/// TODO: this should eventually be size 11 when we use all the sections.
47sections: [4]*Section,
4848
49pub fn openPath(allocator: *Allocator, dir: fs.Dir, sub_path: []const u8, options: link.Options) !*link.File {49pub fn openPath(allocator: *Allocator, dir: fs.Dir, sub_path: []const u8, options: link.Options) !*link.File {
50 assert(options.object_format == .wasm);50 assert(options.object_format == .wasm);
...@@ -58,10 +58,6 @@ pub fn openPath(allocator: *Allocator, dir: fs.Dir, sub_path: []const u8, option...@@ -58,10 +58,6 @@ pub fn openPath(allocator: *Allocator, dir: fs.Dir, sub_path: []const u8, option
5858
59 try file.writeAll(&(spec.magic ++ spec.version));59 try file.writeAll(&(spec.magic ++ spec.version));
6060
61 // TODO: this should vary depending on the section and be less arbitrary
62 const size = 1024;
63 const offset = @sizeOf(@TypeOf(spec.magic ++ spec.version));
64
65 wasm.* = .{61 wasm.* = .{
66 .base = .{62 .base = .{
67 .tag = .wasm,63 .tag = .wasm,
...@@ -69,52 +65,40 @@ pub fn openPath(allocator: *Allocator, dir: fs.Dir, sub_path: []const u8, option...@@ -69,52 +65,40 @@ pub fn openPath(allocator: *Allocator, dir: fs.Dir, sub_path: []const u8, option
69 .file = file,65 .file = file,
70 .allocator = allocator,66 .allocator = allocator,
71 },67 },
72
73 .types = try Types.init(file, offset, size),
74 .funcs = try Funcs.init(file, offset + size, size, offset + 3 * size, size),
75 .exports = try Exports.init(file, offset + 2 * size, size),
76
77 // These must be ordered as they will appear in the output file
78 .sections = [_]*Section{
79 &wasm.types.typesec.section,
80 &wasm.funcs.funcsec,
81 &wasm.exports.exportsec,
82 &wasm.funcs.codesec.section,
83 },
84 };68 };
8569
86 try file.setEndPos(offset + 4 * size);
87
88 return &wasm.base;70 return &wasm.base;
89}71}
9072
91pub fn deinit(self: *Wasm) void {73pub fn deinit(self: *Wasm) void {
92 self.types.deinit();74 for (self.funcs.items) |decl| {
93 self.funcs.deinit();75 decl.fn_link.wasm.?.functype.deinit(self.base.allocator);
76 decl.fn_link.wasm.?.code.deinit(self.base.allocator);
77 }
78 self.funcs.deinit(self.base.allocator);
94}79}
9580
81// Generate code for the Decl, storing it in memory to be later written to
82// the file on flush().
96pub fn updateDecl(self: *Wasm, module: *Module, decl: *Module.Decl) !void {83pub fn updateDecl(self: *Wasm, module: *Module, decl: *Module.Decl) !void {
97 if (decl.typed_value.most_recent.typed_value.ty.zigTypeTag() != .Fn)84 if (decl.typed_value.most_recent.typed_value.ty.zigTypeTag() != .Fn)
98 return error.TODOImplementNonFnDeclsForWasm;85 return error.TODOImplementNonFnDeclsForWasm;
9986
100 if (decl.fn_link.wasm) |fn_data| {87 if (decl.fn_link.wasm) |*fn_data| {
101 self.funcs.free(fn_data.funcidx);88 fn_data.functype.items.len = 0;
102 }89 fn_data.code.items.len = 0;
10390 } else {
104 var buf = std.ArrayList(u8).init(self.base.allocator);91 decl.fn_link.wasm = .{};
105 defer buf.deinit();92 try self.funcs.append(self.base.allocator, decl);
10693 }
107 try codegen.genFunctype(&buf, decl);94 const fn_data = &decl.fn_link.wasm.?;
108 const typeidx = try self.types.new(buf.items);95
109 buf.items.len = 0;96 var managed_functype = fn_data.functype.toManaged(self.base.allocator);
11097 var managed_code = fn_data.code.toManaged(self.base.allocator);
111 try codegen.genCode(&buf, decl);98 try codegen.genFunctype(&managed_functype, decl);
112 const funcidx = try self.funcs.new(typeidx, buf.items);99 try codegen.genCode(&managed_code, decl);
113100 fn_data.functype = managed_functype.toUnmanaged();
114 decl.fn_link.wasm = .{ .funcidx = funcidx };101 fn_data.code = managed_code.toUnmanaged();
115
116 // TODO: we should be more smart and set this only when needed
117 self.exports.dirty = true;
118}102}
119103
120pub fn updateDeclExports(104pub fn updateDeclExports(
...@@ -122,332 +106,122 @@ pub fn updateDeclExports(...@@ -122,332 +106,122 @@ pub fn updateDeclExports(
122 module: *Module,106 module: *Module,
123 decl: *const Module.Decl,107 decl: *const Module.Decl,
124 exports: []const *Module.Export,108 exports: []const *Module.Export,
125) !void {109) !void {}
126 self.exports.dirty = true;
127}
128110
129pub fn freeDecl(self: *Wasm, decl: *Module.Decl) void {111pub fn freeDecl(self: *Wasm, decl: *Module.Decl) void {
130 // TODO: remove this assert when non-function Decls are implemented112 // TODO: remove this assert when non-function Decls are implemented
131 assert(decl.typed_value.most_recent.typed_value.ty.zigTypeTag() == .Fn);113 assert(decl.typed_value.most_recent.typed_value.ty.zigTypeTag() == .Fn);
132 if (decl.fn_link.wasm) |fn_data| {114 _ = self.funcs.swapRemove(self.getFuncidx(decl).?);
133 self.funcs.free(fn_data.funcidx);115 decl.fn_link.wasm.?.functype.deinit(self.base.allocator);
134 decl.fn_link.wasm = null;116 decl.fn_link.wasm.?.code.deinit(self.base.allocator);
135 }117 decl.fn_link.wasm = null;
136}118}
137119
138pub fn flush(self: *Wasm, module: *Module) !void {120pub fn flush(self: *Wasm, module: *Module) !void {
139 if (self.exports.dirty) try self.exports.writeAll(module);121 const file = self.base.file.?;
140}122 const header_size = 5 + 1;
141123
142/// This struct describes the location of a named section + custom section124 // No need to rewrite the magic/version header
143/// padding in the output file. This is all the data we need to allow for125 try file.setEndPos(@sizeOf(@TypeOf(spec.magic ++ spec.version)));
144/// shifting sections around when padding runs out.126 try file.seekTo(@sizeOf(@TypeOf(spec.magic ++ spec.version)));
145const Section = struct {127
146 /// The size of a section header: 1 byte section id + 5 bytes128 // Type section
147 /// for the fixed-width ULEB128 encoded contents size.129 {
148 const header_size = 1 + 5;130 const header_offset = try reserveVecSectionHeader(file);
149 /// Offset of the section id byte from the start of the file.131 for (self.funcs.items) |decl| {
150 offset: u64,132 try file.writeAll(decl.fn_link.wasm.?.functype.items);
151 /// Size of the section, including the header and directly
152 /// following custom section used for padding if any.
153 size: u64,
154
155 /// Resize the usable part of the section, handling the following custom
156 /// section used for padding. If there is not enough padding left, shift
157 /// all following sections to make space. Takes the current and target
158 /// contents sizes of the section as arguments.
159 fn resize(self: *Section, file: fs.File, current: u32, target: u32) !void {
160 // Section header + target contents size + custom section header
161 // + custom section name + empty custom section > owned chunk of the file
162 if (header_size + target + header_size + 1 + 0 > self.size)
163 return error.TODOImplementSectionShifting;
164
165 const new_custom_start = self.offset + header_size + target;
166 const new_custom_contents_size = self.size - target - 2 * header_size;
167 assert(new_custom_contents_size >= 1);
168 // +1 for the name of the custom section, which we set to an empty string
169 var custom_header: [header_size + 1]u8 = undefined;
170 custom_header[0] = spec.custom_id;
171 leb.writeUnsignedFixed(5, custom_header[1..header_size], @intCast(u32, new_custom_contents_size));
172 custom_header[header_size] = 0;
173 try file.pwriteAll(&custom_header, new_custom_start);
174 }
175};
176
177/// This can be used to manage the contents of any section which uses a vector
178/// of contents. This interface maintains index stability while allowing for
179/// reuse of "dead" indexes.
180const VecSection = struct {
181 /// Represents a single entry in the vector (e.g. a type in the type section)
182 const Entry = struct {
183 /// Offset from the start of the section contents in bytes
184 offset: u32,
185 /// Size in bytes of the entry
186 size: u32,
187 };
188 section: Section,
189 /// Size in bytes of the contents of the section. Does not include
190 /// the "header" containing the section id and this value.
191 contents_size: u32,
192 /// List of all entries in the contents of the section.
193 entries: std.ArrayListUnmanaged(Entry) = std.ArrayListUnmanaged(Entry){},
194 /// List of indexes of unreferenced entries which may be
195 /// overwritten and reused.
196 dead_list: std.ArrayListUnmanaged(u32) = std.ArrayListUnmanaged(u32){},
197
198 /// Write the headers of the section and custom padding section
199 fn init(comptime section_id: u8, file: fs.File, offset: u64, initial_size: u64) !VecSection {
200 // section id, section size, empty vector, custom section id,
201 // custom section size, empty custom section name
202 var initial_data: [1 + 5 + 5 + 1 + 5 + 1]u8 = undefined;
203
204 assert(initial_size >= initial_data.len);
205
206 comptime var i = 0;
207 initial_data[i] = section_id;
208 i += 1;
209 leb.writeUnsignedFixed(5, initial_data[i..(i + 5)], 5);
210 i += 5;
211 leb.writeUnsignedFixed(5, initial_data[i..(i + 5)], 0);
212 i += 5;
213 initial_data[i] = spec.custom_id;
214 i += 1;
215 leb.writeUnsignedFixed(5, initial_data[i..(i + 5)], @intCast(u32, initial_size - @sizeOf(@TypeOf(initial_data))));
216 i += 5;
217 initial_data[i] = 0;
218
219 try file.pwriteAll(&initial_data, offset);
220
221 return VecSection{
222 .section = .{
223 .offset = offset,
224 .size = initial_size,
225 },
226 .contents_size = 5,
227 };
228 }
229
230 fn deinit(self: *VecSection, allocator: *Allocator) void {
231 self.entries.deinit(allocator);
232 self.dead_list.deinit(allocator);
233 }
234
235 /// Write a new entry into the file, returning the index used.
236 fn addEntry(self: *VecSection, file: fs.File, allocator: *Allocator, data: []const u8) !u32 {
237 // First look for a dead entry we can reuse
238 for (self.dead_list.items) |dead_idx, i| {
239 const dead_entry = &self.entries.items[dead_idx];
240 if (dead_entry.size == data.len) {
241 // Found a dead entry of the right length, overwrite it
242 try file.pwriteAll(data, self.section.offset + Section.header_size + dead_entry.offset);
243 _ = self.dead_list.swapRemove(i);
244 return dead_idx;
245 }
246 }133 }
247134 try writeVecSectionHeader(
248 // TODO: We can be more efficient if we special-case one or135 file,
249 // more consecutive dead entries at the end of the vector.136 header_offset,
250137 spec.types_id,
251 // We failed to find a dead entry to reuse, so write the new138 @intCast(u32, (try file.getPos()) - header_offset - header_size),
252 // entry to the end of the section.139 @intCast(u32, self.funcs.items.len),
253 try self.section.resize(file, self.contents_size, self.contents_size + @intCast(u32, data.len));140 );
254 try file.pwriteAll(data, self.section.offset + Section.header_size + self.contents_size);141 }
255 try self.entries.append(allocator, .{142
256 .offset = self.contents_size,143 // Function section
257 .size = @intCast(u32, data.len),144 {
258 });145 const header_offset = try reserveVecSectionHeader(file);
259 self.contents_size += @intCast(u32, data.len);146 const writer = file.writer();
260 // Make sure the dead list always has enough space to store all free'd147 for (self.funcs.items) |_, typeidx| try leb.writeULEB128(writer, @intCast(u32, typeidx));
261 // entries. This makes it so that delEntry() cannot fail.148 try writeVecSectionHeader(
262 // TODO: figure out a better way that doesn't waste as much memory149 file,
263 try self.dead_list.ensureCapacity(allocator, self.entries.items.len);150 header_offset,
264151 spec.funcs_id,
265 // Update the size in the section header and the item count of152 @intCast(u32, (try file.getPos()) - header_offset - header_size),
266 // the contents vector.153 @intCast(u32, self.funcs.items.len),
267 var size_and_count: [10]u8 = undefined;154 );
268 leb.writeUnsignedFixed(5, size_and_count[0..5], self.contents_size);155 }
269 leb.writeUnsignedFixed(5, size_and_count[5..], @intCast(u32, self.entries.items.len));156
270 try file.pwriteAll(&size_and_count, self.section.offset + 1);157 // Export section
271158 {
272 return @intCast(u32, self.entries.items.len - 1);159 const header_offset = try reserveVecSectionHeader(file);
273 }160 const writer = file.writer();
274161 var count: u32 = 0;
275 /// Mark the type referenced by the given index as dead.
276 fn delEntry(self: *VecSection, index: u32) void {
277 self.dead_list.appendAssumeCapacity(index);
278 }
279};
280
281const Types = struct {
282 typesec: VecSection,
283
284 fn init(file: fs.File, offset: u64, initial_size: u64) !Types {
285 return Types{ .typesec = try VecSection.init(spec.types_id, file, offset, initial_size) };
286 }
287
288 fn deinit(self: *Types) void {
289 const wasm = @fieldParentPtr(Wasm, "types", self);
290 self.typesec.deinit(wasm.base.allocator);
291 }
292
293 fn new(self: *Types, data: []const u8) !u32 {
294 const wasm = @fieldParentPtr(Wasm, "types", self);
295 return self.typesec.addEntry(wasm.base.file.?, wasm.base.allocator, data);
296 }
297
298 fn free(self: *Types, typeidx: u32) void {
299 self.typesec.delEntry(typeidx);
300 }
301};
302
303const Funcs = struct {
304 /// This section needs special handling to keep the indexes matching with
305 /// the codesec, so we cant just use a VecSection.
306 funcsec: Section,
307 /// The typeidx stored for each function, indexed by funcidx.
308 func_types: std.ArrayListUnmanaged(u32) = std.ArrayListUnmanaged(u32){},
309 codesec: VecSection,
310
311 fn init(file: fs.File, funcs_offset: u64, funcs_size: u64, code_offset: u64, code_size: u64) !Funcs {
312 return Funcs{
313 .funcsec = (try VecSection.init(spec.funcs_id, file, funcs_offset, funcs_size)).section,
314 .codesec = try VecSection.init(spec.code_id, file, code_offset, code_size),
315 };
316 }
317
318 fn deinit(self: *Funcs) void {
319 const wasm = @fieldParentPtr(Wasm, "funcs", self);
320 self.func_types.deinit(wasm.base.allocator);
321 self.codesec.deinit(wasm.base.allocator);
322 }
323
324 /// Add a new function to the binary, first finding space for and writing
325 /// the code then writing the typeidx to the corresponding index in the
326 /// funcsec. Returns the function index used.
327 fn new(self: *Funcs, typeidx: u32, code: []const u8) !u32 {
328 const wasm = @fieldParentPtr(Wasm, "funcs", self);
329 const file = wasm.base.file.?;
330 const allocator = wasm.base.allocator;
331
332 assert(self.func_types.items.len == self.codesec.entries.items.len);
333
334 // TODO: consider nop-padding the code if there is a close but not perfect fit
335 const funcidx = try self.codesec.addEntry(file, allocator, code);
336
337 if (self.func_types.items.len < self.codesec.entries.items.len) {
338 // u32 vector length + funcs_count u32s in the vector
339 const current = 5 + @intCast(u32, self.func_types.items.len) * 5;
340 try self.funcsec.resize(file, current, current + 5);
341 try self.func_types.append(allocator, typeidx);
342
343 // Update the size in the section header and the item count of
344 // the contents vector.
345 const count = @intCast(u32, self.func_types.items.len);
346 var size_and_count: [10]u8 = undefined;
347 leb.writeUnsignedFixed(5, size_and_count[0..5], 5 + count * 5);
348 leb.writeUnsignedFixed(5, size_and_count[5..], count);
349 try file.pwriteAll(&size_and_count, self.funcsec.offset + 1);
350 } else {
351 // We are overwriting a dead function and may now free the type
352 wasm.types.free(self.func_types.items[funcidx]);
353 }
354
355 assert(self.func_types.items.len == self.codesec.entries.items.len);
356
357 var typeidx_leb: [5]u8 = undefined;
358 leb.writeUnsignedFixed(5, &typeidx_leb, typeidx);
359 try file.pwriteAll(&typeidx_leb, self.funcsec.offset + Section.header_size + 5 + funcidx * 5);
360
361 return funcidx;
362 }
363
364 fn free(self: *Funcs, funcidx: u32) void {
365 self.codesec.delEntry(funcidx);
366 }
367};
368
369/// Exports are tricky. We can't leave dead entries in the binary as they
370/// would obviously be visible from the execution environment. The simplest
371/// way to work around this is to re-emit the export section whenever
372/// something changes. This also makes it easier to ensure exported function
373/// and global indexes are updated as they change.
374const Exports = struct {
375 exportsec: Section,
376 /// Size in bytes of the contents of the section. Does not include
377 /// the "header" containing the section id and this value.
378 contents_size: u32,
379 /// If this is true, then exports will be rewritten on flush()
380 dirty: bool,
381
382 fn init(file: fs.File, offset: u64, initial_size: u64) !Exports {
383 return Exports{
384 .exportsec = (try VecSection.init(spec.exports_id, file, offset, initial_size)).section,
385 .contents_size = 5,
386 .dirty = false,
387 };
388 }
389
390 fn writeAll(self: *Exports, module: *Module) !void {
391 const wasm = @fieldParentPtr(Wasm, "exports", self);
392 const file = wasm.base.file.?;
393 var buf: [5]u8 = undefined;
394
395 // First ensure the section is the right size
396 var export_count: u32 = 0;
397 var new_contents_size: u32 = 5;
398 for (module.decl_exports.entries.items) |entry| {162 for (module.decl_exports.entries.items) |entry| {
399 for (entry.value) |e| {163 for (entry.value) |exprt| {
400 export_count += 1;164 // Export name length + name
401 new_contents_size += calcSize(e);165 try leb.writeULEB128(writer, @intCast(u32, exprt.options.name.len));
166 try writer.writeAll(exprt.options.name);
167
168 switch (exprt.exported_decl.typed_value.most_recent.typed_value.ty.zigTypeTag()) {
169 .Fn => {
170 // Type of the export
171 try writer.writeByte(0x00);
172 // Exported function index
173 try leb.writeULEB128(writer, self.getFuncidx(exprt.exported_decl).?);
174 },
175 else => return error.TODOImplementNonFnDeclsForWasm,
176 }
177
178 count += 1;
402 }179 }
403 }180 }
404 if (new_contents_size != self.contents_size) {181 try writeVecSectionHeader(
405 try self.exportsec.resize(file, self.contents_size, new_contents_size);182 file,
406 leb.writeUnsignedFixed(5, &buf, new_contents_size);183 header_offset,
407 try file.pwriteAll(&buf, self.exportsec.offset + 1);184 spec.exports_id,
408 }185 @intCast(u32, (try file.getPos()) - header_offset - header_size),
409186 count,
410 try file.seekTo(self.exportsec.offset + Section.header_size);187 );
411 const writer = file.writer();188 }
412189
413 // Length of the exports vec190 // Code section
414 leb.writeUnsignedFixed(5, &buf, export_count);191 {
415 try writer.writeAll(&buf);192 const header_offset = try reserveVecSectionHeader(file);
416193 for (self.funcs.items) |decl| try file.writeAll(decl.fn_link.wasm.?.code.items);
417 for (module.decl_exports.entries.items) |entry|194 try writeVecSectionHeader(
418 for (entry.value) |e| try writeExport(writer, e);195 file,
419196 header_offset,
420 self.dirty = false;197 spec.code_id,
421 }198 @intCast(u32, (try file.getPos()) - header_offset - header_size),
422199 @intCast(u32, self.funcs.items.len),
423 /// Return the total number of bytes an export will take.200 );
424 /// TODO: fixed-width LEB128 is currently used for simplicity, but should
425 /// be replaced with proper variable-length LEB128 as it is inefficient.
426 fn calcSize(e: *Module.Export) u32 {
427 // LEB128 name length + name bytes + export type + LEB128 index
428 return 5 + @intCast(u32, e.options.name.len) + 1 + 5;
429 }201 }
202}
430203
431 /// Write the data for a single export to the given file at a given offset.204/// Get the current index of a given Decl in the function list
432 /// TODO: fixed-width LEB128 is currently used for simplicity, but should205/// TODO: we could maintain a hash map to potentially make this
433 /// be replaced with proper variable-length LEB128 as it is inefficient.206fn getFuncidx(self: Wasm, decl: *Module.Decl) ?u32 {
434 fn writeExport(writer: anytype, e: *Module.Export) !void {207 return for (self.funcs.items) |func, idx| {
435 var buf: [5]u8 = undefined;208 if (func == decl) break @intCast(u32, idx);
209 } else null;
210}
436211
437 // Export name length + name212fn reserveVecSectionHeader(file: fs.File) !u64 {
438 leb.writeUnsignedFixed(5, &buf, @intCast(u32, e.options.name.len));213 // section id + fixed leb contents size + fixed leb vector length
439 try writer.writeAll(&buf);214 const header_size = 1 + 5 + 5;
440 try writer.writeAll(e.options.name);215 // TODO: this should be a single lseek(2) call, but fs.File does not
216 // currently provide a way to do this.
217 try file.seekBy(header_size);
218 return (try file.getPos()) - header_size;
219}
441220
442 switch (e.exported_decl.typed_value.most_recent.typed_value.ty.zigTypeTag()) {221fn writeVecSectionHeader(file: fs.File, offset: u64, section: u8, size: u32, items: u32) !void {
443 .Fn => {222 var buf: [1 + 5 + 5]u8 = undefined;
444 // Type of the export223 buf[0] = section;
445 try writer.writeByte(0x00);224 leb.writeUnsignedFixed(5, buf[1..6], size);
446 // Exported function index225 leb.writeUnsignedFixed(5, buf[6..], items);
447 leb.writeUnsignedFixed(5, &buf, e.exported_decl.fn_link.wasm.?.funcidx);226 try file.pwriteAll(&buf, offset);
448 try writer.writeAll(&buf);227}
449 },
450 else => return error.TODOImplementNonFnDeclsForWasm,
451 }
452 }
453};