Browse Source

Implement missing browser stream methods (#15701)

* Implement missing WriteAsync/BeginWrite/BeginRead browser stream methods

* Optimize/hack StreamHelper.write to use buffer directly
Max Katz 1 year ago
parent
commit
3dc92e0fbd

+ 6 - 4
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@@ -1,9 +1,11 @@
 using System;
 using System.Buffers;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
 using System.Reflection;
 using System.Security;
+using System.Text;
 using System.Threading.Tasks;
 using Avalonia;
 using Avalonia.Controls;
@@ -255,12 +257,12 @@ namespace ControlCatalog.Pages
                     // Sync disposal of StreamWriter is not supported on WASM
 #if NET6_0_OR_GREATER
                     await using var stream = await file.OpenWriteAsync();
-                    await using var reader = new System.IO.StreamWriter(stream);
+                    await using var writer = new System.IO.StreamWriter(stream);
 #else
-                                using var stream = await file.OpenWriteAsync();
-                                using var reader = new System.IO.StreamWriter(stream);
+                    using var stream = await file.OpenWriteAsync();
+                    using var writer = new System.IO.StreamWriter(stream);
 #endif
-                    await reader.WriteLineAsync(openedFileContent.Text);
+                    await writer.WriteLineAsync(openedFileContent.Text);
 
                     SetFolder(await file.GetParentAsync());
                 }

+ 1 - 1
src/Browser/Avalonia.Browser/Interop/StreamHelper.cs

@@ -16,7 +16,7 @@ internal static partial class StreamHelper
     public static partial void Truncate(JSObject stream, [JSMarshalAs<JSType.Number>] long size);
 
     [JSImport("StreamHelper.write", AvaloniaModule.MainModuleName)]
-    public static partial Task WriteAsync(JSObject stream, [JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> data);
+    public static partial Task WriteAsync(JSObject stream, [JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> data, int offset, int count);
 
     [JSImport("StreamHelper.close", AvaloniaModule.MainModuleName)]
     public static partial Task CloseAsync(JSObject stream);

+ 11 - 0
src/Browser/Avalonia.Browser/Storage/BlobReadableStream.cs

@@ -77,6 +77,17 @@ internal class BlobReadableStream : Stream
         return bytesRead.Length;
     }
 
+    public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
+    {
+        var task = ReadAsync(buffer, offset, count, default);
+        return TaskToAsyncResult.Begin(task, callback, state);
+    }
+
+    public override int EndRead(IAsyncResult asyncResult)
+    {
+        return TaskToAsyncResult.End<int>(asyncResult);
+    }
+
     protected override void Dispose(bool disposing)
     {
         if (_jSReference is { } jsReference)

+ 20 - 4
src/Browser/Avalonia.Browser/Storage/WriteableStream.cs

@@ -80,14 +80,30 @@ internal sealed class WriteableStream : Stream
 
     public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
     {
-        return new ValueTask(WriteAsyncInternal(buffer.ToArray(), cancellationToken));
+        return new ValueTask(WriteAsyncInternal(buffer.ToArray(), 0, buffer.Length, cancellationToken));
     }
 
-    private Task WriteAsyncInternal(byte[] buffer, CancellationToken _)
+    public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
     {
-        _position += buffer.Length;
+        return WriteAsyncInternal(buffer, offset, count, cancellationToken);
+    }
+
+    public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
+    {
+        var task = WriteAsyncInternal(buffer, offset, count, default);
+        return TaskToAsyncResult.Begin(task, callback, state);
+    }
+
+    public override void EndWrite(IAsyncResult asyncResult)
+    {
+        TaskToAsyncResult.End(asyncResult);
+    }
+
+    private Task WriteAsyncInternal(byte[] buffer, int offset, int count, CancellationToken _)
+    {
+        _position += count;
 
-        return StreamHelper.WriteAsync(JSReference, buffer);
+        return StreamHelper.WriteAsync(JSReference, buffer, offset, count);
     }
 
     protected override void Dispose(bool disposing)

+ 24 - 6
src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts

@@ -1,5 +1,12 @@
 import FileSystemWritableFileStream from "native-file-system-adapter/types/src/FileSystemWritableFileStream";
-import { IMemoryView } from "../../types/dotnet";
+
+const sharedArrayBufferDefined = typeof SharedArrayBuffer !== "undefined";
+export function isSharedArrayBuffer(buffer: any): buffer is SharedArrayBuffer {
+    // BEWARE: In some cases, `instanceof SharedArrayBuffer` returns false even though buffer is an SAB.
+    // Patch adapted from https://github.com/emscripten-core/emscripten/pull/16994
+    // See also https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
+    return sharedArrayBufferDefined && buffer[Symbol.toStringTag] === "SharedArrayBuffer";
+}
 
 export class StreamHelper {
     public static async seek(stream: FileSystemWritableFileStream, position: number) {
@@ -14,11 +21,22 @@ export class StreamHelper {
         return await stream.close();
     }
 
-    public static async write(stream: FileSystemWritableFileStream, span: IMemoryView) {
-        const array = new Uint8Array(span.byteLength);
-        span.copyTo(array);
-
-        return await stream.write(array);
+    public static async write(stream: FileSystemWritableFileStream, span: any, offset: number, count: number) {
+        const heap8 = globalThis.getDotnetRuntime(0)?.localHeapViewU8();
+
+        let buffer: Uint8Array;
+        if (span._pointer > 0 && span._length > 0 && heap8 && !isSharedArrayBuffer(heap8.buffer)) {
+            // Attempt to use undocumented access to the HEAP8 directly
+            // Note, SharedArrayBuffer cannot be used with ImageData (when WasmEnableThreads = true).
+            buffer = new Uint8Array(heap8.buffer, span._pointer as number + offset, count);
+        } else {
+            // Or fallback to the normal API that does multiple array copies.
+            const copy = new Uint8Array(count);
+            span.copyTo(copy, offset);
+            buffer = span;
+        }
+
+        return await stream.write(buffer);
     }
 
     public static byteLength(stream: Blob) {

+ 1 - 8
src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts

@@ -1,14 +1,7 @@
 import { BrowserRenderingMode } from "./surfaceBase";
 import { HtmlCanvasSurfaceBase } from "./htmlSurfaceBase";
 import { RuntimeAPI } from "../../../types/dotnet";
-
-const sharedArrayBufferDefined = typeof SharedArrayBuffer !== "undefined";
-function isSharedArrayBuffer(buffer: any): buffer is SharedArrayBuffer {
-    // BEWARE: In some cases, `instanceof SharedArrayBuffer` returns false even though buffer is an SAB.
-    // Patch adapted from https://github.com/emscripten-core/emscripten/pull/16994
-    // See also https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
-    return sharedArrayBufferDefined && buffer[Symbol.toStringTag] === "SharedArrayBuffer";
-}
+import { isSharedArrayBuffer } from "../stream";
 
 export class SoftwareSurface extends HtmlCanvasSurfaceBase {
     private readonly runtime: RuntimeAPI | undefined;