Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions dotnet-bridge-benchmarks/AbiBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using NativeScriptBridge;

/// <summary>
/// Full managed cost of one bridge call as the runtime pays it: the request goes through the
/// [UnmanagedCallersOnly] InvokeBinary entry point (via an unmanaged function pointer, like Rust
/// calls it) and the unmanaged response buffer is freed through Free.
///
/// dotnet run -c Release -- --filter *AbiBenchmarks* --job short
/// </summary>
[MemoryDiagnoser]
public unsafe class AbiBenchmarks
{
private static readonly delegate* unmanaged[Cdecl]<byte*, int, byte**, int*, int> s_invoke = &Bridge.InvokeBinary;
private static readonly delegate* unmanaged[Cdecl]<byte*, void> s_free = &Bridge.Free;

private byte[] _staticCbrt = null!;
private byte[] _staticCbrtAsm = null!;
private byte[] _overloadMax = null!;
private byte[] _overloadGetExtension = null!;
private byte[] _stringArg = null!;
private byte[] _propGet = null!;
private byte[] _propSet = null!;
private byte[] _ctor = null!;
private byte[] _returnHandle = null!;

[GlobalSetup]
public void Setup()
{
Bridge.ClearCaches();
_staticCbrt = Static("System.Math", "", "Cbrt", w => { w.WriteByte(0x03); w.WriteI32(27); });
_staticCbrtAsm = Static("System.Math", "System.Runtime", "Cbrt", w => { w.WriteByte(0x03); w.WriteI32(27); });
_overloadMax = Static("System.Math", "", "Max", w => { w.WriteByte(0x03); w.WriteI32(300); w.WriteByte(0x03); w.WriteI32(7); }, 2);
_overloadGetExtension = Static("System.IO.Path", "", "GetExtension", w => { w.WriteByte(0x05); w.WriteString16("file.txt"); });
_stringArg = Static("System.String", "", "IsNullOrEmpty", w => { w.WriteByte(0x05); w.WriteString16("abc"); });
_ctor = Packet(w => { w.WriteByte(0x03); w.WriteString16("System.Text.StringBuilder"); w.WriteString16(""); w.WriteByte(0); });
_returnHandle = Static("System.Diagnostics.Stopwatch", "", "StartNew", null, 0);

var sb = Bridge.Dispatch(new InvokeRequest(null, "System.Text.StringBuilder", ".ctor", null, null)).HandleId();
_propGet = Packet(w => { w.WriteByte(0x01); w.WriteI32(sb); w.WriteString16("get_Length"); w.WriteByte(0); });
_propSet = Packet(w => { w.WriteByte(0x01); w.WriteI32(sb); w.WriteString16("set_Length"); w.WriteByte(1); w.WriteByte(0x03); w.WriteI32(0); });

foreach (var p in new[] { _staticCbrt, _staticCbrtAsm, _overloadMax, _overloadGetExtension, _stringArg, _propGet, _propSet })
{
var resp = Call(p);
if (resp[0] == 0xFF) throw new InvalidOperationException("bridge error: " + System.Text.Encoding.UTF8.GetString(resp, 5, resp.Length - 5));
}
}

[Benchmark] public int StaticCall() => CallLen(_staticCbrt);
[Benchmark] public int StaticCallWithAssembly() => CallLen(_staticCbrtAsm);
[Benchmark] public int OverloadedInt() => CallLen(_overloadMax);
[Benchmark] public int OverloadedString() => CallLen(_overloadGetExtension);
[Benchmark] public int StringArg() => CallLen(_stringArg);
[Benchmark] public int PropertyGet() => CallLen(_propGet);
[Benchmark] public int PropertySet() => CallLen(_propSet);

[Benchmark]
public void ConstructRelease() => Release(Call(_ctor));

[Benchmark]
public void ReturnHandleRelease() => Release(Call(_returnHandle));

private void Release(byte[] handleResponse)
{
var id = BitConverter.ToInt32(handleResponse, 1);
CallLen(Packet(w => { w.WriteByte(0x04); w.WriteI32(id); }));
}

private static int CallLen(byte[] packet)
{
byte* resp = null;
int len = 0;
fixed (byte* p = packet) s_invoke(p, packet.Length, &resp, &len);
s_free(resp);
return len;
}

private static byte[] Call(byte[] packet)
{
byte* resp = null;
int len = 0;
fixed (byte* p = packet) s_invoke(p, packet.Length, &resp, &len);
var bytes = new ReadOnlySpan<byte>(resp, len).ToArray();
s_free(resp);
return bytes;
}

private static byte[] Static(string type, string assembly, string method, Action<BinWriter>? args, int argCount = 1)
=> Packet(w =>
{
w.WriteByte(0x02);
w.WriteString16(type);
w.WriteString16(assembly);
w.WriteString16(method);
w.WriteByte((byte)argCount);
args?.Invoke(w);
});

private static byte[] Packet(Action<BinWriter> write)
{
var buf = new ArrayBufferWriter<byte>(64);
write(new BinWriter(buf));
return buf.WrittenSpan.ToArray();
}
}
2 changes: 1 addition & 1 deletion dotnet-bridge-benchmarks/BridgeBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using BenchmarkDotNet.Running;
using NativeScriptBridge;

BenchmarkRunner.Run<BridgeBenchmarks>(args: args);
BenchmarkSwitcher.FromTypes([typeof(BridgeBenchmarks), typeof(AbiBenchmarks)]).Run(args);

/// <summary>
/// Measures the hot-path dispatch cost for the JSON and binary protocols.
Expand Down
231 changes: 231 additions & 0 deletions dotnet-bridge-tests/MarshalingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using NativeScriptBridge;
using Xunit;

namespace DotNetBridgeTests;

public static class MarshalingFixtures
{
public static int Sum(params int[] values) => values.Sum();
public static string Describe(string label, params object[] values) => label + ":" + values.Length;
public static int Total(List<int> values) => values.Sum();
public static int Count(IEnumerable<string> values) => values.Count();
public static List<string> Names() => ["a", "b"];
public static string[] NameArray() => ["x", "y"];
public static DateTime Epoch() => new(2020, 1, 2, 3, 4, 5, DateTimeKind.Utc);
public static int YearOf(DateTime value) => value.Year;
public static double HoursOf(DateTimeOffset value) => value.UtcDateTime.Hour;
public static int GuidVersion(Guid value) => value.Version;
}

[Collection("Bridge")]
/// Arrays and params arrays as arguments, collections and date/time values as results.
public sealed class MarshalingTests : IDisposable
{
public MarshalingTests() => Bridge.ClearCaches();
public void Dispose() => Bridge.ClearCaches();

private const string Fixtures = "DotNetBridgeTests.MarshalingFixtures";

[Fact]
public void ParamsArray_TakesTrailingArguments()
{
var result = Static("System.String", "Join", w =>
{
w.WriteByte(0x05); w.WriteString16(",");
w.WriteByte(0x05); w.WriteString16("a");
w.WriteByte(0x05); w.WriteString16("b");
w.WriteByte(0x05); w.WriteString16("c");
}, 4);
Assert.Equal("a,b,c", result.PrimitiveValue());
}

[Fact]
public void ParamsArray_ConvertsToElementType()
{
var result = Static(Fixtures, "Sum", w =>
{
w.WriteByte(0x03); w.WriteI32(1);
w.WriteByte(0x03); w.WriteI32(2);
w.WriteByte(0x03); w.WriteI32(3);
}, 3);
Assert.Equal(6, result.PrimitiveValue());
}

[Fact]
public void ParamsArray_CanBeEmpty()
{
var result = Static(Fixtures, "Describe", w => { w.WriteByte(0x05); w.WriteString16("none"); });
Assert.Equal("none:0", result.PrimitiveValue());
}

[Fact]
public void ArrayArgument_ForArrayParameter()
{
var result = Static("System.String", "Join", w =>
{
w.WriteByte(0x05); w.WriteString16("-");
WriteArray(w, "a", "b");
}, 2);
Assert.Equal("a-b", result.PrimitiveValue());
}

[Fact]
public void ArrayArgument_ForListParameter()
{
var result = Static(Fixtures, "Total", w =>
{
w.WriteByte(0x07); w.WriteU32(3);
w.WriteByte(0x03); w.WriteI32(4);
w.WriteByte(0x03); w.WriteI32(5);
w.WriteByte(0x04); w.WriteF64(6);
});
Assert.Equal(15, result.PrimitiveValue());
}

[Fact]
public void ArrayArgument_ForEnumerableParameter()
{
var result = Static(Fixtures, "Count", w => WriteArray(w, "a", "b", "c"));
Assert.Equal(3, result.PrimitiveValue());
}

[Fact]
public void ListResult_IsAnObjectNotACopy()
{
var result = Static(Fixtures, "Names", null, 0);
Assert.Equal(DispatchKind.Handle, result.Kind());
Assert.True(Bridge.s_handles.TryGetValue(result.HandleId(), out var list));
Assert.IsType<List<string>>(list);
}

[Fact]
public void ArrayResult_IsStillACopy()
{
Assert.Equal(DispatchKind.Collection, Static(Fixtures, "NameArray", null, 0).Kind());
}

[Fact]
public void CollectionHelpers_ReadAndWriteAList()
{
var list = new List<int> { 10, 20, 30 };
Assert.Equal(7, Bridge.CollectionKind(list));
Assert.Equal(3, Bridge.CollectionCount(list));
Assert.Equal(20, Bridge.ItemAt(list, 1));
Assert.Null(Bridge.ItemAt(list, 3));
Bridge.SetItemAt(list, 1, 25.0);
Assert.Equal(25, list[1]);
Assert.Equal(new object?[] { 10, 25, 30 }, Bridge.CollectionItems(list));
}

[Fact]
public void CollectionHelpers_ReadOnlyAndEnumerables()
{
IReadOnlyList<string> readOnly = new[] { "a" }.AsReadOnly();
Assert.Equal(7, Bridge.CollectionKind(readOnly));
Assert.Equal("a", Bridge.ItemAt(readOnly, 0));
Assert.Equal(1, Bridge.CollectionKind(Enumerable.Range(0, 3).Select(i => i)));
Assert.Equal(3, Bridge.CollectionCount(Enumerable.Range(0, 3).Select(i => i)));
Assert.Equal(0, Bridge.CollectionKind("text"));
Assert.Equal(0, Bridge.CollectionKind(new object()));
}

[Fact]
public void DateTimeResult_IsAnObject()
{
var result = Static(Fixtures, "Epoch", null, 0);
Assert.Equal(DispatchKind.Handle, result.Kind());
Assert.True(Bridge.s_handles.TryGetValue(result.HandleId(), out var value));
Assert.Equal(2020, ((DateTime)value!).Year);
}

[Fact]
public void IsoString_ForDateTimeParameter()
{
var result = Static(Fixtures, "YearOf", w => { w.WriteByte(0x05); w.WriteString16("2031-05-06T07:08:09.000Z"); });
Assert.Equal(2031, result.PrimitiveValue());
}

[Fact]
public void IsoString_ForDateTimeOffsetParameter()
{
var result = Static(Fixtures, "HoursOf", w => { w.WriteByte(0x05); w.WriteString16("2031-05-06T07:08:09.000Z"); });
Assert.Equal(7.0, result.PrimitiveValue());
}

[Fact]
public void String_ForGuidParameter()
{
var result = Static(Fixtures, "GuidVersion", w => { w.WriteByte(0x05); w.WriteString16(Guid.NewGuid().ToString()); });
Assert.Equal(4, result.PrimitiveValue());
}

[Fact]
public void IndexerAccessor_WithArgument_IsAMethodCall()
{
var list = new List<string> { "first" };
var id = 900001;
Bridge.s_handles[id] = list;
var buf = new ArrayBufferWriter<byte>(64);
var w = new BinWriter(buf);
w.WriteByte(0x01);
w.WriteI32(id);
w.WriteString16("get_Item");
w.WriteByte(1);
w.WriteByte(0x03); w.WriteI32(0);
var r = new BinReader(buf.WrittenSpan);
Assert.Equal("first", Bridge.DispatchBin(ref r).PrimitiveValue());
}

[Fact]
public void Indexers_AreNotListedAsProperties()
{
var buf = new ArrayBufferWriter<byte>(64);
Bridge.BuildMembersResult(typeof(List<int>)).WriteAsBin(buf);
Assert.Equal(0x08, buf.WrittenSpan[0]);
var methods = ReadStringArray(buf.WrittenSpan[1..], out var consumed);
var properties = ReadStringArray(buf.WrittenSpan[(1 + consumed)..], out _);
Assert.Contains("Count", properties);
Assert.DoesNotContain("Item", properties);
Assert.Contains("Add", methods);
}

private static string[] ReadStringArray(ReadOnlySpan<byte> span, out int consumed)
{
var count = BitConverter.ToUInt16(span);
var pos = 2;
var names = new string[count];
for (int i = 0; i < count; i++)
{
var len = BitConverter.ToUInt16(span[pos..]);
names[i] = System.Text.Encoding.UTF8.GetString(span.Slice(pos + 2, len));
pos += 2 + len;
}
consumed = pos;
return names;
}

private static void WriteArray(BinWriter w, params string[] items)
{
w.WriteByte(0x07);
w.WriteU32((uint)items.Length);
foreach (var item in items) { w.WriteByte(0x05); w.WriteString16(item); }
}

private static DispatchResult Static(string type, string method, Action<BinWriter>? args, int argCount = 1)
{
var buf = new ArrayBufferWriter<byte>(64);
var w = new BinWriter(buf);
w.WriteByte(0x02);
w.WriteString16(type);
w.WriteString16("");
w.WriteString16(method);
w.WriteByte((byte)argCount);
args?.Invoke(w);
var r = new BinReader(buf.WrittenSpan);
return Bridge.DispatchBin(ref r);
}
}
Loading
Loading