Skip to content

Commit

Permalink
Implement ctx.Identity for C# (#2091)
Browse files Browse the repository at this point in the history
  • Loading branch information
RReverser authored Jan 7, 2025
1 parent 763ba5e commit 40faad6
Show file tree
Hide file tree
Showing 11 changed files with 96 additions and 122 deletions.
148 changes: 45 additions & 103 deletions crates/bindings-csharp/BSATN.Runtime/Builtins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ namespace SpacetimeDB;

internal static class Util
{
public static Span<byte> AsBytes<T>(ref T val)
where T : struct => MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref val, 1));

/// <summary>
/// Convert this object to a BIG-ENDIAN hex string.
///
Expand All @@ -17,35 +20,28 @@ internal static class Util
///
/// (This might be wrong if the string is printed after, say, a unicode right-to-left marker.
/// But, well, what can you do.)
///
/// Similar to `Convert.ToHexString`, but that method is not available in .NET Standard
/// which we need to target for Unity support.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="val"></param>
/// <returns></returns>
public static string ToHexBigEndian<T>(T val)
where T : struct => BitConverter.ToString(AsBytesBigEndian(val).ToArray()).Replace("-", "");

/// <summary>
/// Read a value of type T from the passed span, which is assumed to be in little-endian format.
/// The behavior of this method is independent of the endianness of the host, unlike MemoryMarshal.Read.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <returns></returns>
public static T ReadLittleEndian<T>(ReadOnlySpan<byte> source)
where T : struct => Read<T>(source, !BitConverter.IsLittleEndian);

/// <summary>
/// Read a value of type T from the passed span, which is assumed to be in big-endian format.
/// The behavior of this method is independent of the endianness of the host, unlike MemoryMarshal.Read.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <returns></returns>
public static T ReadBigEndian<T>(ReadOnlySpan<byte> source)
where T : struct => Read<T>(source, BitConverter.IsLittleEndian);
where T : struct
{
var bytes = AsBytes(ref val);
// If host is little-endian, reverse the bytes.
// Note that this reverses our stack copy of `val`, not the original value, and doesn't require heap `byte[]` allocation.
if (BitConverter.IsLittleEndian)
{
bytes.Reverse();
}
#if NET5_0_OR_GREATER
return Convert.ToHexString(bytes);
#else
// Similar to `Convert.ToHexString`, but that method is not available in .NET Standard
// which we need to target for Unity support.
return BitConverter.ToString(bytes.ToArray()).Replace("-", "");
#endif
}

/// <summary>
/// Convert the passed byte array to a value of type T, optionally reversing it before performing the conversion.
Expand All @@ -54,9 +50,9 @@ public static T ReadBigEndian<T>(ReadOnlySpan<byte> source)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <param name="reverse"></param>
/// <param name="littleEndian"></param>
/// <returns></returns>
static T Read<T>(ReadOnlySpan<byte> source, bool reverse)
public static T Read<T>(ReadOnlySpan<byte> source, bool littleEndian)
where T : struct
{
Debug.Assert(
Expand All @@ -66,62 +62,25 @@ static T Read<T>(ReadOnlySpan<byte> source, bool reverse)

var result = MemoryMarshal.Read<T>(source);

if (reverse)
if (littleEndian != BitConverter.IsLittleEndian)
{
var resultSpan = MemoryMarshal.CreateSpan(ref result, 1);
MemoryMarshal.AsBytes(resultSpan).Reverse();
AsBytes(ref result).Reverse();
}

return result;
}

/// <summary>
/// Convert the passed T to a little-endian byte array.
/// The behavior of this method is independent of the endianness of the host, unlike MemoryMarshal.Read.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <returns></returns>
public static byte[] AsBytesLittleEndian<T>(T source)
where T : struct => AsBytes(source, !BitConverter.IsLittleEndian);

/// <summary>
/// Convert the passed T to a big-endian byte array.
/// The behavior of this method is independent of the endianness of the host, unlike MemoryMarshal.Read.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <returns></returns>
public static byte[] AsBytesBigEndian<T>(T source)
where T : struct => AsBytes<T>(source, BitConverter.IsLittleEndian);

/// <summary>
/// Convert the passed T to a byte array, and optionally reverse the array before returning it.
/// If the output is not reversed, it will have the native endianness of the host system.
/// (The endianness of the host system can be checked via System.BitConverter.IsLittleEndian.)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <param name="reverse"></param>
/// <returns></returns>
static byte[] AsBytes<T>(T source, bool reverse)
where T : struct
{
var result = MemoryMarshal.AsBytes([source]).ToArray();
if (reverse)
{
Array.Reverse(result, 0, result.Length);
}
return result;
}

/// <summary>
/// Convert a hex string to a byte array.
/// </summary>
/// <param name="hex"></param>
/// <returns></returns>
public static byte[] StringToByteArray(string hex)
{
#if NET5_0_OR_GREATER
return Convert.FromHexString(hex);
#else
// Manual implementation for .NET Standard compatibility.
Debug.Assert(
hex.Length % 2 == 0,
$"Expected input string (\"{hex}\") to be of even length"
Expand All @@ -134,18 +93,8 @@ public static byte[] StringToByteArray(string hex)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
#endif
}

/// <summary>
/// Read a value from a "big-endian" hex string.
/// All hex strings we expect to encounter are big-endian (store most significant bytes
/// at low indexes) so this should always be used.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="hex"></param>
/// <returns></returns>
public static T ReadFromBigEndianHexString<T>(string hex)
where T : struct => ReadBigEndian<T>(StringToByteArray(hex));
}

public readonly partial struct Unit
Expand All @@ -162,6 +111,7 @@ public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
}
}

[StructLayout(LayoutKind.Sequential)]
public readonly record struct Address
{
private readonly U128 value;
Expand All @@ -177,9 +127,9 @@ public readonly record struct Address
/// Returns null if the resulting address is the default.
/// </summary>
/// <param name="bytes"></param>
public static Address? From(byte[] bytes)
public static Address? From(ReadOnlySpan<byte> bytes)
{
var addr = new Address(Util.ReadLittleEndian<U128>(bytes));
var addr = Util.Read<Address>(bytes, littleEndian: true);
return addr == default ? null : addr;
}

Expand All @@ -196,9 +146,9 @@ public readonly record struct Address
/// Returns null if the resulting address is the default.
/// </summary>
/// <param name="bytes"></param>
public static Address? FromBigEndian(byte[] bytes)
public static Address? FromBigEndian(ReadOnlySpan<byte> bytes)
{
var addr = new Address(Util.ReadBigEndian<U128>(bytes));
var addr = Util.Read<Address>(bytes, littleEndian: false);
return addr == default ? null : addr;
}

Expand All @@ -207,18 +157,14 @@ public readonly record struct Address
/// </summary>
/// <param name="hex"></param>
/// <returns></returns>
public static Address? FromHexString(string hex)
{
var addr = new Address(Util.ReadFromBigEndianHexString<U128>(hex));
return addr == default ? null : addr;
}
public static Address? FromHexString(string hex) => FromBigEndian(Util.StringToByteArray(hex));

public static Address Random()
{
var random = new Random();
var bytes = new byte[16];
random.NextBytes(bytes);
return Address.From(bytes) ?? default;
var addr = new Address();
random.NextBytes(Util.AsBytes(ref addr));
return addr;
}

public readonly struct BSATN : IReadWrite<Address>
Expand All @@ -236,6 +182,7 @@ public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
public override string ToString() => Util.ToHexBigEndian(value);
}

[StructLayout(LayoutKind.Sequential)]
public readonly record struct Identity
{
private readonly U256 value;
Expand All @@ -249,10 +196,7 @@ public readonly record struct Identity
/// or, failing that, FromBigEndian.
/// </summary>
/// <param name="bytes"></param>
public Identity(byte[] bytes)
{
value = Util.ReadLittleEndian<U256>(bytes);
}
public Identity(ReadOnlySpan<byte> bytes) => this = From(bytes);

/// <summary>
/// Create an Identity from a LITTLE-ENDIAN byte array.
Expand All @@ -261,7 +205,8 @@ public Identity(byte[] bytes)
/// or, failing that, FromBigEndian.
/// </summary>
/// <param name="bytes"></param>
public static Identity From(byte[] bytes) => new(bytes);
public static Identity From(ReadOnlySpan<byte> bytes) =>
Util.Read<Identity>(bytes, littleEndian: true);

/// <summary>
/// Create an Identity from a BIG-ENDIAN byte array.
Expand All @@ -274,18 +219,15 @@ public Identity(byte[] bytes)
/// [0xb0, 0xb1, 0xb2, ...]
/// </summary>
/// <param name="bytes"></param>
public static Identity FromBigEndian(byte[] bytes)
{
return new Identity(Util.ReadBigEndian<U256>(bytes));
}
public static Identity FromBigEndian(ReadOnlySpan<byte> bytes) =>
Util.Read<Identity>(bytes, littleEndian: false);

/// <summary>
/// Create an Identity from a hex string.
/// </summary>
/// <param name="hex"></param>
/// <returns></returns>
public static Identity FromHexString(string hex) =>
new Identity(Util.ReadFromBigEndianHexString<U256>(hex));
public static Identity FromHexString(string hex) => FromBigEndian(Util.StringToByteArray(hex));

public readonly struct BSATN : IReadWrite<Identity>
{
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public sealed record ReducerContext : DbContext<Local>, Internal.IReducerContext
public readonly Random Rng;
public readonly DateTimeOffset Timestamp;

// We need this property to be non-static for parity with client SDK.
public Identity Identity => Internal.IReducerContext.GetIdentity();

internal ReducerContext(
Identity identity,
Address? address,
Expand Down
3 changes: 3 additions & 0 deletions crates/bindings-csharp/Codegen/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,9 @@ public sealed record ReducerContext : DbContext<Local>, Internal.IReducerContext
public readonly Random Rng;
public readonly DateTimeOffset Timestamp;
// We need this property to be non-static for parity with client SDK.
public Identity Identity => Internal.IReducerContext.GetIdentity();
internal ReducerContext(Identity identity, Address? address, Random random, DateTimeOffset time) {
CallerIdentity = identity;
CallerAddress = address;
Expand Down
12 changes: 12 additions & 0 deletions crates/bindings-csharp/Runtime/Internal/FFI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,16 @@ public static partial void volatile_nonatomic_schedule_immediate(
[In] byte[] args,
uint args_len
);

// Note #1: our Identity type has the same layout as a fixed-size 32-byte little-endian buffer,
// so instead of working around C#'s lack of fixed-size arrays, we just accept the pointer to
// the Identity itself. In this regard it's different from Rust declaration, but is still
// functionally the same.
// Note #2: we can't use `LibraryImport` here due to https://github.com/dotnet/runtime/issues/98616
// which prevents source-generated PInvokes from working with types from other assemblies, and
// `Identity` lives in another assembly (`BSATN.Runtime`). Luckily, `DllImport` is enough here.
#pragma warning disable SYSLIB1054 // Suppress "Use 'LibraryImportAttribute' instead of 'DllImportAttribute'" warning.
[DllImport(StdbNamespace)]
public static extern void identity(out Identity dest);
#pragma warning restore SYSLIB1054
}
9 changes: 8 additions & 1 deletion crates/bindings-csharp/Runtime/Internal/IReducer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ namespace SpacetimeDB.Internal;
using System.Text;
using SpacetimeDB.BSATN;

public interface IReducerContext { }
public interface IReducerContext
{
public static Identity GetIdentity()
{
FFI.identity(out var identity);
return identity;
}
}

public interface IReducer
{
Expand Down
1 change: 1 addition & 0 deletions crates/bindings-csharp/Runtime/bindings.c
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ IMPORT(Status, console_timer_end,
IMPORT(void, volatile_nonatomic_schedule_immediate,
(const uint8_t* name, size_t name_len, const uint8_t* args, size_t args_len),
(name, name_len, args, args_len));
IMPORT(void, identity, (void* id_ptr), (id_ptr));

#ifndef EXPERIMENTAL_WASM_AOT
static MonoClass* ffi_class;
Expand Down
8 changes: 8 additions & 0 deletions crates/testing/tests/standalone_integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ fn test_calling_a_reducer_in_module(module_name: &'static str) {
.to_string();
module.send(json).await.unwrap();

let json =
r#"{"CallReducer": {"reducer": "log_module_identity", "args": "[]", "request_id": 4, "flags": 0 }}"#
.to_string();
module.send(json).await.unwrap();

assert_eq!(
read_logs(&module).await,
[
Expand All @@ -73,7 +78,10 @@ fn test_calling_a_reducer_in_module(module_name: &'static str) {
"Hello, World!",
"Cersei has age 31 >= 30",
]
.into_iter()
.map(String::from)
.chain(std::iter::once(format!("Module identity: {}", module.db_identity)))
.collect::<Vec<_>>()
);
},
);
Expand Down
8 changes: 8 additions & 0 deletions modules/spacetimedb-quickstart-cs/Lib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,12 @@ public static void list_over_age(ReducerContext ctx, byte age)
Log.Info($"{person.name} has age {person.age} >= {age}");
}
}

[SpacetimeDB.Reducer]
public static void log_module_identity(ReducerContext ctx)
{
// Note: we use ToLower() because Rust side stringifies identities as lowercase hex.
// Is this something we need to align on in the future?
Log.Info($"Module identity: {ctx.Identity.ToString().ToLower()}");
}
}
5 changes: 5 additions & 0 deletions modules/spacetimedb-quickstart/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ pub fn list_over_age(ctx: &ReducerContext, age: u8) {
log::info!("{} has age {} >= {}", person.name, person.age, age);
}
}

#[spacetimedb::reducer]
fn log_module_identity(ctx: &ReducerContext) {
log::info!("Module identity: {}", ctx.identity());
}
Loading

1 comment on commit 40faad6

@github-actions
Copy link

@github-actions github-actions bot commented on 40faad6 Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Criterion benchmark results

Error when comparing benchmarks: Couldn't find AWS credentials in environment, credentials file, or IAM role.

Caused by:
Couldn't find AWS credentials in environment, credentials file, or IAM role.

Please sign in to comment.