Skip to content

Commit 3dc92e0

Browse files
authored
Implement missing browser stream methods (#15701)
* Implement missing WriteAsync/BeginWrite/BeginRead browser stream methods * Optimize/hack StreamHelper.write to use buffer directly
1 parent aa351ee commit 3dc92e0

File tree

6 files changed

+63
-23
lines changed

6 files changed

+63
-23
lines changed

samples/ControlCatalog/Pages/DialogsPage.xaml.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using System;
22
using System.Buffers;
33
using System.Collections.Generic;
4+
using System.IO;
45
using System.Linq;
56
using System.Reflection;
67
using System.Security;
8+
using System.Text;
79
using System.Threading.Tasks;
810
using Avalonia;
911
using Avalonia.Controls;
@@ -255,12 +257,12 @@ List<FileDialogFilter> GetFilters()
255257
// Sync disposal of StreamWriter is not supported on WASM
256258
#if NET6_0_OR_GREATER
257259
await using var stream = await file.OpenWriteAsync();
258-
await using var reader = new System.IO.StreamWriter(stream);
260+
await using var writer = new System.IO.StreamWriter(stream);
259261
#else
260-
using var stream = await file.OpenWriteAsync();
261-
using var reader = new System.IO.StreamWriter(stream);
262+
using var stream = await file.OpenWriteAsync();
263+
using var writer = new System.IO.StreamWriter(stream);
262264
#endif
263-
await reader.WriteLineAsync(openedFileContent.Text);
265+
await writer.WriteLineAsync(openedFileContent.Text);
264266

265267
SetFolder(await file.GetParentAsync());
266268
}

src/Browser/Avalonia.Browser/Interop/StreamHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal static partial class StreamHelper
1616
public static partial void Truncate(JSObject stream, [JSMarshalAs<JSType.Number>] long size);
1717

1818
[JSImport("StreamHelper.write", AvaloniaModule.MainModuleName)]
19-
public static partial Task WriteAsync(JSObject stream, [JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> data);
19+
public static partial Task WriteAsync(JSObject stream, [JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> data, int offset, int count);
2020

2121
[JSImport("StreamHelper.close", AvaloniaModule.MainModuleName)]
2222
public static partial Task CloseAsync(JSObject stream);

src/Browser/Avalonia.Browser/Storage/BlobReadableStream.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ public override async ValueTask<int> ReadAsync(Memory<byte> buffer, Cancellation
7777
return bytesRead.Length;
7878
}
7979

80+
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
81+
{
82+
var task = ReadAsync(buffer, offset, count, default);
83+
return TaskToAsyncResult.Begin(task, callback, state);
84+
}
85+
86+
public override int EndRead(IAsyncResult asyncResult)
87+
{
88+
return TaskToAsyncResult.End<int>(asyncResult);
89+
}
90+
8091
protected override void Dispose(bool disposing)
8192
{
8293
if (_jSReference is { } jsReference)

src/Browser/Avalonia.Browser/Storage/WriteableStream.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,30 @@ public override void Write(byte[] buffer, int offset, int count)
8080

8181
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
8282
{
83-
return new ValueTask(WriteAsyncInternal(buffer.ToArray(), cancellationToken));
83+
return new ValueTask(WriteAsyncInternal(buffer.ToArray(), 0, buffer.Length, cancellationToken));
8484
}
8585

86-
private Task WriteAsyncInternal(byte[] buffer, CancellationToken _)
86+
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
8787
{
88-
_position += buffer.Length;
88+
return WriteAsyncInternal(buffer, offset, count, cancellationToken);
89+
}
90+
91+
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
92+
{
93+
var task = WriteAsyncInternal(buffer, offset, count, default);
94+
return TaskToAsyncResult.Begin(task, callback, state);
95+
}
96+
97+
public override void EndWrite(IAsyncResult asyncResult)
98+
{
99+
TaskToAsyncResult.End(asyncResult);
100+
}
101+
102+
private Task WriteAsyncInternal(byte[] buffer, int offset, int count, CancellationToken _)
103+
{
104+
_position += count;
89105

90-
return StreamHelper.WriteAsync(JSReference, buffer);
106+
return StreamHelper.WriteAsync(JSReference, buffer, offset, count);
91107
}
92108

93109
protected override void Dispose(bool disposing)

src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import FileSystemWritableFileStream from "native-file-system-adapter/types/src/FileSystemWritableFileStream";
2-
import { IMemoryView } from "../../types/dotnet";
2+
3+
const sharedArrayBufferDefined = typeof SharedArrayBuffer !== "undefined";
4+
export function isSharedArrayBuffer(buffer: any): buffer is SharedArrayBuffer {
5+
// BEWARE: In some cases, `instanceof SharedArrayBuffer` returns false even though buffer is an SAB.
6+
// Patch adapted from https://github.com/emscripten-core/emscripten/pull/16994
7+
// See also https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
8+
return sharedArrayBufferDefined && buffer[Symbol.toStringTag] === "SharedArrayBuffer";
9+
}
310

411
export class StreamHelper {
512
public static async seek(stream: FileSystemWritableFileStream, position: number) {
@@ -14,11 +21,22 @@ export class StreamHelper {
1421
return await stream.close();
1522
}
1623

17-
public static async write(stream: FileSystemWritableFileStream, span: IMemoryView) {
18-
const array = new Uint8Array(span.byteLength);
19-
span.copyTo(array);
20-
21-
return await stream.write(array);
24+
public static async write(stream: FileSystemWritableFileStream, span: any, offset: number, count: number) {
25+
const heap8 = globalThis.getDotnetRuntime(0)?.localHeapViewU8();
26+
27+
let buffer: Uint8Array;
28+
if (span._pointer > 0 && span._length > 0 && heap8 && !isSharedArrayBuffer(heap8.buffer)) {
29+
// Attempt to use undocumented access to the HEAP8 directly
30+
// Note, SharedArrayBuffer cannot be used with ImageData (when WasmEnableThreads = true).
31+
buffer = new Uint8Array(heap8.buffer, span._pointer as number + offset, count);
32+
} else {
33+
// Or fallback to the normal API that does multiple array copies.
34+
const copy = new Uint8Array(count);
35+
span.copyTo(copy, offset);
36+
buffer = span;
37+
}
38+
39+
return await stream.write(buffer);
2240
}
2341

2442
public static byteLength(stream: Blob) {

src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
import { BrowserRenderingMode } from "./surfaceBase";
22
import { HtmlCanvasSurfaceBase } from "./htmlSurfaceBase";
33
import { RuntimeAPI } from "../../../types/dotnet";
4-
5-
const sharedArrayBufferDefined = typeof SharedArrayBuffer !== "undefined";
6-
function isSharedArrayBuffer(buffer: any): buffer is SharedArrayBuffer {
7-
// BEWARE: In some cases, `instanceof SharedArrayBuffer` returns false even though buffer is an SAB.
8-
// Patch adapted from https://github.com/emscripten-core/emscripten/pull/16994
9-
// See also https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
10-
return sharedArrayBufferDefined && buffer[Symbol.toStringTag] === "SharedArrayBuffer";
11-
}
4+
import { isSharedArrayBuffer } from "../stream";
125

136
export class SoftwareSurface extends HtmlCanvasSurfaceBase {
147
private readonly runtime: RuntimeAPI | undefined;

0 commit comments

Comments
 (0)