Skip to content

Implement missing browser stream methods #15701

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

Merged
merged 2 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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