diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 7cfb036577d..fc66f533c6b 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/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 @@ List GetFilters() // 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()); } diff --git a/src/Browser/Avalonia.Browser/Interop/StreamHelper.cs b/src/Browser/Avalonia.Browser/Interop/StreamHelper.cs index 46fa671779f..5ba55475b71 100644 --- a/src/Browser/Avalonia.Browser/Interop/StreamHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/StreamHelper.cs @@ -16,7 +16,7 @@ internal static partial class StreamHelper public static partial void Truncate(JSObject stream, [JSMarshalAs] long size); [JSImport("StreamHelper.write", AvaloniaModule.MainModuleName)] - public static partial Task WriteAsync(JSObject stream, [JSMarshalAs] ArraySegment data); + public static partial Task WriteAsync(JSObject stream, [JSMarshalAs] ArraySegment data, int offset, int count); [JSImport("StreamHelper.close", AvaloniaModule.MainModuleName)] public static partial Task CloseAsync(JSObject stream); diff --git a/src/Browser/Avalonia.Browser/Storage/BlobReadableStream.cs b/src/Browser/Avalonia.Browser/Storage/BlobReadableStream.cs index 3404452bcac..83a1eda74df 100644 --- a/src/Browser/Avalonia.Browser/Storage/BlobReadableStream.cs +++ b/src/Browser/Avalonia.Browser/Storage/BlobReadableStream.cs @@ -77,6 +77,17 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation 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(asyncResult); + } + protected override void Dispose(bool disposing) { if (_jSReference is { } jsReference) diff --git a/src/Browser/Avalonia.Browser/Storage/WriteableStream.cs b/src/Browser/Avalonia.Browser/Storage/WriteableStream.cs index b2f14b6a7a9..95e640ca1f6 100644 --- a/src/Browser/Avalonia.Browser/Storage/WriteableStream.cs +++ b/src/Browser/Avalonia.Browser/Storage/WriteableStream.cs @@ -80,14 +80,30 @@ public override void Write(byte[] buffer, int offset, int count) public override ValueTask WriteAsync(ReadOnlyMemory 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) diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts index 2e160ec618c..27397c6e992 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts +++ b/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) { diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts index df32711a372..79c27493d3f 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts +++ b/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;