diff --git a/Capnp.Net.Runtime.Tests/RpcSchemaTests.cs b/Capnp.Net.Runtime.Tests/RpcSchemaTests.cs index 82ab2d0..0a95dc6 100644 --- a/Capnp.Net.Runtime.Tests/RpcSchemaTests.cs +++ b/Capnp.Net.Runtime.Tests/RpcSchemaTests.cs @@ -378,6 +378,10 @@ namespace Capnp.Net.Runtime.Tests var mb = MessageBuilder.Create(); var root = mb.BuildRoot(); obj.Serialize(root); + using (var tr = new FrameTracing.RpcFrameTracer(Console.Out)) + { + tr.TraceFrame(FrameTracing.FrameDirection.Tx, mb.Frame); + } var d = (DeserializerState)root; var obj2 = new TD(); obj2.Deserialize(d); diff --git a/Capnp.Net.Runtime.Tests/SerializationTests.cs b/Capnp.Net.Runtime.Tests/SerializationTests.cs index 153feb8..67ae5bc 100644 --- a/Capnp.Net.Runtime.Tests/SerializationTests.cs +++ b/Capnp.Net.Runtime.Tests/SerializationTests.cs @@ -1,4 +1,5 @@ using Capnp.Net.Runtime.Tests.GenImpls; +using Capnp.Rpc; using Capnproto_test.Capnp.Test; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; @@ -229,7 +230,7 @@ namespace Capnp.Net.Runtime.Tests } [TestMethod] - public void ListOfStructs() + public void ListOfStructs1() { var b = MessageBuilder.Create(); var list = b.CreateObject>(); @@ -259,6 +260,19 @@ namespace Capnp.Net.Runtime.Tests Assert.AreEqual("3", list3[3].SomeText); } + [TestMethod] + public void ListOfStructs2() + { + var b = MessageBuilder.Create(); + var list = b.CreateObject(); + list.SetListOfStructs(3, 4, 5); + list.SetListOfStructs(3, 4, 5); + Assert.ThrowsException(() => list.SetListOfStructs(1, 4, 5)); + Assert.ThrowsException(() => list.SetListOfStructs(3, 1, 5)); + Assert.ThrowsException(() => list.SetListOfStructs(3, 4, 1)); + Assert.ThrowsException(() => list.StructWriteData(0, 1, 1)); + } + [TestMethod] public void ListOfText() { @@ -741,5 +755,171 @@ namespace Capnp.Net.Runtime.Tests Assert.ThrowsException(() => { var _ = list[0]; }); Assert.AreEqual(0, list.ToArray().Length); } + + class TestSerializerStateStruct11 : SerializerState + { + public TestSerializerStateStruct11() + { + SetStruct(1, 1); + } + } + + class TestSerializerStateStruct20 : SerializerState + { + public TestSerializerStateStruct20() + { + SetStruct(2, 0); + } + } + + class TestSerializerStateStruct11X : SerializerState + { + public TestSerializerStateStruct11X() + { + SetStruct(1, 1); + } + } + + [TestMethod] + public void SerializerStateInvalidRewrap1() + { + var dss = new DynamicSerializerState(MessageBuilder.Create()); + dss.SetListOfValues(8, 1); + Assert.ThrowsException(() => dss.Rewrap()); + } + + [TestMethod] + public void SerializerStateInvalidRewrap2() + { + var dss = new DynamicSerializerState(); + dss.SetStruct(1, 0); + Assert.ThrowsException(() => dss.Rewrap()); + } + + [TestMethod] + public void SerializerStateInvalidRewrap3() + { + var dss = new DynamicSerializerState(); + dss.SetStruct(0, 1); + Assert.ThrowsException(() => dss.Rewrap()); + } + + [TestMethod] + public void SerializerStateInvalidRewrap4() + { + var dss = new DynamicSerializerState(); + dss.SetStruct(1, 0); + Assert.ThrowsException(() => dss.Rewrap()); + } + + [TestMethod] + public void UnboundSerializerState() + { + var dss = new DynamicSerializerState(); + dss.SetStruct(1, 0); + Assert.ThrowsException(() => dss.WriteData(0, 0)); + } + + [TestMethod] + public void LinkBadUsage1() + { + var mb = MessageBuilder.Create(); + var dss = mb.CreateObject(); + dss.SetStruct(0, 1); + Assert.ThrowsException(() => dss.Link(0, null)); + Assert.ThrowsException(() => dss.Link(-1, dss)); + Assert.ThrowsException(() => dss.Link(1, dss)); + dss.Link(0, dss); + Assert.ThrowsException(() => dss.Link(0, mb.CreateObject())); + } + + [TestMethod] + public void LinkBadUsage2() + { + var dss = new DynamicSerializerState(MessageBuilder.Create()); + dss.SetListOfPointers(1); + Assert.ThrowsException(() => dss.Link(0, null)); + Assert.ThrowsException(() => dss.Link(-1, dss)); + Assert.ThrowsException(() => dss.Link(1, dss)); + } + + [TestMethod] + public void StructReadCapNoCapTable() + { + var dss = new DynamicSerializerState(MessageBuilder.Create()); + dss.SetStruct(0, 1); + Assert.ThrowsException(() => dss.ReadCap(0)); + } + + [TestMethod] + public void StructReadCap() + { + var dss = DynamicSerializerState.CreateForRpc(); + dss.SetStruct(0, 3); + Assert.ThrowsException(() => dss.ReadCap(-1)); + Assert.ThrowsException(() => dss.ReadCap(100)); + Assert.IsTrue(dss.ReadCap(0).IsNull); + dss.LinkToCapability(1, 99); + dss.Link(2, dss); + dss.Allocate(); + Assert.IsTrue(dss.ReadCap(0).IsNull); + Assert.IsTrue(dss.ReadCap(0) is Proxy proxy && proxy.IsNull); + Assert.ThrowsException(() => dss.ReadCap(1)); + Assert.ThrowsException(() => dss.ReadCap(2)); + } + + [TestMethod] + public void Rewrap() + { + var mb = MessageBuilder.Create(); + var list1 = mb.CreateObject>(); + list1.Init(3); + var list2 = list1.Rewrap>(); + list2[0].WriteData(0, 0); + Assert.ThrowsException(() => list2.Rewrap()); + var obj = mb.CreateObject(); + var obj2 = obj.Rewrap(); + Assert.ThrowsException(() => obj2.Rewrap()); + } + + [TestMethod] + public void AllocatedNil() + { + var mb = MessageBuilder.Create(); + mb.InitCapTable(); + var dss = mb.CreateObject(); + dss.Allocate(); + Assert.ThrowsException(() => dss.SetCapability(0)); + dss.SetCapability(null); + Assert.ThrowsException(() => dss.SetListOfPointers(1)); + Assert.ThrowsException(() => dss.SetListOfStructs(1, 1, 1)); + Assert.ThrowsException(() => dss.SetListOfValues(8, 1)); + Assert.ThrowsException(() => dss.SetObject(mb.CreateObject())); + dss.SetObject(null); + Assert.ThrowsException(() => dss.SetStruct(1, 1)); + } + + [TestMethod] + public void SetCapability1() + { + var mb = MessageBuilder.Create(); + mb.InitCapTable(); + var dss = mb.CreateObject(); + dss.SetStruct(1, 1); + Assert.ThrowsException(() => dss.SetCapability(null)); + Assert.ThrowsException(() => dss.SetCapability(0)); + } + + [TestMethod] + public void SetCapability2() + { + var mb = MessageBuilder.Create(); + mb.InitCapTable(); + var dss = mb.CreateObject(); + dss.SetCapability(7); + dss.SetCapability(7); + Assert.ThrowsException(() => dss.SetCapability(8)); + Assert.ThrowsException(() => dss.SetCapability(null)); + } } } diff --git a/Capnp.Net.Runtime.Tests/Testsuite.cs b/Capnp.Net.Runtime.Tests/Testsuite.cs index 90b4675..443beea 100644 --- a/Capnp.Net.Runtime.Tests/Testsuite.cs +++ b/Capnp.Net.Runtime.Tests/Testsuite.cs @@ -571,12 +571,12 @@ namespace Capnp.Net.Runtime.Tests task1.Result.Dispose(); testbed.FlushCommunication(); - Assert.AreEqual(1, counters.HandleCount); + Assert.IsTrue(SpinWait.SpinUntil(() => counters.HandleCount == 1, TestBase.ShortTimeout)); task2.Result.Dispose(); testbed.FlushCommunication(); - Assert.AreEqual(0, counters.HandleCount); + Assert.IsTrue(SpinWait.SpinUntil(() => counters.HandleCount == 0, TestBase.ShortTimeout)); } } diff --git a/Capnp.Net.Runtime/DeserializerState.cs b/Capnp.Net.Runtime/DeserializerState.cs index 735ae7e..6fbb996 100644 --- a/Capnp.Net.Runtime/DeserializerState.cs +++ b/Capnp.Net.Runtime/DeserializerState.cs @@ -49,7 +49,7 @@ namespace Capnp /// /// The capabilities imported from the capability table. Only valid in RPC context. /// - public IList? Caps { get; set; } + public IList? Caps { get; set; } /// /// Current segment (essentially Segments[CurrentSegmentIndex]) /// diff --git a/Capnp.Net.Runtime/DynamicSerializerState.cs b/Capnp.Net.Runtime/DynamicSerializerState.cs index fe750d9..d6092c6 100644 --- a/Capnp.Net.Runtime/DynamicSerializerState.cs +++ b/Capnp.Net.Runtime/DynamicSerializerState.cs @@ -79,7 +79,7 @@ namespace Capnp /// This state does neither describe a struct, nor a list of pointers /// Another state is already linked to the specified position (sorry, no overwrite allowed) /// - public new void LinkToCapability(int slot, uint capabilityIndex) => base.LinkToCapability(slot, capabilityIndex); + public new void LinkToCapability(int slot, uint? capabilityIndex) => base.LinkToCapability(slot, capabilityIndex); /// /// Determines the underlying object to be a struct. @@ -89,6 +89,8 @@ namespace Capnp /// The object type was already set to something different public new void SetStruct(ushort dataCount, ushort ptrCount) => base.SetStruct(dataCount, ptrCount); + public new void SetCapability(uint? capabilityIndex) => base.SetCapability(capabilityIndex); + /// /// Determines the underlying object to be a list of (primitive) values. /// diff --git a/Capnp.Net.Runtime/MessageBuilder.cs b/Capnp.Net.Runtime/MessageBuilder.cs index 6afb40e..ba2f556 100644 --- a/Capnp.Net.Runtime/MessageBuilder.cs +++ b/Capnp.Net.Runtime/MessageBuilder.cs @@ -10,7 +10,7 @@ namespace Capnp { readonly ISegmentAllocator _allocator; readonly DynamicSerializerState _rootPtrBuilder; - List? _capTable; + List? _capTable; MessageBuilder(ISegmentAllocator allocator) { @@ -92,13 +92,13 @@ namespace Capnp if (_capTable != null) throw new InvalidOperationException("Capability table was already initialized"); - _capTable = new List(); + _capTable = new List(); } /// /// Returns this message builder's segment allocator. /// public ISegmentAllocator Allocator => _allocator; - internal List? Caps => _capTable; + internal List? Caps => _capTable; } } \ No newline at end of file diff --git a/Capnp.Net.Runtime/Rpc/Impatient.cs b/Capnp.Net.Runtime/Rpc/Impatient.cs index 4b73082..bddc2be 100644 --- a/Capnp.Net.Runtime/Rpc/Impatient.cs +++ b/Capnp.Net.Runtime/Rpc/Impatient.cs @@ -180,7 +180,7 @@ namespace Capnp.Rpc } } - public static async Task Unwrap(this TInterface cap) where TInterface: class, IDisposable + public static async Task Unwrap(this TInterface cap) where TInterface: class, IDisposable { using var proxy = cap as Proxy; @@ -188,6 +188,9 @@ namespace Capnp.Rpc return cap; var unwrapped = await proxy.ConsumedCap.Unwrap(); + if (unwrapped == null) + return null; + return ((CapabilityReflection.CreateProxy(unwrapped)) as TInterface)!; } diff --git a/Capnp.Net.Runtime/Rpc/LazyCapability.cs b/Capnp.Net.Runtime/Rpc/LazyCapability.cs index 6b11347..880b837 100644 --- a/Capnp.Net.Runtime/Rpc/LazyCapability.cs +++ b/Capnp.Net.Runtime/Rpc/LazyCapability.cs @@ -16,8 +16,6 @@ namespace Capnp.Rpc return new LazyCapability(Task.FromCanceled(token)); } - public static LazyCapability Null => CreateBrokenCap("Null capability"); - readonly Task? _proxyTask; public LazyCapability(Task capabilityTask) diff --git a/Capnp.Net.Runtime/Rpc/PendingAnswer.cs b/Capnp.Net.Runtime/Rpc/PendingAnswer.cs index 4bc0f89..a195b0c 100644 --- a/Capnp.Net.Runtime/Rpc/PendingAnswer.cs +++ b/Capnp.Net.Runtime/Rpc/PendingAnswer.cs @@ -108,7 +108,7 @@ namespace Capnp.Rpc try { var cap = aorcq.Answer.Caps![(int)cur.CapabilityIndex]; - proxy = new Proxy(cap ?? LazyCapability.Null); + proxy = new Proxy(cap); } catch (ArgumentOutOfRangeException) { diff --git a/Capnp.Net.Runtime/Rpc/Proxy.cs b/Capnp.Net.Runtime/Rpc/Proxy.cs index 65616b0..53ec1af 100644 --- a/Capnp.Net.Runtime/Rpc/Proxy.cs +++ b/Capnp.Net.Runtime/Rpc/Proxy.cs @@ -197,16 +197,13 @@ namespace Capnp.Rpc /// Whether to Dispose() this Proxy instance /// Proxy for desired capability interface /// did not qualify as capability interface. - /// This capability is broken, or mismatch between generic type arguments (if capability interface is generic). + /// Mismatch between generic type arguments (if capability interface is generic). /// Mismatch between generic type arguments (if capability interface is generic). /// Problem with instatiating the Proxy (constructor threw exception). /// Caller does not have permission to invoke the Proxy constructor. /// Problem with building the Proxy type, or problem with loading some dependent class. public T Cast(bool disposeThis) where T: class { - if (IsNull) - throw new InvalidOperationException("Capability is broken"); - using (disposeThis ? this : null) { return (CapabilityReflection.CreateProxy(ConsumedCap) as T)!; diff --git a/Capnp.Net.Runtime/Rpc/ResolvingCapabilityExtensions.cs b/Capnp.Net.Runtime/Rpc/ResolvingCapabilityExtensions.cs index 86c5fa8..23b3fab 100644 --- a/Capnp.Net.Runtime/Rpc/ResolvingCapabilityExtensions.cs +++ b/Capnp.Net.Runtime/Rpc/ResolvingCapabilityExtensions.cs @@ -5,13 +5,11 @@ namespace Capnp.Rpc { static class ResolvingCapabilityExtensions { - public static async Task Unwrap(this ConsumedCapability? cap) + public static async Task Unwrap(this ConsumedCapability? cap) { - cap ??= LazyCapability.Null; - while (cap is IResolvingCapability resolving) { - cap = await resolving.WhenResolved ?? LazyCapability.Null; + cap = await resolving.WhenResolved; } return cap; @@ -66,7 +64,7 @@ namespace Capnp.Rpc switch (obj) { case Proxy proxy: return proxy; - case null: return new Proxy(LazyCapability.Null); + case null: return new Proxy(null); default: return BareProxy.FromImpl(obj); } } diff --git a/Capnp.Net.Runtime/Rpc/RpcEngine.cs b/Capnp.Net.Runtime/Rpc/RpcEngine.cs index c59c3d8..e7e0df1 100644 --- a/Capnp.Net.Runtime/Rpc/RpcEngine.cs +++ b/Capnp.Net.Runtime/Rpc/RpcEngine.cs @@ -1347,9 +1347,9 @@ namespace Capnp.Rpc } } - internal IList ImportCapTable(Payload.READER payload) + internal IList ImportCapTable(Payload.READER payload) { - var list = new List(); + var list = new List(); if (payload.CapTable != null) { @@ -1378,16 +1378,8 @@ namespace Capnp.Rpc foreach (var cap in state.MsgBuilder.Caps) { var capDesc = payload.CapTable[i++]; - - if (cap == null) - { - LazyCapability.Null.Export(this, capDesc); - } - else - { - postAction += cap.Export(this, capDesc); - cap.Release(); - } + postAction += cap.Export(this, capDesc); + cap.Release(); } Tx(state.MsgBuilder.Frame); diff --git a/Capnp.Net.Runtime/Rpc/Vine.cs b/Capnp.Net.Runtime/Rpc/Vine.cs index 1542681..3572f6c 100644 --- a/Capnp.Net.Runtime/Rpc/Vine.cs +++ b/Capnp.Net.Runtime/Rpc/Vine.cs @@ -7,7 +7,7 @@ namespace Capnp.Rpc { class Vine : Skeleton { - public static Skeleton Create(ConsumedCapability cap) + public static Skeleton Create(ConsumedCapability? cap) { if (cap is LocalCapability lcap) return lcap.ProvidedCap; @@ -15,9 +15,9 @@ namespace Capnp.Rpc return new Vine(cap); } - Vine(ConsumedCapability consumedCap) + Vine(ConsumedCapability? consumedCap) { - Proxy = new Proxy(consumedCap ?? throw new ArgumentNullException(nameof(consumedCap))); + Proxy = new Proxy(consumedCap); #if DebugFinalizers CreatorStackTrace = Environment.StackTrace; diff --git a/Capnp.Net.Runtime/SerializerState.cs b/Capnp.Net.Runtime/SerializerState.cs index 42c5522..c89ca61 100644 --- a/Capnp.Net.Runtime/SerializerState.cs +++ b/Capnp.Net.Runtime/SerializerState.cs @@ -30,7 +30,7 @@ namespace Capnp internal MessageBuilder? MsgBuilder { get; set; } internal ISegmentAllocator? Allocator => MsgBuilder?.Allocator; - internal List? Caps => MsgBuilder?.Caps; + internal List? Caps => MsgBuilder?.Caps; internal SerializerState? Owner { get; set; } internal int OwnerSlot { get; set; } internal uint SegmentIndex { get; set; } @@ -156,7 +156,7 @@ namespace Capnp /// Returns the allocated memory slice (given this state already is allocated). Note that this definition is somewhat /// non-symmetric to DeserializerState.RawData. Never mind: You should not use it directly, anyway. /// - public Span RawData => SegmentSpan.Slice(Offset, (int)WordsAllocated); + public Span RawData => IsAllocated ? SegmentSpan.Slice(Offset, (int)WordsAllocated) : Span.Empty; /// /// The kind of object this state currently represents. @@ -170,12 +170,6 @@ namespace Capnp SegmentIndex = 0; Offset = 0; } - else if (Owner?.Kind == ObjectKind.ListOfStructs) - { - Owner.Allocate(); - SegmentIndex = Owner.SegmentIndex; - Offset = Owner.Offset + OwnerSlot + 1; - } else { if (Allocator == null) @@ -242,25 +236,43 @@ namespace Capnp } } - internal void EncodePointer(int offset, SerializerState target, bool allowCopy) + internal Rpc.ConsumedCapability? DecodeCapPointer(int offset) { - if (target == null) - throw new ArgumentNullException(nameof(target)); + if (Caps == null) + throw new InvalidOperationException("Capbility table not set"); - if (!target.IsAllocated) - throw new InvalidOperationException("Target must be allocated before a pointer can be built"); - - if (MsgBuilder == null) - throw Unbound(); - - try + if (!IsAllocated) { - if (SegmentSpan[offset] != 0) - throw new InvalidOperationException("Won't replace an already allocated pointer to prevent memory leaks and security flaws"); + return null; } - catch (IndexOutOfRangeException) + + WirePointer pointer = RawData[offset]; + + if (pointer.IsNull) { - throw new ArgumentOutOfRangeException(nameof(offset)); + return null; + } + + if (pointer.Kind != PointerKind.Other) + { + throw new Rpc.RpcException( + "Expected a capability pointer, but got something different"); + } + + if (pointer.CapabilityIndex >= Caps.Count) + { + throw new Rpc.RpcException( + "Capability index out of range"); + } + + return Caps[(int)pointer.CapabilityIndex]; + } + + void EncodePointer(int offset, SerializerState target, bool allowCopy) + { + if (SegmentSpan[offset] != 0) + { + throw new InvalidOperationException("Won't replace an already allocated pointer to prevent memory leaks and security flaws"); } if (target.Allocator != null && @@ -375,31 +387,6 @@ namespace Capnp } } - internal Rpc.ConsumedCapability? DecodeCapPointer(int offset) - { - if (offset < 0) - throw new IndexOutOfRangeException(nameof(offset)); - - if (Caps == null) - throw new InvalidOperationException("Capbility table not set"); - - WirePointer pointer = RawData[offset]; - - if (pointer.Kind != PointerKind.Other) - { - throw new Rpc.RpcException( - "Expected a capability pointer, but got something different"); - } - - if (pointer.CapabilityIndex >= Caps.Count) - { - throw new Rpc.RpcException( - "Capability index out of range"); - } - - return Caps[(int)pointer.CapabilityIndex]; - } - /// /// Links a sub-item (struct field or list element) of this state to another state. Usually, this operation is not necessary, since objects are constructed top-down. /// However, there might be some advanced scenarios where you want to reference the same object twice (also interesting for designing amplification attacks). @@ -471,7 +458,7 @@ namespace Capnp /// This state does neither describe a struct, nor a list of pointers /// Another state is already linked to the specified position (sorry, no overwrite allowed) /// - protected void LinkToCapability(int slot, uint capabilityIndex) + protected void LinkToCapability(int slot, uint? capabilityIndex) { var cstate = new SerializerState(); cstate.SetCapability(capabilityIndex); @@ -511,19 +498,29 @@ namespace Capnp } } - public void SetCapability(uint capabilityIndex) + protected void SetCapability(uint? capabilityIndex) { - if (Kind == ObjectKind.Nil) + if (capabilityIndex.HasValue) { - VerifyNotYetAllocated(); + if (Kind == ObjectKind.Nil) + { + VerifyNotYetAllocated(); - Kind = ObjectKind.Capability; - CapabilityIndex = capabilityIndex; - Allocate(); + Kind = ObjectKind.Capability; + CapabilityIndex = capabilityIndex.Value; + Allocate(); + } + else if (Kind != ObjectKind.Capability || CapabilityIndex != capabilityIndex) + { + throw AlreadySet(); + } } - else if (Kind != ObjectKind.Capability || CapabilityIndex != capabilityIndex) + else { - throw AlreadySet(); + if (Kind != ObjectKind.Nil) + { + throw AlreadySet(); + } } } @@ -1225,10 +1222,13 @@ namespace Capnp /// Adds an entry to the capability table if the provided capability does not yet exist. /// /// The low-level capability object to provide. - /// Index of the given capability in the capability table + /// Index of the given capability in the capability table, null if capability is null /// The underlying message builder was not configured for capability table support. - public uint ProvideCapability(Rpc.ConsumedCapability? capability) + public uint? ProvideCapability(Rpc.ConsumedCapability? capability) { + if (capability == null) + return null; + if (Caps == null) throw new InvalidOperationException("Underlying MessageBuilder was not enabled to support capabilities"); @@ -1252,7 +1252,7 @@ namespace Capnp /// The underlying message builder was not configured for capability table support. public uint ProvideCapability(Rpc.Skeleton capability) { - return ProvideCapability(Rpc.LocalCapability.Create(capability)); + return ProvideCapability(Rpc.LocalCapability.Create(capability))!.Value; } /// @@ -1267,12 +1267,12 @@ namespace Capnp /// /// Index of the given capability in the capability table /// The underlying message builder was not configured for capability table support. - public uint ProvideCapability(object? obj) + public uint? ProvideCapability(object? obj) { switch (obj) { case null: - return ProvideCapability(default(Rpc.ConsumedCapability)); + return null; case Rpc.Proxy proxy: using (proxy) return ProvideCapability(proxy.ConsumedCap); case Rpc.ConsumedCapability consumedCapability: @@ -1348,8 +1348,8 @@ namespace Capnp if (Kind != ObjectKind.Struct && Kind != ObjectKind.Nil) throw new InvalidOperationException("Allowed on structs only"); - if (index >= StructPtrCount) - return null; + if (index < 0 || index >= StructPtrCount) + throw new ArgumentOutOfRangeException(nameof(index)); return DecodeCapPointer(index + StructDataCount); } diff --git a/Capnp.Net.Runtime/WirePointer.cs b/Capnp.Net.Runtime/WirePointer.cs index 197886e..5f17dca 100644 --- a/Capnp.Net.Runtime/WirePointer.cs +++ b/Capnp.Net.Runtime/WirePointer.cs @@ -219,10 +219,13 @@ namespace Capnp /// /// Encodes a capability pointer. /// - /// capability index - public void SetCapability(uint index) + /// capability index, 'null' means 'null pointer' + public void SetCapability(uint? index) { - _ptrData = ((ulong)index << 32) | (ulong)PointerKind.Other; + if (index.HasValue) + _ptrData = ((ulong)index << 32) | (ulong)PointerKind.Other; + else + _ptrData = 0; } } } \ No newline at end of file