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

c#: Use span and memory apis for primitive type parameters on imports #1138

Merged
merged 5 commits into from
Jan 31, 2025

Conversation

jsturtevant
Copy link
Collaborator

@jsturtevant jsturtevant commented Jan 21, 2025

Fixes: #1080 and follow up to #1122.

Putting in draft for now since it has parts of #1137.

This generates both Span and Memory function signatures for and function that has a canonical list type.

Note that no tests have to change since the memory/span API' provide implicit operators that automatically convert Byte Arrays to Memory. But I added an additional check to the test.

Signed-off-by: James Sturtevant <[email protected]>
@pavelsavara
Copy link
Collaborator

Streams also support Span<>

https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.write?view=net-9.0#system-io-stream-write(system-readonlyspan((system-byte)))

Rule #1: For a synchronous API, use Span<T> instead of Memory<T> as a parameter if possible.
From
https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines

Maybe for some things we could generate both ?

@jsturtevant
Copy link
Collaborator Author

jsturtevant commented Jan 22, 2025

Streams also support Span<>

https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.write?view=net-9.0#system-io-stream-write(system-readonlyspan((system-byte)))

This is for the synchronous api though? I was thinking since we don't really support synchronous API's and we have p3 async coming down the line we might now be able to use Span anyways. I am not sure here.

Rule #1: For a synchronous API, use Span instead of Memory as a parameter if possible.
From
https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines

Maybe for some things we could generate both ?

The issue I ran into was when list is used in a variant. I was initially going to generate both but then this seemed like an option. Reading up on Memory it sounds like it makes small heap based allocations for every memory held so this might not be the right option for a default.

I will look to see if I can only generate this in the case of parameters and not for Variant based versions. I tried once but have a much better understanding of how it all comes together now.

@jsturtevant
Copy link
Collaborator Author

I've pushed a version that users Spans for any lists of primitive types and uses Fixed statements. PTAL

I think we will want/need to generate a second function that takes memory as well. The write use case is in the Stream API's WriteAsync method that takes Memory<T> and you cannot go from span to Memory?

@jsturtevant jsturtevant changed the title c#: Use memory api c#: Use span and memory apis for primative type parameters Jan 23, 2025
Signed-off-by: James Sturtevant <[email protected]>
Signed-off-by: James Sturtevant <[email protected]>
@pavelsavara
Copy link
Collaborator

This is for the synchronous api though? I was thinking since we don't really support synchronous API's and we have p3 async coming down the line we might now be able to use Span anyways. I am not sure here.

You are right, sorry. We can't have sync/blocking stream APIs until we have multi-threading.

(Unless we are happy to block on the stream operation, blocking also other async tasks in the ST thread-pool. And I have strong opinion against that.)

@jsturtevant
Copy link
Collaborator Author

I've added additional functions so we can support Span and memory for arrays of primitive types like u8, etc.

Signed-off-by: James Sturtevant <[email protected]>
@jsturtevant jsturtevant marked this pull request as ready for review January 24, 2025 00:44
@jsturtevant jsturtevant requested review from silesmo and yowl January 27, 2025 22:24
@pavelsavara pavelsavara changed the title c#: Use span and memory apis for primative type parameters c#: Use span and memory apis for primitive type parameters Jan 30, 2025
@pavelsavara
Copy link
Collaborator

pavelsavara commented Jan 30, 2025

We still generate GCHandle.Alloc for arrays

        public  static unsafe void EmptyListParam(byte[] a)
        {
            var cleanups = new List<Action>();
            var gcHandle = GCHandle.Alloc(a, GCHandleType.Pinned);
            var listPtr = gcHandle.AddrOfPinnedObject();
            cleanups.Add(()=> gcHandle.Free());
            EmptyListParamWasmInterop.wasmImportEmptyListParam((nint)listPtr, (a).Length);
            foreach (var cleanup in cleanups)
            {
                cleanup();
            }
        }

But we could use fixed there as well

    public static unsafe void EmptyListParam(byte[] a)
    {
        fixed (byte* aPtr = a)
        {
            EmptyListParamWasmInterop.wasmImportEmptyListParam((nint)aPtr, (a).Length);
        }
    }

@pavelsavara
Copy link
Collaborator

I also looked at

public  static unsafe byte[] EmptyListResult()
{

    var retArea = stackalloc uint[2+1];
    var ptr = ((int)retArea) + (4 - 1) & -4;
    EmptyListResultWasmInterop.wasmImportEmptyListResult(ptr);

    var array = new byte[BitConverter.ToInt32(new Span<byte>((void*)(ptr + 4), 4))];
    new Span<byte>((void*)(BitConverter.ToInt32(new Span<byte>((void*)(ptr + 0), 4))), BitConverter.ToInt32(new Span<byte>((void*)(ptr + 4), 4))).CopyTo(new Span<byte>(array));
    return array;

}

And I think it could be slightly easier to read like this

public static unsafe byte[] EmptyListResult()
{
    uint* retArea = stackalloc uint[2 + 1];
    var alignedPtr = ((int)retArea) + (4 - 1) & -4;
    EmptyListResultWasmInterop.wasmImportEmptyListResult(alignedPtr);
    var start = (void*)Marshal.ReadIntPtr(alignedPtr);
    var count = Marshal.ReadInt32(alignedPtr + 4);
    var array = new byte[count];
    Unsafe.Copy(ref array, start);
    return array;
}

I have not tested this, tho

@pavelsavara
Copy link
Collaborator

For

public static unsafe void ListParam2(string a)
{
    var cleanups = new List<Action>();
    var utf8Bytes = Encoding.UTF8.GetBytes(a);
    var length = utf8Bytes.Length;
    var gcHandle = GCHandle.Alloc(utf8Bytes, GCHandleType.Pinned);
    var interopString = gcHandle.AddrOfPinnedObject();
    cleanups.Add(() => gcHandle.Free());
    ListParam2WasmInterop.wasmImportListParam2(interopString.ToInt32(), length);
    foreach (var cleanup in cleanups)
    {
        cleanup();
    }
}

Ideally, we could tell the host that we use UTF16 and let the host to handle the re-encoding if necessary.
It would have many performance benefits.

Note that a.Length here is characters. And there are surrogate characters with 2x16 bits.

public static unsafe void ListParam2(string a)
{
    fixed(char* aPtr = a)
    {
        Program.wasmImportListParam2((nint)aPtr, a.Length);
    }
}

if we need to stick to UTF8

public static unsafe void ListParam2(string a)
{
    var bytes = Encoding.UTF8.GetBytes(a);
    fixed (byte* aPtr = bytes)
    {
        Program.wasmImportListParam2((nint)aPtr, bytes.Length);
    }
}

@pavelsavara
Copy link
Collaborator

public static unsafe nint wasmExportEmptyStringResult() {

    string ret;
    ret = TestImpl.EmptyStringResult();

    var ptr = InteropReturnArea.returnArea.AddressOfReturnArea();

    var utf8Bytes = Encoding.UTF8.GetBytes(ret);
    var length = utf8Bytes.Length;
    var gcHandle = GCHandle.Alloc(utf8Bytes, GCHandleType.Pinned);
    var interopString = gcHandle.AddrOfPinnedObject();

    BitConverter.TryWriteBytes(new Span<byte>((void*)(ptr + 4), 4), length);
    BitConverter.TryWriteBytes(new Span<byte>((void*)(ptr + 0), 4), interopString.ToInt32());
    return ptr;

}

could become

public static unsafe nint wasmExportEmptyStringResult()
{
    string ret;
    ret = TestImpl.EmptyStringResult();
    IntPtr ptr = InteropReturnArea.returnArea.AddressOfReturnArea();
    var len = Encoding.UTF8.GetByteCount(ret);
    var buffer = Marshal.AllocHGlobal(len);
    var span = new Span<byte>((void*)buffer, len);
    Encoding.UTF8.GetBytes(ret, span);
    Marshal.WriteIntPtr(ptr, buffer);
    Marshal.WriteInt32(ptr+4, len);
    return ptr;
}

@pavelsavara
Copy link
Collaborator

pavelsavara commented Jan 30, 2025

In the exports, we generate

public interface ITest {
    static abstract void EmptyListParam(byte[] a);
}

but we probably generate

public interface ITest {
    static abstract void EmptyListParam(ReadOnlySpan<byte> a)
}

because we don't expect the callee to modify that memory space.

This would allow us to pass the host buffer, instead of making a managed copy on the heap (and GC it later).

Note they could always make copy themself.

    public static void EmptyListParam(ReadOnlySpan<byte> a) {
        byte[] copy = a.ToArray();
    }

@jsturtevant jsturtevant changed the title c#: Use span and memory apis for primitive type parameters c#: Use span and memory apis for primitive type parameters on imports Jan 30, 2025
@jsturtevant
Copy link
Collaborator Author

We still generate GCHandle.Alloc for arrays

Add it to places to clean up in #1150

if we need to stick to UTF8
public static unsafe void ListParam2(string a)

Added to clean up in #1150

And I think it could be slightly easier to read like this
byte[] EmptyListResult()

We can see if we can clean this up a bit, particularly with naming of variables, but some of the structure of the code comes from the way the base library we in the generator calls our generation code.

public static unsafe nint wasmExportEmptyStringResult()
Some of this partially addressed in #1145, the rest is covered under other issues I think.

In the exports, we generate

This pr was focused on Imports (I've updated the PR to include this in the title), but lets create an issue. #1151

@jsturtevant
Copy link
Collaborator Author

Thanks for all the feedback! I'm learning a bunch. I've opened issues to track most of it. If I missed something lets get it in an issue.

For this PR, does the general direction of generating additional Span and Memory functions make sense? If so we can move forward with this then I can follow up with some of the other suggestions.

Copy link
Collaborator

@pavelsavara pavelsavara left a comment

Choose a reason for hiding this comment

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

all we got so far in this PR is in the right direction

@jsturtevant jsturtevant added this pull request to the merge queue Jan 31, 2025
Merged via the queue into bytecodealliance:main with commit b601633 Jan 31, 2025
25 checks passed
@jsturtevant jsturtevant deleted the use-memory-api-2 branch January 31, 2025 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

c#: Use Spans instead of e.g. byte[]
2 participants