diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/SectionWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/SectionWriter.cs index e2dd6be3f9e7ef..bf6ced1ca929db 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/SectionWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/SectionWriter.cs @@ -176,6 +176,12 @@ public readonly void WriteUtf8StringNoNull(string value) bufferWriter.Advance(size); } + public readonly void WriteUtf8WithLength(string value) + { + WriteULEB128((ulong)Encoding.UTF8.GetByteCount(value)); + WriteUtf8StringNoNull(value); + } + public readonly void WritePadding(int size) => _sectionData.AppendPadding(size); public readonly long Position => _sectionData.Length; diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmNative.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmNative.cs index b31a65dcb25963..531ee005bb120e 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmNative.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmNative.cs @@ -8,6 +8,12 @@ namespace ILCompiler.ObjectWriter { + public interface IWasmEncodable + { + int EncodeSize(); + int Encode(Span buffer); + } + public enum WasmSectionType { Custom = 0, @@ -51,6 +57,12 @@ public enum WasmValueType : byte F64 = 0x7C } + public enum WasmMutabilityType : byte + { + Const = 0x00, + Mut = 0x01 + } + public static class WasmValueTypeExtensions { public static string ToTypeString(this WasmValueType valueType) @@ -66,7 +78,7 @@ public static string ToTypeString(this WasmValueType valueType) } } - #nullable enable +#nullable enable public readonly struct WasmResultType : IEquatable { private readonly WasmValueType[] _types; @@ -150,7 +162,7 @@ public readonly int Encode(Span buffer) buffer[0] = 0x60; // function type indicator int paramSize = _params.Encode(buffer.Slice(1)); - int returnSize = _returns.Encode(buffer.Slice(1+paramSize)); + int returnSize = _returns.Encode(buffer.Slice(1 + paramSize)); Debug.Assert(totalSize == 1 + paramSize + returnSize); return totalSize; @@ -158,7 +170,7 @@ public readonly int Encode(Span buffer) public bool Equals(WasmFuncType other) { - return _params.Equals(other._params) && _returns.Equals(other._returns); + return _params.Equals(other._params) && _returns.Equals(other._returns); } public override bool Equals(object? obj) @@ -183,18 +195,84 @@ public override string ToString() } // Represents a WebAssembly expression used in simple contexts for address calculation - enum WasmExprKind + public enum WasmExprKind { I32Const = 0x41, - I64Const = 0x42 + I64Const = 0x42, + GlobalGet = 0x23, + I32Add = 0x6A, + } + + public static class WasmExprKindExtensions + { + public static bool IsConstExpr(this WasmExprKind kind) + { + return kind == WasmExprKind.I32Const || kind == WasmExprKind.I64Const; + } + + public static bool IsBinaryExpr(this WasmExprKind kind) + { + return kind == WasmExprKind.I32Add; + } + + public static bool IsGlobalVarExpr(this WasmExprKind kind) + { + return kind == WasmExprKind.GlobalGet; + } + } + + class WasmInstructionGroup : IWasmEncodable + { + readonly WasmExpr[] WasmExprs; + public WasmInstructionGroup(WasmExpr[] wasmExprs) + { + WasmExprs = wasmExprs; + } + + public int Encode(Span buffer) + { + int pos = 0; + foreach (var expr in WasmExprs) + { + pos += expr.Encode(buffer.Slice(pos)); + } + buffer[pos++] = 0x0B; // end opcode + return pos; + } + + public int EncodeSize() + { + int size = 0; + foreach (var expr in WasmExprs) + { + size += expr.EncodeSize(); + } + // plus one for the end opcode + return size + 1; + } } - class WasmConstExpr + public abstract class WasmExpr : IWasmEncodable { WasmExprKind _kind; + public WasmExpr(WasmExprKind kind) + { + _kind = kind; + } + + public virtual int EncodeSize() => 1; + public virtual int Encode(Span buffer) + { + buffer[0] = (byte)_kind; + return 1; + } + } + + class WasmConstExpr : WasmExpr + { long ConstValue; - public WasmConstExpr(WasmExprKind kind, long value) + public WasmConstExpr(WasmExprKind kind, long value) : base(kind) { if (kind == WasmExprKind.I32Const) { @@ -202,25 +280,170 @@ public WasmConstExpr(WasmExprKind kind, long value) ArgumentOutOfRangeException.ThrowIfLessThan(value, int.MinValue); } - _kind = kind; ConstValue = value; } - public int EncodeSize() + public override int EncodeSize() { - uint valSize = DwarfHelper.SizeOfSLEB128(ConstValue); - return 1 + (int)valSize + 1; // opcode + value + end opcode + uint valSize = DwarfHelper.SizeOfSLEB128(ConstValue); + return base.EncodeSize() + (int)valSize; } - public int Encode(Span buffer) + public override int Encode(Span buffer) { - int pos = 0; - buffer[pos++] = (byte)_kind; // the kind is the opcode, either i32.const or i64.const - + int pos = base.Encode(buffer); pos += DwarfHelper.WriteSLEB128(buffer.Slice(pos), ConstValue); - buffer[pos++] = 0x0B; // end opcode return pos; } } + + // Simple DSL wrapper for creating Wasm expressions + static class Global + { + public static WasmExpr Get(int index) + { + return new WasmGlobalVarExpr(WasmExprKind.GlobalGet, index); + } + } + + static class I32 + { + public static WasmExpr Const(long value) + { + return new WasmConstExpr(WasmExprKind.I32Const, value); + } + + public static WasmExpr Add => new WasmBinaryExpr(WasmExprKind.I32Add); + } + + class WasmGlobalVarExpr : WasmExpr + { + public readonly int GlobalIndex; + public WasmGlobalVarExpr(WasmExprKind kind, int globalIndex) : base(kind) + { + Debug.Assert(globalIndex >= 0); + Debug.Assert(kind.IsGlobalVarExpr()); + GlobalIndex = globalIndex; + } + + public override int Encode(Span buffer) + { + int pos = base.Encode(buffer); + pos += DwarfHelper.WriteULEB128(buffer.Slice(pos), (uint)GlobalIndex); + return pos; + } + + public override int EncodeSize() + { + return base.EncodeSize() + (int)DwarfHelper.SizeOfULEB128((uint)GlobalIndex); + } + } + + // Represents a binary expression (e.g., i32.add) + class WasmBinaryExpr : WasmExpr + { + public WasmBinaryExpr(WasmExprKind kind) : base(kind) + { + Debug.Assert(kind.IsBinaryExpr()); + } + + // base class defaults are sufficient as the base class encodes just the opcode + } + public abstract class WasmImportType : IWasmEncodable + { + public abstract int Encode(Span buffer); + public abstract int EncodeSize(); + } + + + public enum WasmExternalKind : byte + { + Function = 0x00, + Table = 0x01, + Memory = 0x02, + Global = 0x03, + Tag = 0x04 + } + + public class WasmGlobalType : WasmImportType + { + WasmValueType ValueType; + WasmMutabilityType Mutability; + + public WasmGlobalType(WasmValueType valueType, WasmMutabilityType mutability) + { + ValueType = valueType; + Mutability = mutability; + } + + public override int Encode(Span buffer) + { + buffer[0] = (byte)ValueType; + buffer[1] = (byte)Mutability; + return 2; + } + + public override int EncodeSize() => 2; + } + + public enum WasmLimitType : byte + { + HasMin = 0x00, + HasMinAndMax = 0x01 + } + + public class WasmMemoryType : WasmImportType + { + WasmLimitType LimitType; + uint Min; + uint? Max; + + public WasmMemoryType(WasmLimitType limitType, uint min, uint? max = null) + { + if (LimitType == WasmLimitType.HasMinAndMax && !Max.HasValue) + { + throw new ArgumentException("Max must be provided when LimitType is HasMinAndMax"); + } + + LimitType = limitType; + Min = min; + Max = max; + } + + public override int Encode(Span buffer) + { + int pos = 0; + buffer[pos++] = (byte)LimitType; + pos += DwarfHelper.WriteULEB128(buffer.Slice(pos), Min); + if (LimitType == WasmLimitType.HasMinAndMax) + { + pos += DwarfHelper.WriteULEB128(buffer.Slice(pos), Max!.Value); + } + return pos; + } + + public override int EncodeSize() => 2; + } + + public class WasmImport : IWasmEncodable + { + public string Module; + public string Name; + public WasmExternalKind Kind; + WasmImportType Import; + + public WasmImport(string module, string name, WasmExternalKind kind, WasmImportType import) + { + Module = module; + Name = name; + Kind = kind; + Import = import; + } + + public int Encode(Span buffer) => Import.Encode(buffer); + public int EncodeSize() => Import.EncodeSize(); + +#nullable disable + } } diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs index 4b52fa9da4c79c..cc1d150cdf1fea 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs @@ -29,6 +29,7 @@ internal static class WasmObjectNodeSection public static readonly ObjectNodeSection ExportSection = new ObjectNodeSection("wasm.export", SectionType.ReadOnly, needsAlign: false); public static readonly ObjectNodeSection MemorySection = new ObjectNodeSection("wasm.memory", SectionType.ReadOnly, needsAlign: false); public static readonly ObjectNodeSection TableSection = new ObjectNodeSection("wasm.table", SectionType.ReadOnly, needsAlign: false); + public static readonly ObjectNodeSection ImportSection = new ObjectNodeSection("wasm.import", SectionType.ReadOnly, needsAlign: false); } /// @@ -37,7 +38,6 @@ internal static class WasmObjectNodeSection internal sealed class WasmObjectWriter : ObjectWriter { protected override CodeDataLayout LayoutMode => CodeDataLayout.Separate; - private const int DataStartOffset = 0x10000; // Start of linear memory for data segments (leaving 1 page for stack) public WasmObjectWriter(NodeFactory factory, ObjectWritingOptions options, OutputInfoBuilder outputInfoBuilder) : base(factory, options, outputInfoBuilder) @@ -98,6 +98,26 @@ private void WriteSignatureIndexForFunction(MethodDesc desc) writer.WriteULEB128((ulong)signatureIndex); } + + private int _numImports; + /// + /// Writes the common prefix for an import entry, which includes the module name, import name, and kind. + /// + private SectionWriter WriteImport(WasmImport import) + { + SectionWriter writer = GetOrCreateSection(WasmObjectNodeSection.ImportSection); + writer.WriteUtf8WithLength(import.Module); + writer.WriteUtf8WithLength(import.Name); + writer.WriteByte((byte)import.Kind); + + int size = import.EncodeSize(); + import.Encode(writer.Buffer.GetSpan(size)); + writer.Buffer.Advance((int)size); + + _numImports++; + return writer; + } + /// /// WebAssembly export descriptor kinds per the spec. /// @@ -134,7 +154,6 @@ private void WriteMemoryExport(string name, int memoryIndex) => private void WriteGlobalExport(string name, int globalIndex) => WriteExport(name, WasmExportKind.Global, globalIndex); - private List _sections = new(); private Dictionary _sectionNameToIndex = new(); private Dictionary _sectionToType = new() @@ -143,10 +162,10 @@ private void WriteGlobalExport(string name, int globalIndex) => { WasmObjectNodeSection.FunctionSection, WasmSectionType.Function }, { WasmObjectNodeSection.TableSection, WasmSectionType.Table }, { WasmObjectNodeSection.ExportSection, WasmSectionType.Export }, + { WasmObjectNodeSection.ImportSection, WasmSectionType.Import }, { WasmObjectNodeSection.TypeSection, WasmSectionType.Type }, { ObjectNodeSection.WasmCodeSection, WasmSectionType.Code } }; - private WasmSectionType GetWasmSectionType(ObjectNodeSection section) { if (!_sectionToType.ContainsKey(section)) @@ -163,16 +182,25 @@ protected internal override void UpdateSectionAlignment(int sectionIndex, int al // This is a no-op for now under Wasm } - private WasmDataSection CreateCombinedDataSection(int dataStartOffset) + private WasmDataSection CreateCombinedDataSection() { + WasmInstructionGroup GetR2StartOffset(int offset) + { + return new WasmInstructionGroup([ + Global.Get(1), // __r2r_start + I32.Const(offset), + I32.Add, + ]); + } + IEnumerable dataSections = _sections.Where(s => s.Type == WasmSectionType.Data); - int offset = dataStartOffset; + int offset = 0; List segments = new(); foreach (WasmSection wasmSection in dataSections) { Debug.Assert(wasmSection.Type == WasmSectionType.Data); WasmDataSegment segment = new WasmDataSegment(wasmSection.Stream, wasmSection.Name, WasmDataSectionType.Active, - new WasmConstExpr(WasmExprKind.I32Const, (long)offset)); + GetR2StartOffset(offset)); segments.Add(segment); offset += segment.ContentSize; } @@ -212,7 +240,7 @@ private protected override void CreateSection(ObjectNodeSection section, Utf8Str WasmSection wasmSection; if (section == WasmObjectNodeSection.CombinedDataSection) { - wasmSection = CreateCombinedDataSection(DataStartOffset); + wasmSection = CreateCombinedDataSection(); } else { @@ -239,8 +267,6 @@ private void WriteMemorySection(ulong contentSize) private protected override void EmitSectionsAndLayout() { GetOrCreateSection(WasmObjectNodeSection.CombinedDataSection); - ulong dataContentSize = (ulong)SectionByName(WasmObjectNodeSection.CombinedDataSection.Name).ContentSize; - WriteMemorySection(dataContentSize + DataStartOffset); WriteTableSection(); } @@ -271,12 +297,12 @@ private protected override void EmitObjectFile(Stream outputFileStream) // Type section (1) SectionByName(WasmObjectNodeSection.TypeSection.Name).Emit(outputFileStream); + // Import section (2) + SectionByName(WasmObjectNodeSection.ImportSection.Name).Emit(outputFileStream); // Function section (3) SectionByName(WasmObjectNodeSection.FunctionSection.Name).Emit(outputFileStream); // Table section (4) SectionByName(WasmObjectNodeSection.TableSection.Name).Emit(outputFileStream); - // Memory section (5) - SectionByName(WasmObjectNodeSection.MemorySection.Name).Emit(outputFileStream); // Export section (7) SectionByName(WasmObjectNodeSection.ExportSection.Name).Emit(outputFileStream); // Code section (10) @@ -292,12 +318,31 @@ private protected override void EmitRelocations(int sectionIndex, List definedSymbols, SortedSet undefinedSymbols) + private WasmImport[] _defaultImports = new[] { - WriteMemoryExport("memory", 0); - WriteTableExport("table", 0); + null, // placeholder for memory, which is set up dynamically in WriteImports() + new WasmImport("env", "__stack_pointer", WasmExternalKind.Global, new WasmGlobalType(WasmValueType.I32, WasmMutabilityType.Mut)), + new WasmImport("env", "__r2r_start", WasmExternalKind.Global, new WasmGlobalType(WasmValueType.I32, WasmMutabilityType.Const)), + }; + private void WriteImports() + { + // Calculate the required memory size based on the combined data section size + ulong contentSize = (ulong)SectionByName(WasmObjectNodeSection.CombinedDataSection.Name).ContentSize; + ulong numPages = (contentSize + (1<<16) - 1) >> 16; + + _defaultImports[0] = new WasmImport("env", "memory", WasmExternalKind.Memory, + new WasmMemoryType(0x00, (uint)numPages)); // memory limits: flags (0 = only minimum) + + foreach (WasmImport import in _defaultImports) + { + WriteImport(import); + } + } + + private void WriteExports() + { + WriteTableExport("table", 0); string[] functionExports = _uniqueSymbols.Keys.ToArray(); // TODO-WASM: Handle exports better (e.g., only export public methods, etc.) // Also, see if we could leverage definedSymbols for this instead of doing our own bookkeeping in _uniqueSymbols. @@ -305,6 +350,13 @@ private protected override void EmitSymbolTable(IDictionary definedSymbols, SortedSet undefinedSymbols) + { + WriteImports(); + WriteExports(); int funcIdx = _sectionNameToIndex[WasmObjectNodeSection.FunctionSection.Name]; PrependCount(_sections[funcIdx], _methodCount); @@ -314,6 +366,8 @@ private protected override void EmitSymbolTable(IDictionary