diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 2674ac5..c86aefd 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -2738,6 +2738,31 @@ fn install_interface_prototype_members( ) { // One prototype slot per name (a repeated descriptor aborts V8 at instantiation). let mut seen: AHashSet = AHashSet::new(); + install_interface_members_into(scope, proto, clazz, iid, type_args, fallback, &mut seen); +} + +/// Names install_interface_members_into puts on the prototype, required interfaces included. +#[cfg(feature = "classic")] +fn collect_interface_member_names(clazz: &dyn BaseClassDeclarationImpl, names: &mut AHashSet) { + names.extend(clazz.methods().iter().map(|m| m.name().to_string())); + names.extend(clazz.properties().iter().map(|p| p.name().to_string())); + for iface in clazz.implemented_interfaces() { + collect_interface_member_names(iface, names); + } +} + +/// Installs `clazz`'s members, then those of the interfaces it requires (IAsyncInfo.Status on +/// an IAsyncOperation), each called through its own IID. Names already in `seen` win. +#[cfg(feature = "classic")] +fn install_interface_members_into( + scope: &mut v8::PinScope<'_, '_>, + proto: Local, + clazz: &dyn BaseClassDeclarationImpl, + iid: GUID, + type_args: &[String], + fallback: &Option, + seen: &mut AHashSet, +) { for method in clazz.methods().iter() { if method.is_static() { continue; @@ -2895,6 +2920,11 @@ fn install_interface_prototype_members( v8::PropertyAttribute::NONE, ); } + + for iface in clazz.implemented_interfaces() { + let child_args = extract_generic_type_args(iface.full_name()); + install_interface_members_into(scope, proto, iface, iface.id(), &child_args, fallback, seen); + } } @@ -4322,12 +4352,9 @@ fn create_ns_ctor_instance_object<'a>( // must not shadow them on the prototype, which is how a non-masking wrapper // gives the interface's own members priority. let own_names: AHashSet = if iface_proto.is_some() { - clazz - .methods() - .iter() - .map(|m| m.name().to_string()) - .chain(clazz.properties().iter().map(|p| p.name().to_string())) - .collect() + let mut names = AHashSet::new(); + collect_interface_member_names(clazz, &mut names); + names } else { AHashSet::new() }; diff --git a/test-app/README.md b/test-app/README.md index 5bd184d..04e718f 100644 --- a/test-app/README.md +++ b/test-app/README.md @@ -43,6 +43,7 @@ The script prints the log and exits non-zero when any spec failed. | `threading.js` | WinRT and .NET callbacks invoked on background threads are delivered on the JS thread that created them (the UI thread or a worker). Return values and exceptions flow back to the native caller. Worker specs are skipped on engines without workers. | | `boxing.js` | Values in `Object`-typed members (`Tag`, `PropertySet`) read back as JS primitives. | | `lifetime.js` | JS subclass instances that nothing references are collected on both sides. An instance only native code holds keeps working, with its JS object revived. WinRT UI subclasses in the visual tree keep their fields. Needs `gc()`, so it is skipped on engines that don't expose it. | +| `winrt-async.js` | `NSWinRT.toPromise` on `IAsyncAction`/`IAsyncOperation`: results and rejection. Operations are not thenable, so promises keep the operation itself, and they expose the `IAsyncInfo` members they inherit (`Status`, `Cancel`). | | `layout-mutation.js` | Adding, removing and reparenting XAML elements synchronously from `SizeChanged`, `LayoutUpdated` and `CompositionTarget.Rendering`, and from promise continuations scheduled there. | Each spec asserts behaviour that works the same way on iOS and Android. diff --git a/test-app/app/main.js b/test-app/app/main.js index 7446752..e56d8cd 100644 --- a/test-app/app/main.js +++ b/test-app/app/main.js @@ -20,6 +20,7 @@ const specs = [ "./tests/boxing.js", "./tests/layout-mutation.js", "./tests/lifetime.js", + "./tests/winrt-async.js", ]; function env(name) { diff --git a/test-app/app/tests/winrt-async.js b/test-app/app/tests/winrt-async.js new file mode 100644 index 0000000..67b0282 --- /dev/null +++ b/test-app/app/tests/winrt-async.js @@ -0,0 +1,51 @@ +// WinRT async operations (IAsyncAction, IAsyncOperation and the WithProgress variants) are +// converted to promises explicitly with NSWinRT.toPromise. They are not thenable, so an +// operation stays an operation and can be passed back to native code. + +const StorageFolder = Windows.Storage.StorageFolder; +const ThreadPool = Windows.System.Threading.ThreadPool; +const WINDOWS_DIR = "C:\\Windows"; + +describe("WinRT async operations", () => { + it("resolves an IAsyncOperation to its result", async () => { + const folder = await NSWinRT.toPromise(StorageFolder.GetFolderFromPathAsync(WINDOWS_DIR)); + expect(folder.Name).toBe("Windows"); + }); + + it("resolves an IAsyncAction to undefined", async () => { + let ran = false; + const result = await NSWinRT.toPromise( + ThreadPool.RunAsync(() => { + ran = true; + }), + ); + expect(ran).toBe(true); + expect(result).toBe(undefined); + }); + + it("rejects when the operation fails", async () => { + let error = null; + try { + await NSWinRT.toPromise(StorageFolder.GetFolderFromPathAsync("C:\\ns-does-not-exist-7f3a")); + } catch (e) { + error = e; + } + expect(error).not.toBe(null); + }); + + it("is not thenable, so promises keep the operation itself", async () => { + const op = StorageFolder.GetFolderFromPathAsync(WINDOWS_DIR); + expect(typeof op.then).toBe("undefined"); + expect(await Promise.resolve(op)).toBe(op); + await NSWinRT.toPromise(op); + }); + + it("exposes the IAsyncInfo members it inherits", async () => { + const op = StorageFolder.GetFolderFromPathAsync(WINDOWS_DIR); + expect("Completed" in op).toBe(true); + expect(typeof op.Status).toBe("number"); + expect(typeof op.Cancel).toBe("function"); + await NSWinRT.toPromise(op); + expect(op.Status).toBe(1); + }); +}); diff --git a/windows-napi/test/async-test.js b/windows-napi/test/async-test.js index ea14aff..562c0a7 100644 --- a/windows-napi/test/async-test.js +++ b/windows-napi/test/async-test.js @@ -34,6 +34,26 @@ async function main() { let ran3 = false; await toPromise(ThreadPool.RunAsync(() => { ran3 = true; })); check('auto-pump transparent await', ran3 === true); + + // IAsyncOperation: toPromise resolves to GetResults() and rejects on failure. + const StorageFolder = Windows.Storage.StorageFolder; + const folder = await toPromise(StorageFolder.GetFolderFromPathAsync('C:\\Windows')); + check('toPromise(IAsyncOperation) resolves to GetResults()', folder && folder.Name === 'Windows'); + + let rejected = false; + try { + await toPromise(StorageFolder.GetFolderFromPathAsync('C:\\ns-does-not-exist-7f3a')); + } catch (_) { + rejected = true; + } + check('toPromise of a failing operation rejects', rejected); + + // Operations are not thenable, so they stay operations and can be passed back to native. + const op = StorageFolder.GetFolderFromPathAsync('C:\\Windows'); + check('operation is not thenable', typeof op.then === 'undefined'); + check('Promise.resolve keeps the operation', (await Promise.resolve(op)) === op); + check('operation exposes IAsyncInfo.Status', typeof op.Status === 'number'); + await toPromise(op); disableAutoPump(); console.log(`\n${pass} passed, ${fail} failed`);