Skip to content

Commit

Permalink
Implement missing browser stream methods (#15701)
Browse files Browse the repository at this point in the history
* Implement missing WriteAsync/BeginWrite/BeginRead browser stream methods

* Optimize/hack StreamHelper.write to use buffer directly
  • Loading branch information
maxkatz6 authored and grokys committed Jun 4, 2024
1 parent 873f3cf commit e3d8f06
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 23 deletions.
10 changes: 6 additions & 4 deletions samples/ControlCatalog/Pages/DialogsPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -255,12 +257,12 @@ List<FileDialogFilter> 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());
}
Expand Down
2 changes: 1 addition & 1 deletion src/Browser/Avalonia.Browser/Interop/StreamHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions src/Browser/Avalonia.Browser/Storage/BlobReadableStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ public override async ValueTask<int> ReadAsync(Memory<byte> 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<int>(asyncResult);
}

protected override void Dispose(bool disposing)
{
if (_jSReference is { } jsReference)
Expand Down
24 changes: 20 additions & 4 deletions src/Browser/Avalonia.Browser/Storage/WriteableStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,30 @@ public override void Write(byte[] buffer, int offset, int count)

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)
Expand Down
30 changes: 24 additions & 6 deletions src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down

0 comments on commit e3d8f06

Please sign in to comment.