Browse Source

Implement missing browser stream methods (#15701)

* Implement missing WriteAsync/BeginWrite/BeginRead browser stream methods

* Optimize/hack StreamHelper.write to use buffer directly
release/11.1.0-rc1
Max Katz 2 years ago
committed by Steven Kirk
parent
commit
e3d8f06008
  1. 10
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  2. 2
      src/Browser/Avalonia.Browser/Interop/StreamHelper.cs
  3. 11
      src/Browser/Avalonia.Browser/Storage/BlobReadableStream.cs
  4. 24
      src/Browser/Avalonia.Browser/Storage/WriteableStream.cs
  5. 30
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts
  6. 9
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts

10
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());
}

2
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
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)

24
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)

30
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) {

9
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;

Loading…
Cancel
Save