Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jeremie/cs module #1719

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
11 changes: 11 additions & 0 deletions crates/bindings-csharp/BSATN.Runtime/Db.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace SpacetimeDB;

public abstract class DbContext<DbView>
where DbView : class, new()
{
public readonly DbView Db;

public DbContext() => Db = new();

public DbContext(DbView db) => Db = db;
}
258 changes: 158 additions & 100 deletions crates/bindings-csharp/Codegen/Module.cs

Large diffs are not rendered by default.

89 changes: 64 additions & 25 deletions crates/bindings-csharp/Runtime/Attrs.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,10 @@
namespace SpacetimeDB;

public static class ReducerKind
{
public const string Init = "__init__";
public const string Update = "__update__";
public const string Connect = "__identity_connected__";
public const string Disconnect = "__identity_disconnected__";
}

[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class ReducerAttribute(string? name = null) : Attribute
[Type]
public enum IndexType : byte
{
public string? Name => name;
}

[AttributeUsage(
AttributeTargets.Struct | AttributeTargets.Class,
Inherited = false,
AllowMultiple = false
)]
public sealed class TableAttribute : Attribute
{
public bool Public { get; init; }
public string? Scheduled { get; init; }
BTree,
Hash,
}

[Flags]
Expand All @@ -40,8 +22,65 @@ public enum ColumnAttrs : byte
PrimaryKeyIdentity = PrimaryKeyAuto,
}

[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class ColumnAttribute(ColumnAttrs type) : Attribute
/// <summary>
/// Registers a type as the row structure of a SpacetimeDB table, enabling codegen for it.
///
/// <para>
/// Multiple [Table] attributes per type are supported. This is useful to reuse row types.
/// Each attribute instance must have a unique name and will create a SpacetimeDB table.
/// </para>
/// </summary>
[AttributeUsage(AttributeTargets.Struct, AllowMultiple = true)]
public sealed class TableAttribute : Attribute
{
public ColumnAttrs Type => type;
/// <summary>
/// This identifier is used to name the SpacetimeDB table on the host as well as the
/// table handle structures generated to access the table from within a reducer call.
///
/// <para>Defaults to the <c>nameof</c> of the target type.</para>
/// </summary>
public string? Name;

/// <summary>
/// Set to <c>true</c> to make the table visible to everyone.
///
/// <para>Defaults to the table only being visible to its owner.</para>
/// </summary>
public bool Public = false;

public string? Index;

public IndexType? IndexType;

public string[]? IndexColumns;
}

[AttributeUsage(AttributeTargets.Field)]
public abstract class ColumnAttribute : Attribute
{
public string? Table;
}

public sealed class AutoIncAttribute : ColumnAttribute { }

public sealed class PrimaryKeyAttribute : ColumnAttribute { }

public sealed class UniqueAttribute : ColumnAttribute { }

public sealed class IndexedAttribute : ColumnAttribute { }

public static class ReducerKind
{
public const string Init = "__init__";
public const string Update = "__update__";
public const string Connect = "__identity_connected__";
public const string Disconnect = "__identity_disconnected__";
}

[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class ReducerAttribute : Attribute
{
public string? Name;
Copy link
Member

Choose a reason for hiding this comment

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

Reducer kind being a special name was more of an implementation detail, and that will be changing in the new proposal (WIP in #1670) where ReducerKind is stored separately.

AFAIK the SDK proposal also doesn't allow custom #[spacetimedb::reducer(name = "...")], so we shouldn't be doing that in C# either - can you please revert this to a regular attribute parameter, so it would be easier to migrate to non-name-based ReducerKind in the future.

}


4 changes: 3 additions & 1 deletion crates/bindings-csharp/Runtime/Internal/IReducer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ namespace SpacetimeDB.Internal;
using System.Text;
using SpacetimeDB.BSATN;

public interface IReducerContext { }

public interface IReducer
{
ReducerDef MakeReducerDef(ITypeRegistrar registrar);

// This one is not static because we need to be able to store IReducer in a list.
void Invoke(BinaryReader reader, ReducerContext args);
void Invoke(BinaryReader reader, IReducerContext args);

public static void VolatileNonatomicScheduleImmediate(string name, MemoryStream args)
{
Expand Down
28 changes: 17 additions & 11 deletions crates/bindings-csharp/Runtime/Internal/ITable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ public interface ITable<T> : IStructuralReadWrite
where T : ITable<T>, new()
{
// These are the methods that codegen needs to implement.
void ReadGenFields(BinaryReader reader);
static abstract TableDesc MakeTableDesc(ITypeRegistrar registrar);
static abstract IEnumerable<TableDesc> MakeTableDesc(ITypeRegistrar registrar);

static abstract Filter CreateFilter();
}

// These are static helpers that codegen can use.
public interface ITableView<View, T>
where View : ITableView<View, T>
where T : ITable<T>, new()
{
static abstract void ReadGenFields(BinaryReader reader, ref T row);

// These are static helpers that codegen can use.
private abstract class RawTableIterBase
{
public class Enumerator(FFI.RowIter handle) : IDisposable
Expand Down Expand Up @@ -105,7 +111,7 @@ public IEnumerable<T> Parse()
using var reader = new BinaryReader(stream);
while (stream.Position < stream.Length)
{
yield return Read<T>(reader);
yield return IStructuralReadWrite.Read<T>(reader);
}
}
}
Expand Down Expand Up @@ -134,7 +140,7 @@ protected override void IterStart(out FFI.RowIter handle) =>
private static readonly Lazy<FFI.TableId> tableId_ =
new(() =>
{
var name_bytes = System.Text.Encoding.UTF8.GetBytes(typeof(T).Name);
var name_bytes = System.Text.Encoding.UTF8.GetBytes(typeof(View).Name);
FFI._table_id_from_name(name_bytes, (uint)name_bytes.Length, out var out_);
return out_;
});
Expand All @@ -148,17 +154,17 @@ protected override void IterStart(out FFI.RowIter handle) =>
public static IEnumerable<T> Query(Expression<Func<T, bool>> query) =>
new RawTableIterFiltered(tableId, filter.Value.Compile(query)).Parse();

protected static void Insert(T row)
protected static void Insert(ref T row)
{
// Insert the row.
var bytes = ToBytes(row);
var bytes = IStructuralReadWrite.ToBytes(row);
var bytes_len = (uint)bytes.Length;
FFI._datastore_insert_bsatn(tableId, bytes, ref bytes_len);

// Write back any generated column values.
using var stream = new MemoryStream(bytes, 0, (int)bytes_len);
using var reader = new BinaryReader(stream);
row.ReadGenFields(reader);
View.ReadGenFields(reader, ref row);
}

protected readonly ref struct ColEq
Expand All @@ -175,7 +181,7 @@ private ColEq(FFI.ColId colId, byte[] value)
public static ColEq Where<TCol, TColRW>(ushort colId, TCol colValue, TColRW rw)
where TColRW : IReadWrite<TCol>
{
return new(new FFI.ColId(colId), ToBytes(rw, colValue));
return new(new FFI.ColId(colId), IStructuralReadWrite.ToBytes(rw, colValue));
}

// Note: do not inline FindBy from the Codegen as a helper API here.
Expand All @@ -188,14 +194,14 @@ public bool Delete()
return out_ > 0;
}

public bool Update(T row)
public bool Update(ref T row)
{
// Just like in Rust bindings, updating is just deleting and inserting for now.
if (!Delete())
{
return false;
}
Insert(row);
Insert(ref row);
return true;
}
}
Expand Down
15 changes: 11 additions & 4 deletions crates/bindings-csharp/Runtime/Internal/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ public static class Module
private static readonly RawModuleDefV8 moduleDef = new();
private static readonly List<IReducer> reducers = [];

private static IReducerContext? context = null;

public static void Initialize(IReducerContext ctx) => context = ctx;

readonly struct TypeRegistrar() : ITypeRegistrar
{
private readonly Dictionary<Type, AlgebraicType.Ref> types = [];
Expand Down Expand Up @@ -100,7 +104,9 @@ public static void RegisterReducer<R>()
public static void RegisterTable<T>()
where T : ITable<T>, new()
{
moduleDef.RegisterTable(T.MakeTableDesc(typeRegistrar));
foreach (var t in T.MakeTableDesc(typeRegistrar)) {
moduleDef.RegisterTable(t);
}
}

private static byte[] Consume(this BytesSource source)
Expand Down Expand Up @@ -188,20 +194,21 @@ BytesSink error
)
{
// Piece together the sender identity.
var sender = Identity.From(
Runtime.SenderIdentity = Identity.From(
MemoryMarshal.AsBytes([sender_0, sender_1, sender_2, sender_3]).ToArray()
);

// Piece together the sender address.
var address = Address.From(MemoryMarshal.AsBytes([address_0, address_1]).ToArray());
Runtime.SenderAddress = Address.From(MemoryMarshal.AsBytes([address_0, address_1]).ToArray());

try
{
Runtime.Random = new((int)timestamp.MicrosecondsSinceEpoch);
Runtime.Timestamp = timestamp.ToStd();

using var stream = new MemoryStream(args.Consume());
using var reader = new BinaryReader(stream);
reducers[(int)id].Invoke(reader, new(sender, address, timestamp.ToStd()));
reducers[(int)id].Invoke(reader, context!);
if (stream.Position != stream.Length)
{
throw new Exception("Unrecognised extra bytes in the reducer arguments");
Expand Down
32 changes: 14 additions & 18 deletions crates/bindings-csharp/Runtime/Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,6 @@ namespace SpacetimeDB;
using SpacetimeDB.Internal;
using static System.Text.Encoding;

public class ReducerContext
{
public readonly Identity Sender;
public readonly DateTimeOffset Time;
public readonly Address? Address;

internal ReducerContext(
Identity senderIdentity,
Address? senderAddress,
DateTimeOffset timestamp
)
{
Sender = senderIdentity;
Address = senderAddress;
Time = timestamp;
}
}

// [SpacetimeDB.Type] - we have custom representation of time in microseconds, so implementing BSATN manually
public abstract partial record ScheduleAt
: SpacetimeDB.TaggedEnum<(DateTimeOffset Time, TimeSpan Interval)>
Expand Down Expand Up @@ -77,6 +59,14 @@ public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
}
}

public abstract class BaseReducerContext<DbView> : DbContext<DbView>, IReducerContext
where DbView : class, new()
Copy link
Member

Choose a reason for hiding this comment

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

Unless we have an interface to uphold, I'd rather not spell out extra constraints. In this case it doesn't seem to add much value in terms of checks.

Suggested change
where DbView : class, new()
where DbView : new()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made it a class because it does carry state; the connection handle in this case. A struct could be copied/defaulted and introduce a silent null connection. This forces it to be a reference type, and given there's only one per application the distinction between value/ref is no longer relevant, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well in this case Local is stateless, but the same base class has to support DbConnection on the client side.

{
public Identity Sender => Runtime.SenderIdentity!;
Copy link
Member

@RReverser RReverser Sep 18, 2024

Choose a reason for hiding this comment

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

Why are these set on the singleton now? What happens if someone wants to store a ReducerContext? It would now lose its own identity and point to the one from the currently running reducer instead.

I think those should be own fields, like before.

For Random we only used Runtime because we wanted to access the RNG anywhere in the codebase. It only needed reseeding, but wasn't actually tied to the reducer data - the result of RNG is non-deterministic by design, so returning data from the wrong context was not a concern, but for identity/address it will be.

Even for RNG that's changing in #1681 and they're all going to live on the context now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reducer context is created once for the duration of the module, and @gefjon says each reducer invocation should be seen as its own isolated instance with no memory able to outlive that invocation.

Making them fields would need a new ReducerContext per reducer invocation, which means its DbView instance has to either also be created per invocation, or stored somewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm fine with either approaches; I guess it depends on what semantics we want for memory across reducer invocations, and if we want to leverage isolation in the impl like this ReducerContext does here.

public Address Address => Runtime.SenderAddress!;
public DateTimeOffset Timestamp => Runtime.Timestamp!;
}

public static class Runtime
{
public enum LogLevel : byte
Expand Down Expand Up @@ -115,4 +105,10 @@ public static void Log(

// An instance of `System.Random` that is reseeded by each reducer's timestamp.
public static Random Random { get; internal set; } = new();

public static Identity? SenderIdentity { get; internal set; }

public static Address? SenderAddress { get; internal set; }

public static DateTimeOffset Timestamp { get; internal set; }
}
Loading