hyperlight-component-csharp 0.19.0

Experimental. Generates C# host bindings for Hyperlight components from WIT worlds
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 The Hyperlight Authors.

// Support types for C# bindings generated by hyperlight-component-csharp.
// The encoding is specified in docs/component-wire-format.md.

#nullable enable
// Members named after WIT types and object overrides need no docs.
#pragma warning disable CS1591

using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Text;

namespace Hyperlight.Component
{
    /// <summary>Calls guest functions by wire name.</summary>
    public interface IGuestCaller
    {
        /// <summary>
        /// Calls <paramref name="function"/> with one encoded buffer per
        /// parameter and returns the encoded result.
        /// </summary>
        byte[] Call(string function, byte[][] args);
    }

    /// <summary>Registers host functions by wire name.</summary>
    public interface IHostFunctionRegistry
    {
        /// <summary>
        /// Registers <paramref name="implementation"/>, which receives one
        /// encoded buffer per parameter and returns the encoded result.
        /// </summary>
        void Register(string function, int parameterCount, Func<byte[][], byte[]> implementation);
    }

    /// <summary>Thrown when encoded data does not match the expected type.</summary>
    public sealed class WireFormatException : Exception
    {
        public WireFormatException(string message) : base(message) { }
    }

    /// <summary>The payload of a <c>result</c> case without a type.</summary>
    public readonly struct Unit : IEquatable<Unit>
    {
        public bool Equals(Unit other) => true;
        public override bool Equals(object? obj) => obj is Unit;
        public override int GetHashCode() => 0;
        public override string ToString() => "()";

        public static bool operator ==(Unit left, Unit right) => true;

        public static bool operator !=(Unit left, Unit right) => false;
    }

    /// <summary>
    /// A WIT <c>option</c> whose payload is itself an <c>option</c>.
    /// Other options map to nullable types.
    /// </summary>
    public readonly struct Option<T> : IEquatable<Option<T>>
    {
        private readonly T _value;

        private Option(T value)
        {
            HasValue = true;
            _value = value;
        }

        public static Option<T> None => default;

        public static Option<T> Some(T value) => new(value);

        public bool HasValue { get; }

        public T Value => HasValue ? _value : throw new InvalidOperationException("option has no value");

        public bool Equals(Option<T> other) =>
            HasValue == other.HasValue && (!HasValue || EqualityComparer<T>.Default.Equals(_value, other._value));

        public override bool Equals(object? obj) => obj is Option<T> other && Equals(other);

        public override int GetHashCode() => HasValue ? HashCode.Combine(true, _value) : 0;

        public override string ToString() => HasValue ? $"Some({_value})" : "None";

        public static bool operator ==(Option<T> left, Option<T> right) => left.Equals(right);

        public static bool operator !=(Option<T> left, Option<T> right) => !left.Equals(right);
    }

    /// <summary>A WIT <c>result</c>.</summary>
    public readonly struct Result<TOk, TErr> : IEquatable<Result<TOk, TErr>>
    {
        private readonly TOk _ok;
        private readonly TErr _err;

        private Result(bool isOk, TOk ok, TErr err)
        {
            IsOk = isOk;
            _ok = ok;
            _err = err;
        }

        public static Result<TOk, TErr> FromOk(TOk value) => new(true, value, default!);

        public static Result<TOk, TErr> FromErr(TErr value) => new(false, default!, value);

        public bool IsOk { get; }

        public TOk Ok => IsOk ? _ok : throw new InvalidOperationException("result is an error");

        public TErr Err => IsOk ? throw new InvalidOperationException("result is not an error") : _err;

        public bool Equals(Result<TOk, TErr> other) =>
            IsOk == other.IsOk && (IsOk
                ? EqualityComparer<TOk>.Default.Equals(_ok, other._ok)
                : EqualityComparer<TErr>.Default.Equals(_err, other._err));

        public override bool Equals(object? obj) => obj is Result<TOk, TErr> other && Equals(other);

        public override int GetHashCode() =>
            IsOk ? HashCode.Combine(true, _ok) : HashCode.Combine(false, _err);

        public override string ToString() => IsOk ? $"Ok({_ok})" : $"Err({_err})";

        public static bool operator ==(Result<TOk, TErr> left, Result<TOk, TErr> right) => left.Equals(right);

        public static bool operator !=(Result<TOk, TErr> left, Result<TOk, TErr> right) => !left.Equals(right);
    }

    /// <summary>Encodes values in the Hyperlight component wire format.</summary>
    public sealed class WireWriter
    {
        // Throws on lone surrogates, which a WIT string cannot hold.
        private static readonly UTF8Encoding StrictUtf8 = new(false, true);

        private byte[] _buffer;
        private int _length;

        public WireWriter(int capacity = 64)
        {
            _buffer = new byte[Math.Max(capacity, 1)];
        }

        /// <summary>The generated world's resource tables, for writing handles.</summary>
        public object? Resources { get; init; }

        /// <summary>The call that handles written here are given or lent in.</summary>
        public CallScope? Scope { get; init; }

        public byte[] ToArray() => _buffer.AsSpan(0, _length).ToArray();

        public void Bool(bool value) => U8(value ? (byte)1 : (byte)0);

        public void U8(byte value) => Grow(1)[0] = value;

        public void S8(sbyte value) => Grow(1)[0] = unchecked((byte)value);

        public void U16(ushort value) => BinaryPrimitives.WriteUInt16LittleEndian(Grow(2), value);

        public void S16(short value) => BinaryPrimitives.WriteInt16LittleEndian(Grow(2), value);

        public void U32(uint value) => BinaryPrimitives.WriteUInt32LittleEndian(Grow(4), value);

        public void S32(int value) => BinaryPrimitives.WriteInt32LittleEndian(Grow(4), value);

        public void U64(ulong value) => BinaryPrimitives.WriteUInt64LittleEndian(Grow(8), value);

        public void S64(long value) => BinaryPrimitives.WriteInt64LittleEndian(Grow(8), value);

        public void F32(float value) => BinaryPrimitives.WriteSingleLittleEndian(Grow(4), value);

        public void F64(double value) => BinaryPrimitives.WriteDoubleLittleEndian(Grow(8), value);

        public void Char(Rune value) => U32((uint)value.Value);

        public void String(string value)
        {
            ArgumentNullException.ThrowIfNull(value);
            var length = StrictUtf8.GetByteCount(value);
            Length(length);
            StrictUtf8.GetBytes(value, Grow(length));
        }

        /// <summary>Writes the element count of a <c>list</c>.</summary>
        public void Length(int count) => U32(checked((uint)count));

        /// <summary>Writes a <c>list&lt;u8&gt;</c>.</summary>
        public void Bytes(byte[] value)
        {
            ArgumentNullException.ThrowIfNull(value);
            Length(value.Length);
            value.CopyTo(Grow(value.Length));
        }

        /// <summary>Writes a <c>list&lt;u8, N&gt;</c>.</summary>
        public void FixedBytes(byte[] value, int count)
        {
            CheckFixedLength(value, count);
            value.CopyTo(Grow(count));
        }

        /// <summary>Writes the low <paramref name="byteCount"/> bytes of a <c>flags</c> bitmask.</summary>
        public void Flags(uint bits, int byteCount)
        {
            var bytes = Grow(byteCount);
            for (var i = 0; i < byteCount; i++)
            {
                bytes[i] = (byte)(bits >> (8 * i));
            }
        }

        /// <summary>Throws unless a fixed-length list has <paramref name="count"/> elements.</summary>
        public static void CheckFixedLength<T>(IReadOnlyCollection<T> value, int count)
        {
            ArgumentNullException.ThrowIfNull(value);
            if (value.Count != count)
            {
                throw new ArgumentException($"fixed-length list needs {count} elements, got {value.Count}");
            }
        }

        private Span<byte> Grow(int count)
        {
            if (_buffer.Length - _length < count)
            {
                Array.Resize(ref _buffer, Math.Max(checked(_length + count), _buffer.Length * 2));
            }
            var span = _buffer.AsSpan(_length, count);
            _length += count;
            return span;
        }
    }

    /// <summary>Decodes values in the Hyperlight component wire format.</summary>
    public ref struct WireReader
    {
        private static readonly UTF8Encoding StrictUtf8 = new(false, true);

        private readonly ReadOnlySpan<byte> _data;
        private int _position;

        public WireReader(ReadOnlySpan<byte> data)
        {
            _data = data;
            _position = 0;
        }

        /// <summary>The generated world's resource tables, for reading handles.</summary>
        public object? Resources { get; init; }

        public bool Bool() => U8() != 0;

        public byte U8() => Take(1)[0];

        public sbyte S8() => unchecked((sbyte)Take(1)[0]);

        public ushort U16() => BinaryPrimitives.ReadUInt16LittleEndian(Take(2));

        public short S16() => BinaryPrimitives.ReadInt16LittleEndian(Take(2));

        public uint U32() => BinaryPrimitives.ReadUInt32LittleEndian(Take(4));

        public int S32() => BinaryPrimitives.ReadInt32LittleEndian(Take(4));

        public ulong U64() => BinaryPrimitives.ReadUInt64LittleEndian(Take(8));

        public long S64() => BinaryPrimitives.ReadInt64LittleEndian(Take(8));

        public float F32() => BinaryPrimitives.ReadSingleLittleEndian(Take(4));

        public double F64() => BinaryPrimitives.ReadDoubleLittleEndian(Take(8));

        public Rune Char()
        {
            var value = U32();
            if (!Rune.IsValid(value))
            {
                throw new WireFormatException($"invalid char {value:x}");
            }
            return new Rune(value);
        }

        public string String()
        {
            var bytes = Take(Length());
            try
            {
                return StrictUtf8.GetString(bytes);
            }
            catch (DecoderFallbackException e)
            {
                throw new WireFormatException($"invalid UTF-8: {e.Message}");
            }
        }

        /// <summary>Reads the element count of a <c>list</c>.</summary>
        public int Length()
        {
            var count = U32();
            // Every element takes at least one byte.
            if (count > (uint)(_data.Length - _position))
            {
                throw new WireFormatException($"list length {count} exceeds remaining input");
            }
            return (int)count;
        }

        /// <summary>Reads a <c>list&lt;u8&gt;</c>.</summary>
        public byte[] Bytes() => Take(Length()).ToArray();

        /// <summary>Reads a <c>list&lt;u8, N&gt;</c>.</summary>
        public byte[] FixedBytes(int count) => Take(count).ToArray();

        /// <summary>Reads a four byte tag that must be below <paramref name="cases"/>.</summary>
        public uint Tag32(uint cases)
        {
            var tag = U32();
            if (tag >= cases)
            {
                throw new WireFormatException($"invalid tag {tag}");
            }
            return tag;
        }

        /// <summary>
        /// Reads a <c>flags</c> bitmask of <paramref name="byteCount"/> bytes
        /// and keeps the bits in <paramref name="mask"/>.
        /// </summary>
        public uint Flags(int byteCount, uint mask)
        {
            var bytes = Take(byteCount);
            uint bits = 0;
            for (var i = 0; i < byteCount; i++)
            {
                bits |= (uint)bytes[i] << (8 * i);
            }
            return bits & mask;
        }

        private ReadOnlySpan<byte> Take(int count)
        {
            if ((uint)count > (uint)(_data.Length - _position))
            {
                throw new WireFormatException("unexpected end of input");
            }
            var span = _data.Slice(_position, count);
            _position += count;
            return span;
        }
    }

    /// <summary>
    /// Host objects of one resource type that the guest refers to by
    /// handle. A handle is an index into the table.
    /// </summary>
    public sealed class ResourceTable<T> where T : class
    {
        private readonly List<Entry> _entries = new();

        private sealed class Entry
        {
            public T? Value;
            public bool Lent;
        }

        /// <summary>
        /// Gives <paramref name="value"/> to the guest. With
        /// <paramref name="scope"/>, the gift is undone unless the call
        /// starts.
        /// </summary>
        public uint Give(T value, CallScope? scope = null)
        {
            var handle = Add(value, lent: false);
            scope?.AddGift(() => Clear(handle));
            return handle;
        }

        /// <summary>Lends <paramref name="value"/> to the guest until <paramref name="scope"/> ends.</summary>
        public uint Lend(T value, CallScope scope)
        {
            ArgumentNullException.ThrowIfNull(scope);
            var handle = Add(value, lent: true);
            scope.AddLoan(() => Clear(handle));
            return handle;
        }

        private void Clear(uint handle)
        {
            lock (_entries)
            {
                _entries[(int)handle].Value = null;
            }
        }

        /// <summary>Takes back a value the guest owns.</summary>
        public T Take(uint handle)
        {
            lock (_entries)
            {
                var entry = Get(handle);
                if (entry.Lent || entry.Value is null)
                {
                    throw new WireFormatException($"resource handle {handle} is not owned");
                }
                var value = entry.Value;
                entry.Value = null;
                return value;
            }
        }

        /// <summary>Returns the value for a handle the guest lends back to the host.</summary>
        public T Borrow(uint handle)
        {
            lock (_entries)
            {
                return Get(handle).Value ?? throw new WireFormatException($"resource handle {handle} is not valid");
            }
        }

        private uint Add(T value, bool lent)
        {
            ArgumentNullException.ThrowIfNull(value);
            lock (_entries)
            {
                _entries.Add(new Entry { Value = value, Lent = lent });
                return checked((uint)(_entries.Count - 1));
            }
        }

        private Entry Get(uint handle) =>
            handle < (uint)_entries.Count
                ? _entries[(int)handle]
                : throw new WireFormatException($"invalid resource handle {handle}");
    }

    /// <summary>
    /// Resources lent to the guest for one call, and resources given to it
    /// before the call starts.
    /// </summary>
    public sealed class CallScope
    {
        private List<Action>? _loans;
        private List<Action>? _gifts;

        internal void AddLoan(Action revoke) => (_loans ??= new()).Add(revoke);

        internal void AddGift(Action undo) => (_gifts ??= new()).Add(undo);

        /// <summary>Marks the call as started, which completes the gifts.</summary>
        public void Commit() => _gifts?.Clear();

        /// <summary>Revokes every loan, and undoes gifts made before <see cref="Commit"/>.</summary>
        public void End()
        {
            Run(_gifts);
            Run(_loans);
        }

        private static void Run(List<Action>? actions)
        {
            if (actions is null)
            {
                return;
            }
            foreach (var action in actions)
            {
                action();
            }
            actions.Clear();
        }
    }
}