diff --git a/NativeScriptWindowsDemo/App/core/ui.js b/NativeScriptWindowsDemo/App/core/ui.js new file mode 100644 index 0000000..448bbce --- /dev/null +++ b/NativeScriptWindowsDemo/App/core/ui.js @@ -0,0 +1,184 @@ +const delegateKeepAlive = []; + +export function resolveType(typeName) { + const parts = String(typeName).split("."); + let current = globalThis; + for (let i = 0; i < parts.length; i += 1) { + current = current && current[parts[i]]; + } + return current; +} + +export function text(value, size) { + const control = new Windows.UI.Xaml.Controls.TextBlock(); + control.Text = String(value); + if (size) { + control.FontSize = size; + } + return control; +} + +export function createTypedDelegate(typeName, callback) { + const TypeCtor = resolveType(typeName); + if (typeof TypeCtor !== "function") { + return null; + } + + try { + const delegate = new TypeCtor({ + Invoke: function () { + return callback.apply(null, arguments); + }, + }); + delegateKeepAlive.push(delegate); + return delegate; + } catch (_error) { + return null; + } +} + +export function bindClick(button, onClick) { + const callback = typeof onClick === "function" ? onClick : function () {}; + const delegate = createTypedDelegate("Windows.UI.Xaml.RoutedEventHandler", callback); + + if (delegate && typeof button.add_Click === "function") { + button.add_Click(delegate); + return; + } + + button.Click = delegate || callback; +} + +export function createButton(label, onClick) { + const button = new Windows.UI.Xaml.Controls.Button(); + button.Height = 42; + button.Content = String(label); + bindClick(button, onClick); + return button; +} + +export function createCard(titleValue, bodyValue) { + const border = new Windows.UI.Xaml.Controls.Border(); + + const stack = new Windows.UI.Xaml.Controls.StackPanel(); + stack.Spacing = 8; + + const titleText = text(titleValue, 19); + const bodyText = text(bodyValue, 14); + bodyText.TextWrapping = Windows.UI.Xaml.TextWrapping.Wrap; + + stack.Children.Append(titleText); + stack.Children.Append(bodyText); + + border.Child = stack; + return border; +} + +export function withContentPadding(content) { + const outer = new Windows.UI.Xaml.Controls.StackPanel(); + + const topSpacer = new Windows.UI.Xaml.Controls.Border(); + topSpacer.Height = 18; + outer.Children.Append(topSpacer); + + const row = new Windows.UI.Xaml.Controls.StackPanel(); + row.Orientation = Windows.UI.Xaml.Controls.Orientation.Horizontal; + + const leftSpacer = new Windows.UI.Xaml.Controls.Border(); + leftSpacer.Width = 24; + + row.Children.Append(leftSpacer); + row.Children.Append(content); + outer.Children.Append(row); + + return outer; +} + +export function safeGetChildrenSize(element) { + try { + const children = element && element.Children; + if (children && typeof children.Size === "number") { + return children.Size; + } + } catch (_error) { + } + + return -1; +} + +export function createBrandImage(width, height) { + const host = new Windows.UI.Xaml.Controls.StackPanel(); + host.Spacing = 0; + + const image = new Windows.UI.Xaml.Controls.Image(); + image.Width = width || 280; + image.Height = height || 120; + image.Stretch = Windows.UI.Xaml.Media.Stretch.Uniform; + + const fallback = text("NativeScript", 44); + fallback.Visibility = Windows.UI.Xaml.Visibility.Collapsed; + + host.Children.Append(image); + host.Children.Append(fallback); + + const candidateUris = [ + "ms-appx:///Assets/Wide310x150Logo.scale-200.png", + "ms-appx:///Assets/Square150x150Logo.scale-200.png", + "ms-appx:///Assets/StoreLogo.png", + "ms-appx:///Assets/cover.png", + "https://raw.githubusercontent.com/NativeScript/NativeScript/main/tools/graphics/cover.png", + ].filter(function (value) { + return typeof value === "string" && value.length > 0; + }); + + let assigned = false; + + const tryAssignImageSource = function () { + for (let i = 0; i < candidateUris.length; i += 1) { + const uriValue = candidateUris[i]; + console.log("NativeScriptWindowsDemo: trying image URI", uriValue); + + try { + const uri = new Windows.Foundation.Uri(uriValue); + const bitmap = new Windows.UI.Xaml.Media.Imaging.BitmapImage(); + bitmap.UriSource = uri; + image.Source = bitmap; + console.log("NativeScriptWindowsDemo: image source assigned", uriValue); + assigned = true; + return true; + } catch (error) { + console.log("NativeScriptWindowsDemo: invalid image URI", uriValue, error && error.message ? error.message : error); + } + } + + return false; + }; + + const showTextFallback = function () { + image.Visibility = Windows.UI.Xaml.Visibility.Collapsed; + fallback.Visibility = Windows.UI.Xaml.Visibility.Visible; + }; + + if (!tryAssignImageSource()) { + showTextFallback(); + } + + if (!assigned) { + showTextFallback(); + } + + return host; +} + +export function openExternalUrl(url) { + try { + const launchOp = Windows.System.Launcher.LaunchUriAsync(new Windows.Foundation.Uri(String(url))); + if (launchOp && typeof NSWinRT !== "undefined" && typeof NSWinRT.onCompleted === "function") { + NSWinRT.onCompleted(launchOp, function (_asyncInfo, status) { + console.log("NativeScriptWindowsDemo: launch status", status, url); + }, { timeoutMs: 12000 }); + } + } catch (error) { + console.log("NativeScriptWindowsDemo: failed to open URL", url, error && error.message ? error.message : error); + } +} diff --git a/NativeScriptWindowsDemo/App/features/components.js b/NativeScriptWindowsDemo/App/features/components.js new file mode 100644 index 0000000..7e87aa5 --- /dev/null +++ b/NativeScriptWindowsDemo/App/features/components.js @@ -0,0 +1,59 @@ +import { + createButton, + createCard, + safeGetChildrenSize, + text, +} from "../core/ui.js"; + +export function createComponentsPage() { + const root = new Windows.UI.Xaml.Controls.StackPanel(); + root.Spacing = 12; + + root.Children.Append(text("Components Lab", 34)); + + const description = text( + "This tab demonstrates real WinRT controls manipulated from JavaScript event handlers.", + 14, + ); + description.TextWrapping = Windows.UI.Xaml.TextWrapping.Wrap; + root.Children.Append(description); + + const toggle = new Windows.UI.Xaml.Controls.ToggleSwitch(); + toggle.Header = "Enable runtime polish"; + toggle.IsOn = true; + + const slider = new Windows.UI.Xaml.Controls.Slider(); + slider.Minimum = 0; + slider.Maximum = 100; + slider.Value = 40; + slider.Width = 360; + + const progress = new Windows.UI.Xaml.Controls.ProgressBar(); + progress.Minimum = 0; + progress.Maximum = 100; + progress.Value = 40; + progress.Width = 360; + + const status = text("State: polish enabled at 40%", 13); + + const applyButton = createButton("Apply Slider To Progress", function () { + progress.Value = slider.Value; + status.Text = "State: " + (toggle.IsOn ? "polish enabled" : "polish disabled") + " at " + Math.round(slider.Value) + "%"; + }); + + root.Children.Append(toggle); + root.Children.Append(slider); + root.Children.Append(progress); + root.Children.Append(applyButton); + root.Children.Append(status); + + root.Children.Append( + createCard( + "Copyable Pattern", + "Instantiate controls, keep references, and update state on click handlers.", + ), + ); + + console.log("NativeScriptWindowsDemo: components children", safeGetChildrenSize(root)); + return root; +} diff --git a/NativeScriptWindowsDemo/App/features/dashboard.js b/NativeScriptWindowsDemo/App/features/dashboard.js new file mode 100644 index 0000000..f095072 --- /dev/null +++ b/NativeScriptWindowsDemo/App/features/dashboard.js @@ -0,0 +1,70 @@ +import { + createBrandImage, + createButton, + createCard, + openExternalUrl, + safeGetChildrenSize, + text, +} from "../core/ui.js"; + +export function createDashboardPage(onNavigate) { + const root = new Windows.UI.Xaml.Controls.StackPanel(); + root.Spacing = 14; + + root.Children.Append(text("NativeScript for Windows", 42)); + + const subtitle = text( + "Build native desktop apps with JavaScript, first-class WinRT interop, and a runtime that feels fast and direct.", + 16, + ); + subtitle.TextWrapping = Windows.UI.Xaml.TextWrapping.Wrap; + root.Children.Append(subtitle); + + root.Children.Append(createBrandImage(380, 200)); + + root.Children.Append( + createCard( + "Single JavaScript Runtime", + "Use one language across startup, UI composition, interop calls, and event binding.", + ), + ); + root.Children.Append( + createCard( + "Real Native Controls", + "NavigationView, ToggleSwitch, SplitView, and every WinRT type are available directly from JavaScript.", + ), + ); + root.Children.Append( + createCard( + "Production-minded Tooling", + "Build from Rust crates, test with sample apps, and iterate quickly without leaving native APIs behind.", + ), + ); + + const ctaRow = new Windows.UI.Xaml.Controls.StackPanel(); + ctaRow.Orientation = Windows.UI.Xaml.Controls.Orientation.Horizontal; + ctaRow.Spacing = 8; + + ctaRow.Children.Append( + createButton("Explore Components Demo", function () { + if (typeof onNavigate === "function") { + onNavigate("components"); + } + }), + ); + + ctaRow.Children.Append( + createButton("Visit NativeScript.org", function () { + openExternalUrl("https://nativescript.org"); + }), + ); + + root.Children.Append(ctaRow); + + root.Children.Append( + text("Copy the patterns from each tab to build your own native app shell.", 13), + ); + + console.log("NativeScriptWindowsDemo: dashboard children", safeGetChildrenSize(root)); + return root; +} diff --git a/NativeScriptWindowsDemo/App/features/examples.js b/NativeScriptWindowsDemo/App/features/examples.js new file mode 100644 index 0000000..dca9c28 --- /dev/null +++ b/NativeScriptWindowsDemo/App/features/examples.js @@ -0,0 +1,84 @@ +import { + createButton, + createCard, + safeGetChildrenSize, + text, +} from "../core/ui.js"; + +export function createExamplesPage() { + const root = new Windows.UI.Xaml.Controls.StackPanel(); + root.Spacing = 12; + + root.Children.Append(text("Examples", 34)); + + const intro = text("Interactive examples implemented in plain JavaScript.", 14); + intro.TextWrapping = Windows.UI.Xaml.TextWrapping.Wrap; + root.Children.Append(intro); + + const counterTitle = text("Counter Demo", 20); + root.Children.Append(counterTitle); + + const counterRow = new Windows.UI.Xaml.Controls.StackPanel(); + counterRow.Orientation = Windows.UI.Xaml.Controls.Orientation.Horizontal; + counterRow.Spacing = 10; + + const valueText = text("0", 24); + let value = 0; + + counterRow.Children.Append( + createButton("-", function () { + value = Math.max(value - 1, 0); + valueText.Text = String(value); + }), + ); + counterRow.Children.Append(valueText); + counterRow.Children.Append( + createButton("+", function () { + value += 1; + valueText.Text = String(value); + }), + ); + + root.Children.Append(counterRow); + + root.Children.Append(text("Action Feed", 20)); + + const feed = new Windows.UI.Xaml.Controls.StackPanel(); + feed.Spacing = 4; + + const addFeedEntry = function (entry) { + if (feed.Children.Size > 6) { + feed.Children.RemoveAt(0); + } + feed.Children.Append(text("- " + entry, 13)); + }; + + const examplesRow = new Windows.UI.Xaml.Controls.StackPanel(); + examplesRow.Orientation = Windows.UI.Xaml.Controls.Orientation.Horizontal; + examplesRow.Spacing = 8; + + examplesRow.Children.Append( + createButton("Track Counter", function () { + addFeedEntry("Counter currently at " + value); + }), + ); + + examplesRow.Children.Append( + createButton("Simulate Save", function () { + addFeedEntry("Persisted runtime demo state"); + }), + ); + + root.Children.Append(examplesRow); + root.Children.Append(feed); + + root.Children.Append( + createCard( + "Why this matters", + "You can compose useful behavior with plain JS functions and native controls, then scale into larger modules.", + ), + ); + + console.log("NativeScriptWindowsDemo: examples children", safeGetChildrenSize(root)); + return root; +} diff --git a/NativeScriptWindowsDemo/App/features/getting-started.js b/NativeScriptWindowsDemo/App/features/getting-started.js new file mode 100644 index 0000000..278b54b --- /dev/null +++ b/NativeScriptWindowsDemo/App/features/getting-started.js @@ -0,0 +1,43 @@ +import { + createBrandImage, + createCard, + safeGetChildrenSize, + text, +} from "../core/ui.js"; + +export function createGettingStartedPage() { + const root = new Windows.UI.Xaml.Controls.StackPanel(); + root.Spacing = 12; + + root.Children.Append(text("Getting Started", 34)); + root.Children.Append(createBrandImage(220, 120)); + + const intro = text("Hello NativeScript Windows", 22); + root.Children.Append(intro); + + const sub = text("Let us build something native together.", 16); + sub.TextWrapping = Windows.UI.Xaml.TextWrapping.Wrap; + root.Children.Append(sub); + + root.Children.Append( + createCard( + "1. Explore Native Controls", + "Use the Components tab to try ToggleSwitch, Slider, ProgressRing, and native event wiring.", + ), + ); + root.Children.Append( + createCard( + "2. Explore Full Examples", + "Use the Examples tab for interactive counter and list patterns in plain JavaScript.", + ), + ); + root.Children.Append( + createCard( + "3. Ship and Share", + "Use this shell as your launchpad for real desktop experiences backed by Rust runtime speed.", + ), + ); + + console.log("NativeScriptWindowsDemo: getting-started children", safeGetChildrenSize(root)); + return root; +} diff --git a/NativeScriptWindowsDemo/App/features/setup.js b/NativeScriptWindowsDemo/App/features/setup.js new file mode 100644 index 0000000..50f50b1 --- /dev/null +++ b/NativeScriptWindowsDemo/App/features/setup.js @@ -0,0 +1,43 @@ +import { + createButton, + createCard, + openExternalUrl, + safeGetChildrenSize, + text, +} from "../core/ui.js"; + +export function createSetupPage() { + const root = new Windows.UI.Xaml.Controls.StackPanel(); + root.Spacing = 12; + + root.Children.Append(text("Setup", 34)); + + const body = text( + "Bootstrap your own app from the NativeScript template and map these demo tabs to your production flows.", + 14, + ); + body.TextWrapping = Windows.UI.Xaml.TextWrapping.Wrap; + root.Children.Append(body); + + root.Children.Append( + createButton("Open NativeScript Starter Templates", function () { + openExternalUrl("https://github.com/NativeScript"); + }), + ); + + root.Children.Append( + createCard( + "Runtime + UI", + "This sample runs pure JavaScript while driving native WinRT controls with no JSX/TSX layer.", + ), + ); + root.Children.Append( + createCard( + "Modular Features", + "Each tab lives in its own file so teams can evolve pages independently.", + ), + ); + + console.log("NativeScriptWindowsDemo: setup children", safeGetChildrenSize(root)); + return root; +} diff --git a/NativeScriptWindowsDemo/App/main.js b/NativeScriptWindowsDemo/App/main.js index 478b712..c4dee64 100644 --- a/NativeScriptWindowsDemo/App/main.js +++ b/NativeScriptWindowsDemo/App/main.js @@ -1,159 +1,169 @@ -console.log("NativeScriptWindowsDemo: booting JS UI demo"); - -function createSectionTitle(text) { - const title = new Windows.UI.Xaml.Controls.TextBlock(); - title.Text = text; - title.FontSize = 18; - title.Margin = new Windows.UI.Xaml.Thickness(0, 16, 0, 8); - return title; +import { createButton, text, withContentPadding } from "./core/ui.js"; +import { createDashboardPage } from "./features/dashboard.js"; +import { createGettingStartedPage } from "./features/getting-started.js"; +import { createSetupPage } from "./features/setup.js"; +import { createComponentsPage } from "./features/components.js"; +import { createExamplesPage } from "./features/examples.js"; + +console.log("NativeScriptWindowsDemo: booting"); + +function createShell() { + console.log("NativeScriptWindowsDemo: createShell start"); + try { + const navigation = new Windows.UI.Xaml.Controls.NavigationView(); + navigation.PaneTitle = "NativeScript Studio"; + navigation.IsSettingsVisible = false; + navigation.PaneDisplayMode = Windows.UI.Xaml.Controls.NavigationViewPaneDisplayMode.Left; + navigation.IsBackButtonVisible = Windows.UI.Xaml.Controls.NavigationViewBackButtonVisible.Collapsed; + navigation.IsPaneToggleButtonVisible = true; + + console.log("NativeScriptWindowsDemo: createShell done (NavigationView)"); + return { kind: "navigation", navigation }; + } catch (error) { + console.log( + "NativeScriptWindowsDemo: NavigationView unavailable, falling back to SplitView", + error && error.message ? error.message : error, + ); + } + + const split = new Windows.UI.Xaml.Controls.SplitView(); + split.DisplayMode = Windows.UI.Xaml.Controls.SplitViewDisplayMode.Inline; + split.IsPaneOpen = true; + split.OpenPaneLength = 260; + + const pane = new Windows.UI.Xaml.Controls.StackPanel(); + pane.Spacing = 6; + pane.Children.Append(text("NativeScript Studio", 22)); + pane.Children.Append(text("Windows runtime demo tabs", 12)); + + split.Pane = pane; + + console.log("NativeScriptWindowsDemo: createShell done (SplitView fallback)"); + return { kind: "split", split, pane }; } -function createControlCard() { - const border = new Windows.UI.Xaml.Controls.Border(); - border.BorderBrush = new Windows.UI.Xaml.Media.SolidColorBrush(Windows.UI.Colors.LightGray); - border.BorderThickness = new Windows.UI.Xaml.Thickness(1); - border.Padding = new Windows.UI.Xaml.Thickness(12); - border.CornerRadius = new Windows.UI.Xaml.CornerRadius(8); - - const stack = new Windows.UI.Xaml.Controls.StackPanel(); - stack.Spacing = 8; - border.Child = stack; - return { border, stack }; +function createNavigationItem(key, label, iconGlyph) { + const item = new Windows.UI.Xaml.Controls.NavigationViewItem(); + item.Tag = key; + item.Content = String(label); + + const icon = new Windows.UI.Xaml.Controls.FontIcon(); + icon.Glyph = iconGlyph; + item.Icon = icon; + + return item; +} + +function createScrollablePage(content) { + const scroll = new Windows.UI.Xaml.Controls.ScrollViewer(); + scroll.VerticalScrollBarVisibility = Windows.UI.Xaml.Controls.ScrollBarVisibility.Auto; + scroll.HorizontalScrollBarVisibility = Windows.UI.Xaml.Controls.ScrollBarVisibility.Disabled; + scroll.Content = withContentPadding(content); + return scroll; } -function buildDemoLayout() { - const rootScroll = new Windows.UI.Xaml.Controls.ScrollViewer(); - rootScroll.VerticalScrollBarVisibility = Windows.UI.Xaml.Controls.ScrollBarVisibility.Auto; - - const content = new Windows.UI.Xaml.Controls.StackPanel(); - content.Margin = new Windows.UI.Xaml.Thickness(24, 20, 24, 20); - content.Spacing = 10; - rootScroll.Content = content; - - const header = new Windows.UI.Xaml.Controls.TextBlock(); - header.Text = "NativeScript Windows Demo"; - header.FontSize = 30; - header.FontWeight = Windows.UI.Text.FontWeights.SemiBold; - content.Children.Append(header); - - const subtitle = new Windows.UI.Xaml.Controls.TextBlock(); - subtitle.Text = "WinRT XAML controls created directly from JavaScript"; - subtitle.FontSize = 14; - subtitle.Foreground = new Windows.UI.Xaml.Media.SolidColorBrush(Windows.UI.Colors.DimGray); - subtitle.Margin = new Windows.UI.Xaml.Thickness(0, 0, 0, 6); - content.Children.Append(subtitle); - - content.Children.Append(createSectionTitle("Inputs")); - const inputCard = createControlCard(); - - const nameBox = new Windows.UI.Xaml.Controls.TextBox(); - nameBox.Header = "Display name"; - nameBox.PlaceholderText = "Type your name"; - - const password = new Windows.UI.Xaml.Controls.PasswordBox(); - password.Header = "Password"; - - const toggle = new Windows.UI.Xaml.Controls.ToggleSwitch(); - toggle.Header = "Enable notifications"; - toggle.IsOn = true; - - inputCard.stack.Children.Append(nameBox); - inputCard.stack.Children.Append(password); - inputCard.stack.Children.Append(toggle); - content.Children.Append(inputCard.border); - - content.Children.Append(createSectionTitle("Selection")); - const selectionCard = createControlCard(); - - const combo = new Windows.UI.Xaml.Controls.ComboBox(); - combo.Header = "Theme"; - combo.Items.Append("Ocean"); - combo.Items.Append("Forest"); - combo.Items.Append("Sunset"); - combo.SelectedIndex = 0; - - const sliderLabel = new Windows.UI.Xaml.Controls.TextBlock(); - sliderLabel.Text = "Volume"; - const slider = new Windows.UI.Xaml.Controls.Slider(); - slider.Minimum = 0; - slider.Maximum = 100; - slider.Value = 72; - - const progress = new Windows.UI.Xaml.Controls.ProgressBar(); - progress.Minimum = 0; - progress.Maximum = 100; - progress.Value = slider.Value; - progress.Height = 8; - - slider.ValueChanged = function (sender) { - progress.Value = sender.Value; - }; - - selectionCard.stack.Children.Append(combo); - selectionCard.stack.Children.Append(sliderLabel); - selectionCard.stack.Children.Append(slider); - selectionCard.stack.Children.Append(progress); - content.Children.Append(selectionCard.border); - - content.Children.Append(createSectionTitle("Actions")); - const actionCard = createControlCard(); - const actionRow = new Windows.UI.Xaml.Controls.StackPanel(); - actionRow.Orientation = Windows.UI.Xaml.Controls.Orientation.Horizontal; - actionRow.Spacing = 10; - - const applyButton = new Windows.UI.Xaml.Controls.Button(); - applyButton.Content = "Apply"; - applyButton.MinWidth = 120; - - const resetButton = new Windows.UI.Xaml.Controls.Button(); - resetButton.Content = "Reset"; - resetButton.MinWidth = 120; - - const status = new Windows.UI.Xaml.Controls.TextBlock(); - status.Text = "Ready"; - status.Margin = new Windows.UI.Xaml.Thickness(0, 6, 0, 0); - - applyButton.Click = function () { - const selectedTheme = combo.SelectedItem ? String(combo.SelectedItem) : "Unknown"; - status.Text = "Saved for " + (nameBox.Text || "Guest") + " with " + selectedTheme + " theme"; - }; - - resetButton.Click = function () { - nameBox.Text = ""; - password.Password = ""; - toggle.IsOn = true; - combo.SelectedIndex = 0; - slider.Value = 72; - status.Text = "Reset complete"; - }; - - actionRow.Children.Append(applyButton); - actionRow.Children.Append(resetButton); - actionCard.stack.Children.Append(actionRow); - actionCard.stack.Children.Append(status); - content.Children.Append(actionCard.border); - - content.Children.Append(createSectionTitle("Data List")); - const listCard = createControlCard(); - const list = new Windows.UI.Xaml.Controls.ListView(); - list.Height = 180; - list.Items.Append("Dashboard"); - list.Items.Append("Analytics"); - list.Items.Append("Notifications"); - list.Items.Append("Reports"); - list.Items.Append("Settings"); - listCard.stack.Children.Append(list); - content.Children.Append(listCard.border); - - return rootScroll; +function buildApp() { + console.log("NativeScriptWindowsDemo: buildApp start"); + + const shell = createShell(); + const navItemsByKey = {}; + + const pages = { + dashboard: createScrollablePage(createDashboardPage(showPage)), + gettingStarted: createScrollablePage(createGettingStartedPage()), + setup: createScrollablePage(createSetupPage()), + components: createScrollablePage(createComponentsPage()), + examples: createScrollablePage(createExamplesPage()), + }; + + function showPage(key) { + const page = pages[key] || pages.dashboard; + if (shell.kind === "navigation") { + const host = shell.contentHost; + if (host && typeof host === "object") { + host.Child = page; + } else { + shell.navigation.Content = page; + } + return; + } + + shell.split.Content = page; + shell.split.IsPaneOpen = true; + } + + if (shell.kind === "navigation") { + const host = new Windows.UI.Xaml.Controls.Border(); + shell.contentHost = host; + shell.navigation.Content = host; + + const dashboardItem = createNavigationItem("dashboard", "Overview", "\uE80F"); + const gettingStartedItem = createNavigationItem("gettingStarted", "Getting Started", "\uE8FD"); + const setupItem = createNavigationItem("setup", "Setup", "\uE713"); + const componentsItem = createNavigationItem("components", "Components", "\uE8B8"); + const examplesItem = createNavigationItem("examples", "Examples", "\uE9CE"); + + navItemsByKey.dashboard = dashboardItem; + navItemsByKey.gettingStarted = gettingStartedItem; + navItemsByKey.setup = setupItem; + navItemsByKey.components = componentsItem; + navItemsByKey.examples = examplesItem; + + shell.navigation.MenuItems.Append(dashboardItem); + shell.navigation.MenuItems.Append(gettingStartedItem); + shell.navigation.MenuItems.Append(setupItem); + shell.navigation.MenuItems.Append(componentsItem); + shell.navigation.MenuItems.Append(examplesItem); + + shell.navigation.SelectedItem = dashboardItem; + host.Child = pages.dashboard; + + const resolveNavigationKey = function () { + const keys = Object.keys(navItemsByKey); + + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; + const item = navItemsByKey[key]; + if (!item) { + continue; + } + + try { + if (item.IsSelected) { + return key; + } + } catch (_error) { + } + } + + return null; + }; + + shell.navigation.SelectionChanged = function () { + const key = resolveNavigationKey(); + if (key && pages[key]) { + showPage(key); + } + }; + } else { + shell.pane.Children.Append(createButton("Overview", function () { showPage("dashboard"); })); + shell.pane.Children.Append(createButton("Getting Started", function () { showPage("gettingStarted"); })); + shell.pane.Children.Append(createButton("Setup", function () { showPage("setup"); })); + shell.pane.Children.Append(createButton("Components", function () { showPage("components"); })); + shell.pane.Children.Append(createButton("Examples", function () { showPage("examples"); })); + } + + showPage("dashboard"); + console.log("NativeScriptWindowsDemo: buildApp done"); + return shell.kind === "navigation" ? shell.navigation : shell.split; } try { - const window = Windows.UI.Xaml.Window.Current; - window.Content = buildDemoLayout(); - window.Activate(); - console.log("NativeScriptWindowsDemo: layout created"); + const window = Windows.UI.Xaml.Window.Current; + window.Content = buildApp(); + window.Activate(); + console.log("NativeScriptWindowsDemo: loaded"); } catch (error) { - console.log("NativeScriptWindowsDemo: failed to build layout", error && error.message ? error.message : error); - throw error; + console.log("NativeScriptWindowsDemo: failed", error && error.message ? error.message : error); + throw error; } diff --git a/NativeScriptWindowsDemo/Assets/LockScreenLogo.scale-200.png b/NativeScriptWindowsDemo/Assets/LockScreenLogo.scale-200.png index 735f57a..502dbad 100644 Binary files a/NativeScriptWindowsDemo/Assets/LockScreenLogo.scale-200.png and b/NativeScriptWindowsDemo/Assets/LockScreenLogo.scale-200.png differ diff --git a/NativeScriptWindowsDemo/Assets/SplashScreen.scale-200.png b/NativeScriptWindowsDemo/Assets/SplashScreen.scale-200.png index 023e7f1..26a858c 100644 Binary files a/NativeScriptWindowsDemo/Assets/SplashScreen.scale-200.png and b/NativeScriptWindowsDemo/Assets/SplashScreen.scale-200.png differ diff --git a/NativeScriptWindowsDemo/Assets/Square150x150Logo.scale-200.png b/NativeScriptWindowsDemo/Assets/Square150x150Logo.scale-200.png index af49fec..984b1d0 100644 Binary files a/NativeScriptWindowsDemo/Assets/Square150x150Logo.scale-200.png and b/NativeScriptWindowsDemo/Assets/Square150x150Logo.scale-200.png differ diff --git a/NativeScriptWindowsDemo/Assets/Square44x44Logo.scale-200.png b/NativeScriptWindowsDemo/Assets/Square44x44Logo.scale-200.png index ce342a2..cb6323d 100644 Binary files a/NativeScriptWindowsDemo/Assets/Square44x44Logo.scale-200.png and b/NativeScriptWindowsDemo/Assets/Square44x44Logo.scale-200.png differ diff --git a/NativeScriptWindowsDemo/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/NativeScriptWindowsDemo/Assets/Square44x44Logo.targetsize-24_altform-unplated.png index f6c02ce..8b8dce0 100644 Binary files a/NativeScriptWindowsDemo/Assets/Square44x44Logo.targetsize-24_altform-unplated.png and b/NativeScriptWindowsDemo/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/NativeScriptWindowsDemo/Assets/StoreLogo.png b/NativeScriptWindowsDemo/Assets/StoreLogo.png index 7385b56..001ccfc 100644 Binary files a/NativeScriptWindowsDemo/Assets/StoreLogo.png and b/NativeScriptWindowsDemo/Assets/StoreLogo.png differ diff --git a/NativeScriptWindowsDemo/Assets/Wide310x150Logo.scale-200.png b/NativeScriptWindowsDemo/Assets/Wide310x150Logo.scale-200.png index 288995b..26ea95d 100644 Binary files a/NativeScriptWindowsDemo/Assets/Wide310x150Logo.scale-200.png and b/NativeScriptWindowsDemo/Assets/Wide310x150Logo.scale-200.png differ diff --git a/NativeScriptWindowsDemo/Assets/cover.png b/NativeScriptWindowsDemo/Assets/cover.png new file mode 100644 index 0000000..f95aba2 Binary files /dev/null and b/NativeScriptWindowsDemo/Assets/cover.png differ diff --git a/NativeScriptWindowsDemo/NativeScriptWindowsDemo.csproj b/NativeScriptWindowsDemo/NativeScriptWindowsDemo.csproj index e7ade5e..1809675 100644 --- a/NativeScriptWindowsDemo/NativeScriptWindowsDemo.csproj +++ b/NativeScriptWindowsDemo/NativeScriptWindowsDemo.csproj @@ -37,6 +37,9 @@ PreserveNewest + + PreserveNewest + nativescript.dll diff --git a/NativeScriptWindowsDemo/RuntimeHost.cs b/NativeScriptWindowsDemo/RuntimeHost.cs index f1ac08a..6da8cd5 100644 --- a/NativeScriptWindowsDemo/RuntimeHost.cs +++ b/NativeScriptWindowsDemo/RuntimeHost.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics; using System.IO; +using System.Text; using System.Runtime.InteropServices; namespace NativeScriptWindowsDemo @@ -8,9 +10,12 @@ internal sealed class RuntimeHost : IDisposable { private const string NativeScriptLibrary = "nativescript"; - [DllImport("kernel32.dll")] + [DllImport("kernel32.dll", SetLastError = true)] private static extern bool AttachConsole(int dwProcessId); + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool AllocConsole(); + private const int ATTACH_PARENT_PROCESS = -1; [DllImport(NativeScriptLibrary, EntryPoint = nameof(runtime_init))] @@ -28,6 +33,46 @@ internal sealed class RuntimeHost : IDisposable private long _runtime; private bool _initialized; + private static void LogHost(string message) + { + Debug.WriteLine("[RuntimeHost] " + message); + try + { + Console.WriteLine("[RuntimeHost] " + message); + } + catch + { + // No attached console stream, debug output still receives logs. + } + } + + private static void EnsureDiagnosticsConsole() + { + var attached = AttachConsole(ATTACH_PARENT_PROCESS); + if (!attached) + { + var allocated = AllocConsole(); + if (!allocated) + { + var error = Marshal.GetLastWin32Error(); + LogHost("No console attached (Attach/Alloc failed, Win32=" + error + ")."); + return; + } + } + + try + { + var stdout = Console.OpenStandardOutput(); + var writer = new StreamWriter(stdout, new UTF8Encoding(false)) { AutoFlush = true }; + Console.SetOut(writer); + Console.SetError(writer); + } + catch (Exception ex) + { + Debug.WriteLine("[RuntimeHost] Failed to rebind console streams: " + ex.Message); + } + } + public void Initialize() { if (_initialized) @@ -35,8 +80,7 @@ public void Initialize() return; } - // Best effort for CLI-launched debug sessions. - AttachConsole(ATTACH_PARENT_PROCESS); + EnsureDiagnosticsConsole(); runtime_install_ctrlc_handler(0); _runtime = runtime_init(AppContext.BaseDirectory); diff --git a/TestApp/RuntimeHost.cs b/TestApp/RuntimeHost.cs index b5946dc..3592fc0 100644 --- a/TestApp/RuntimeHost.cs +++ b/TestApp/RuntimeHost.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics; using System.IO; +using System.Text; using System.Runtime.InteropServices; namespace TestApp @@ -8,9 +10,12 @@ internal sealed class RuntimeHost : IDisposable { private const string NativeScriptLibrary = "nativescript"; - [DllImport("kernel32.dll")] + [DllImport("kernel32.dll", SetLastError = true)] private static extern bool AttachConsole(int dwProcessId); + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool AllocConsole(); + private const int ATTACH_PARENT_PROCESS = -1; [DllImport(NativeScriptLibrary, EntryPoint = nameof(runtime_init))] @@ -28,6 +33,46 @@ internal sealed class RuntimeHost : IDisposable private long _runtime; private bool _initialized; + private static void LogHost(string message) + { + Debug.WriteLine("[RuntimeHost] " + message); + try + { + Console.WriteLine("[RuntimeHost] " + message); + } + catch + { + // No attached console stream, debug output still receives logs. + } + } + + private static void EnsureDiagnosticsConsole() + { + var attached = AttachConsole(ATTACH_PARENT_PROCESS); + if (!attached) + { + var allocated = AllocConsole(); + if (!allocated) + { + var error = Marshal.GetLastWin32Error(); + LogHost("No console attached (Attach/Alloc failed, Win32=" + error + ")."); + return; + } + } + + try + { + var stdout = Console.OpenStandardOutput(); + var writer = new StreamWriter(stdout, new UTF8Encoding(false)) { AutoFlush = true }; + Console.SetOut(writer); + Console.SetError(writer); + } + catch (Exception ex) + { + Debug.WriteLine("[RuntimeHost] Failed to rebind console streams: " + ex.Message); + } + } + public void Initialize() { if (_initialized) @@ -35,8 +80,7 @@ public void Initialize() return; } - // Best effort for CLI-launched debug sessions. - AttachConsole(ATTACH_PARENT_PROCESS); + EnsureDiagnosticsConsole(); runtime_install_ctrlc_handler(0); _runtime = runtime_init(AppContext.BaseDirectory); diff --git a/libs/x64/nativescript.dll b/libs/x64/nativescript.dll index e0c0058..c48c43c 100644 Binary files a/libs/x64/nativescript.dll and b/libs/x64/nativescript.dll differ diff --git a/metadata/src/declarations/event_declaration.rs b/metadata/src/declarations/event_declaration.rs index 0e49194..96b7c3d 100644 --- a/metadata/src/declarations/event_declaration.rs +++ b/metadata/src/declarations/event_declaration.rs @@ -204,6 +204,21 @@ impl EventDeclaration { .flatten() } + /// Returns the full name of the underlying delegate type, regardless of + /// whether it's a non-generic delegate, generic delegate, or generic + /// delegate instance (e.g. `TypedEventHandler`). + pub fn delegate_type_full_name(&self) -> Option { + self.type_ + .as_ref() + .map(|f| f.as_declaration().full_name().to_string()) + } + + /// Returns the GUID of the underlying delegate type for any kind of + /// delegate declaration (non-generic or generic instance). + pub fn delegate_type_id(&self) -> Option { + self.type_.as_ref().map(|f| f.id()) + } + pub fn add_method(&self) -> &MethodDeclaration { &self.add_method } diff --git a/metadata/src/declarations/interface_declaration/generic_interface_instance_declaration.rs b/metadata/src/declarations/interface_declaration/generic_interface_instance_declaration.rs index 983580b..cbf17e9 100644 --- a/metadata/src/declarations/interface_declaration/generic_interface_instance_declaration.rs +++ b/metadata/src/declarations/interface_declaration/generic_interface_instance_declaration.rs @@ -74,6 +74,24 @@ impl GenericInterfaceInstanceDeclaration { full_name } } + + pub fn new_with_full_name( + open_metadata: Option<&IMetaDataImport2>, + open_token: CorTokenType, + full_name: String, + ) -> Self { + Self { + base: InterfaceDeclaration::new_with_kind( + DeclarationKind::GenericInterfaceInstance, + open_metadata, + open_token, + ), + closed_metadata: None, + closed_token: CorTokenType::default(), + full_name, + } + } + pub fn id(&self) -> GUID { return GenericInstanceIdBuilder::generate_id(self); } diff --git a/nativescript/Cargo.toml b/nativescript/Cargo.toml index 4ad8af9..691304c 100644 --- a/nativescript/Cargo.toml +++ b/nativescript/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" [lib] name = "nativescript" -crate-type = ["dylib"] +crate-type = ["cdylib", "rlib"] [dependencies] diff --git a/runtime/src/helpers.rs b/runtime/src/helpers.rs index f08f3fd..9169a24 100644 --- a/runtime/src/helpers.rs +++ b/runtime/src/helpers.rs @@ -1,6 +1,126 @@ +use metadata::declarations::base_class_declaration::BaseClassDeclarationImpl; +use metadata::declarations::class_declaration::ClassDeclaration; +use metadata::declarations::declaration::DeclarationKind; +use metadata::declarations::enum_declaration::EnumDeclaration; +use metadata::declarations::interface_declaration::InterfaceDeclaration; +use metadata::meta_data_reader::MetadataReader; +use metadata::signature::Signature; use regex::Regex; +use windows::core::HRESULT; + use crate::value::NativeType; +/// E_FAIL — used as the HRESULT for failed COM/WinRT calls originating from JS. +#[inline] +pub fn call_failure() -> HRESULT { + HRESULT(0x8000_4005u32 as i32) +} + +/// Maps a WinRT signature string to the FFI ABI [`NativeType`] used to describe +/// the native parameter slot. +#[inline] +pub fn ffi_native_type_from_signature(signature: &str) -> NativeType { + let signature = signature.trim(); + let by_ref_inner = signature.strip_prefix("ByRef ").unwrap_or(signature); + + if let Some(element) = by_ref_inner.strip_suffix("[]") { + return match element { + "UInt8" | "Uint8" | "Int8" | "Byte" | "SByte" => NativeType::Buffer, + _ => NativeType::Pointer, + }; + } + + if by_ref_inner.starts_with("Var!") + || by_ref_inner.starts_with("MVar!") + || by_ref_inner.contains('.') + || by_ref_inner == "Object" + || by_ref_inner == "Guid" + { + return NativeType::Pointer; + } + + match by_ref_inner { + "Void" => NativeType::Void, + "String" => NativeType::Pointer, + "Char16" => NativeType::U16, + "Boolean" => NativeType::Bool, + "UInt8" | "Uint8" | "Byte" => NativeType::U8, + "Int8" | "SByte" => NativeType::I8, + "UInt16" => NativeType::U16, + "UInt32" => NativeType::U32, + "UInt64" => NativeType::U64, + "Int16" => NativeType::I16, + "Int32" => NativeType::I32, + "Int64" => NativeType::I64, + "Single" => NativeType::F32, + "Double" => NativeType::F64, + _ => NativeType::Pointer, + } +} + +/// Generic type parameters in projected WinRT signatures (`Var!`/`MVar!`) map +/// to opaque object pointers; everything else is unchanged. +#[inline] +pub fn normalize_parameter_signature(signature: &str) -> &str { + if signature.starts_with("Var!") || signature.starts_with("MVar!") { + return "Object"; + } + signature +} + +/// Best-effort mapping of a WinRT signature to the [`NativeType`] used when +/// parsing JS arguments. Falls back to [`NativeType::Pointer`] for anything not +/// directly representable as a primitive. +#[inline] +pub fn parse_native_type_from_signature(signature: &str) -> NativeType { + if signature.starts_with("Var!") || signature.starts_with("MVar!") { + return NativeType::Pointer; + } + + if let Ok(native_type) = NativeType::try_from(signature) { + if native_type != NativeType::Pointer { + return native_type; + } + } + + if let Some(declaration) = MetadataReader::find_by_name(signature) { + let lock = declaration.read(); + match lock.kind() { + DeclarationKind::Enum => { + if let Some(enum_declaration) = lock.as_any().downcast_ref::() { + let underlying_signature = Signature::as_string(&enum_declaration.type_()); + if let Ok(enum_native) = NativeType::try_from(underlying_signature.as_str()) { + return enum_native; + } + } + return NativeType::I32; + } + DeclarationKind::Class => { + if let Some(class_declaration) = lock.as_any().downcast_ref::() { + if class_declaration.base_full_name() == "System.Enum" { + return NativeType::I32; + } + } + } + _ => {} + } + } + + NativeType::Pointer +} + +/// Recursively counts methods of the inherited interface chain — used to +/// compute the vtable offset of a method declared on a parent interface. +pub fn inherited_interface_method_count(interfaces: &[&InterfaceDeclaration]) -> usize { + let mut count = 0usize; + for interface in interfaces { + count += interface.methods().len(); + count += inherited_interface_method_count(interface.implemented_interfaces().as_slice()); + } + count +} + + pub struct GenericReturnTypes<'s> { names: Vec<&'s str>, types: usize, @@ -41,44 +161,3 @@ pub fn get_generic_return_types(name: &str) -> GenericReturnTypes { GenericReturnTypes { names, types } } - -/// Shared mapping from WinRT signature string to FFI `NativeType`. -/// Used by `MethodCall` and `PropertyCall` during construction. -#[inline] -pub(crate) fn ffi_native_type_from_signature(signature: &str) -> NativeType { - let signature = signature.trim(); - let by_ref_inner = signature.strip_prefix("ByRef ").unwrap_or(signature); - - if let Some(element) = by_ref_inner.strip_suffix("[]") { - return match element { - "UInt8" | "Uint8" | "Int8" | "Byte" | "SByte" => NativeType::Buffer, - _ => NativeType::Pointer, - }; - } - - if by_ref_inner.starts_with("Var!") - || by_ref_inner.contains('.') - || by_ref_inner == "Object" - || by_ref_inner == "Guid" - { - return NativeType::Pointer; - } - - match by_ref_inner { - "Void" => NativeType::Void, - "String" => NativeType::Pointer, - "Char16" => NativeType::U16, - "Boolean" => NativeType::Bool, - "UInt8" | "Uint8" | "Byte" => NativeType::U8, - "Int8" | "SByte" => NativeType::I8, - "UInt16" => NativeType::U16, - "UInt32" => NativeType::U32, - "UInt64" => NativeType::U64, - "Int16" => NativeType::I16, - "Int32" => NativeType::I32, - "Int64" => NativeType::I64, - "Single" => NativeType::F32, - "Double" => NativeType::F64, - _ => NativeType::Pointer, - } -} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index a4909ef..c154454 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -14,18 +14,19 @@ mod proxy_manifest_loader; use std::any::Any; use std::cell::RefCell; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::ffi::{c_char, c_void, CString}; use std::fs; -use std::hash::{Hash, Hasher}; -use ahash::AHasher; use std::mem::MaybeUninit; use std::ops::Deref; +use std::panic::{catch_unwind, AssertUnwindSafe}; use std::path::{Path, PathBuf}; use std::ptr::{addr_of, addr_of_mut, NonNull}; use std::process::Command; use std::result; +use std::sync::atomic::{AtomicPtr, AtomicU32, AtomicU64, Ordering}; use std::sync::{Arc, Once, OnceLock}; +use std::thread::ThreadId; use std::time::{Duration, Instant}; use libffi::high::arg; use libffi::low::{CodePtr, ffi_type}; @@ -35,7 +36,7 @@ use parking_lot::lock_api::{MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLoc use v8::{FunctionTemplate, Global, Local, Number, Object}; use windows::core::{HSTRING, IUnknown, GUID, HRESULT, Interface, IUnknown_Vtbl, PCWSTR, IInspectable, Error}; use windows::Foundation::GuidHelper; -use windows::Win32::System::Com::{IDispatch, IDispatch_Vtbl}; +use windows::Win32::System::Com::{IAgileObject, IDispatch, IDispatch_Vtbl}; use windows::Win32::System::Console::GetConsoleWindow; use windows::Win32::System::WinRT::{IActivationFactory, RoActivateInstance, RoGetActivationFactory, RoInitialize, RoUninitialize, RO_INIT_SINGLETHREADED}; use windows::Win32::System::WinRT::Metadata::ELEMENT_TYPE_CHAR; @@ -53,6 +54,7 @@ use metadata::declarations::delegate_declaration::DelegateDeclarationImpl; use metadata::declarations::delegate_declaration::generic_delegate_declaration::GenericDelegateDeclaration; use metadata::declarations::delegate_declaration::generic_delegate_instance_declaration::GenericDelegateInstanceDeclaration; use metadata::declarations::enum_declaration::EnumDeclaration; +use metadata::declarations::event_declaration::EventDeclaration; use metadata::declarations::interface_declaration::InterfaceDeclaration; use metadata::declarations::namespace_declaration::NamespaceDeclaration; use metadata::declarations::type_declaration::TypeDeclaration; @@ -72,6 +74,7 @@ use crate::value::{ffi_parse_bool_arg, ffi_parse_buffer_arg, ffi_parse_f32_arg, use crate::proxy_manifest_loader::SbgManifestLoader; thread_local!(static ISOLATE: RefCell> = RefCell::new(None)); +thread_local!(static JS_DELEGATE_HANDLERS: RefCell>> = RefCell::new(HashMap::new())); pub struct Runtime { isolate: v8::OwnedIsolate, @@ -82,22 +85,267 @@ pub struct Runtime { static INIT: Once = Once::new(); static PROXY_MANIFESTS: OnceLock>> = OnceLock::new(); +static ACTIVE_RUNTIME_PTR: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); +static ACTIVE_RUNTIME_THREAD: OnceLock>> = OnceLock::new(); +static NEXT_DELEGATE_ID: AtomicU64 = AtomicU64::new(1); + +#[repr(C)] +struct JsDelegateComObject { + vtable: *const c_void, + ref_count: AtomicU32, + delegate_iid: GUID, + base_delegate_iid: Option, + handler_id: u64, +} -fn proxy_manifests() -> &'static Mutex> { - PROXY_MANIFESTS.get_or_init(|| Mutex::new(Vec::new())) +#[repr(C)] +struct JsDelegateComVtable { + iunknown_query_interface: + unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> HRESULT, + iunknown_add_ref: unsafe extern "system" fn(*mut c_void) -> u32, + iunknown_release: unsafe extern "system" fn(*mut c_void) -> u32, + delegate_invoke: unsafe extern "system" fn(*mut c_void, *mut c_void, *mut c_void) -> HRESULT, +} + +unsafe extern "system" fn js_delegate_query_interface( + this: *mut c_void, + iid: *const GUID, + interface: *mut *mut c_void, +) -> HRESULT { + if interface.is_null() { + return HRESULT(0x80004003u32 as i32); + } + + let object = this as *mut JsDelegateComObject; + if object.is_null() || iid.is_null() { + *interface = std::ptr::null_mut(); + return HRESULT(0x80004003u32 as i32); + } + + let requested = *iid; + let delegate_iid = (*object).delegate_iid; + let base_delegate_iid = (*object).base_delegate_iid; + if requested == IUnknown::IID + || requested == delegate_iid + || base_delegate_iid == Some(requested) + || requested == IAgileObject::IID + { + *interface = this; + js_delegate_add_ref(this); + return HRESULT(0); + } + + *interface = std::ptr::null_mut(); + HRESULT(0x80004002u32 as i32) +} + +unsafe extern "system" fn js_delegate_add_ref(this: *mut c_void) -> u32 { + let object = this as *mut JsDelegateComObject; + if object.is_null() { + return 0; + } + (*object).ref_count.fetch_add(1, Ordering::SeqCst) + 1 +} + +unsafe extern "system" fn js_delegate_release(this: *mut c_void) -> u32 { + let object = this as *mut JsDelegateComObject; + if object.is_null() { + return 0; + } + + let remaining = (*object).ref_count.fetch_sub(1, Ordering::SeqCst) - 1; + if remaining == 0 { + let _ = Box::from_raw(object); + } + remaining +} + +unsafe extern "system" fn js_delegate_invoke( + this: *mut c_void, + sender: *mut c_void, + args: *mut c_void, +) -> HRESULT { + let result = catch_unwind(AssertUnwindSafe(|| { + let object = this as *mut JsDelegateComObject; + if object.is_null() { + return HRESULT(0x80004003u32 as i32); + } + + let handler_id = (*object).handler_id; + invoke_registered_js_delegate(handler_id, sender, args); + HRESULT(0) + })); + + match result { + Ok(hr) => hr, + Err(_) => HRESULT(0x8000_4005u32 as i32), + } +} + +static JS_TWO_ARG_DELEGATE_VTABLE: JsDelegateComVtable = JsDelegateComVtable { + iunknown_query_interface: js_delegate_query_interface, + iunknown_add_ref: js_delegate_add_ref, + iunknown_release: js_delegate_release, + delegate_invoke: js_delegate_invoke, +}; + +fn create_native_delegate_instance( + delegate_iid: GUID, + base_delegate_iid: Option, + handler_id: u64, +) -> IUnknown { + let object = Box::new(JsDelegateComObject { + vtable: &JS_TWO_ARG_DELEGATE_VTABLE as *const _ as *const c_void, + ref_count: AtomicU32::new(1), + delegate_iid, + base_delegate_iid, + handler_id, + }); + + let raw = Box::into_raw(object) as *mut c_void; + unsafe { IUnknown::from_raw(raw) } } -/// Tracks hashes of already-loaded manifests to avoid O(N×size) string comparison. -static MANIFEST_HASHES: OnceLock>> = OnceLock::new(); +fn get_delegate_iid(lock: &dyn Declaration) -> Option { + match lock.kind() { + DeclarationKind::Delegate => lock + .as_any() + .downcast_ref::() + .map(|d| d.id()), + DeclarationKind::GenericDelegate => lock + .as_any() + .downcast_ref::() + .map(|d| d.id()), + DeclarationKind::GenericDelegateInstance => lock + .as_any() + .downcast_ref::() + .map(|d| d.id()), + _ => None, + } +} -fn manifest_hashes() -> &'static Mutex> { - MANIFEST_HASHES.get_or_init(|| Mutex::new(HashSet::new())) +fn get_generic_delegate_base_iid(full_name: &str) -> Option { + let generic_index = full_name.find('<')?; + let lookup_name = &full_name[..generic_index]; + let declaration = MetadataReader::find_by_name(lookup_name)?; + let lock = declaration.read(); + get_delegate_iid(lock.deref()) } -fn content_hash(s: &str) -> u64 { - let mut h = AHasher::default(); - s.hash(&mut h); - h.finish() +fn register_js_delegate_handler(callback: v8::Global) -> u64 { + let handler_id = NEXT_DELEGATE_ID.fetch_add(1, Ordering::SeqCst); + + JS_DELEGATE_HANDLERS.with(|handlers| { + handlers.borrow_mut().insert(handler_id, callback); + }); + + handler_id +} + +fn invoke_registered_js_delegate( + handler_id: u64, + sender: *mut c_void, + args: *mut c_void, +) { + if let Some(thread_lock) = ACTIVE_RUNTIME_THREAD.get() { + let expected_thread = thread_lock.lock().clone(); + if let Some(expected_thread) = expected_thread { + if std::thread::current().id() != expected_thread { + return; + } + } + } + + let runtime_ptr = ACTIVE_RUNTIME_PTR.load(Ordering::SeqCst); + if runtime_ptr.is_null() { + return; + } + + let callback = JS_DELEGATE_HANDLERS.with(|handlers| { + let handlers = handlers.borrow(); + handlers.get(&handler_id).cloned() + }); + + let Some(callback) = callback else { + return; + }; + + let runtime = unsafe { &mut *runtime_ptr }; + + v8::scope!(scope, &mut runtime.isolate); + let context = v8::Local::new(scope, &runtime.global_context); + let scope = &mut v8::ContextScope::new(scope, context); + + let callback = v8::Local::new(scope, callback); + let recv: v8::Local = context.global(scope).into(); + + let sender_value = wrap_inspectable_pointer_for_js(scope, sender); + let args_value = wrap_inspectable_pointer_for_js(scope, args); + + let _ = callback.call(scope, recv, &[sender_value, args_value]); + + scope.perform_microtask_checkpoint(); +} + +/// Best-effort wrap of a raw IInspectable pointer (as received in a delegate +/// Invoke call) into a JS object backed by the corresponding metadata-driven +/// declaration, so the JS handler can read properties like +/// `args.SelectedItem`. +/// +/// Returns `null` when the pointer is null or when the runtime class cannot be +/// resolved from metadata. +fn wrap_inspectable_pointer_for_js<'a>( + scope: &mut v8::PinScope<'a, '_>, + raw: *mut c_void, +) -> v8::Local<'a, v8::Value> { + if raw.is_null() { + return v8::null(scope).into(); + } + + let unknown = unsafe { + let u = IUnknown::from_raw_borrowed(&raw); + match u { + Some(u) => u.clone(), + None => { + return v8::null(scope).into(); + } + } + }; + + let inspectable: Option = unknown.cast().ok(); + + if let Some(inspectable) = inspectable.as_ref() { + match inspectable.GetRuntimeClassName() { + Ok(runtime_class) => { + let class_name = runtime_class.to_string(); + if !class_name.is_empty() { + if let Some(declaration) = MetadataReader::find_by_name(class_name.as_str()) + { + let short_name = { + let lock = declaration.read(); + lock.name().to_string() + }; + let inspectable_unknown: IUnknown = inspectable.clone().into(); + return create_ns_ctor_instance_object( + short_name.as_str(), + None, + None, + declaration, + Some(inspectable_unknown), + scope, + ); + } + } + } + Err(_e) => {} + } + } + + v8::null(scope).into() +} + +fn proxy_manifests() -> &'static Mutex> { + PROXY_MANIFESTS.get_or_init(|| Mutex::new(Vec::new())) } fn default_sbg_manifest_path() -> PathBuf { @@ -116,20 +364,14 @@ fn preload_sbg_manifest() { return; } - // Read once; feed the same string to both the loader and the dedup check. - let Ok(content) = fs::read_to_string(&manifest_path) else { return; }; - - let hash = content_hash(&content); - { - let mut hashes = manifest_hashes().lock(); - if !hashes.insert(hash) { - return; // already loaded - } - } - let mut loader = SbgManifestLoader::new(); - if loader.load_manifest_json(&content).is_ok() { - proxy_manifests().lock().push(content); + if loader.load_manifest_file(&manifest_path).is_ok() { + if let Ok(content) = fs::read_to_string(&manifest_path) { + let mut manifests = proxy_manifests().lock(); + if manifests.iter().all(|item| item != &content) { + manifests.push(content); + } + } } } @@ -235,57 +477,121 @@ fn collect_class_properties(class_declaration: &ClassDeclaration) -> Vec bool { - let method_match = |m: &MethodDeclaration| { - let on = m.overload_name(); - (!on.is_empty() && on == name) || m.name() == name - }; - - if class_declaration.methods().iter().any(method_match) { return true; } - - if let Some(di) = class_declaration.default_interface() { - if di.methods().iter().any(method_match) { return true; } +fn collect_declaration_methods(declaration: &dyn Declaration) -> Vec { + match declaration.kind() { + DeclarationKind::Class => declaration + .as_any() + .downcast_ref::() + .map(collect_class_methods) + .unwrap_or_default(), + DeclarationKind::Interface => declaration + .as_any() + .downcast_ref::() + .map(|interface| interface.methods().iter().cloned().collect()) + .unwrap_or_default(), + DeclarationKind::GenericInterface => declaration + .as_any() + .downcast_ref::() + .map(|interface| interface.methods().iter().cloned().collect()) + .unwrap_or_default(), + DeclarationKind::GenericInterfaceInstance => declaration + .as_any() + .downcast_ref::() + .map(|interface| interface.methods().iter().cloned().collect()) + .unwrap_or_default(), + _ => Vec::new(), } +} - for iface in class_declaration.implemented_interfaces() { - if iface.methods().iter().any(method_match) { return true; } +fn collect_declaration_properties(declaration: &dyn Declaration) -> Vec { + match declaration.kind() { + DeclarationKind::Class => declaration + .as_any() + .downcast_ref::() + .map(collect_class_properties) + .unwrap_or_default(), + DeclarationKind::Interface => declaration + .as_any() + .downcast_ref::() + .map(|interface| interface.properties().iter().cloned().collect()) + .unwrap_or_default(), + DeclarationKind::GenericInterface => declaration + .as_any() + .downcast_ref::() + .map(|interface| interface.properties().iter().cloned().collect()) + .unwrap_or_default(), + DeclarationKind::GenericInterfaceInstance => declaration + .as_any() + .downcast_ref::() + .map(|interface| interface.properties().iter().cloned().collect()) + .unwrap_or_default(), + _ => Vec::new(), } +} - if !class_declaration.base_full_name().is_empty() { - if let Some(base_decl) = MetadataReader::find_by_name(class_declaration.base_full_name()) { - let lock = base_decl.read(); - if let Some(base) = lock.as_any().downcast_ref::() { - return class_method_matches(base, name); - } - } +fn collect_declaration_events(declaration: &dyn Declaration) -> Vec { + if declaration.kind() == DeclarationKind::Class { + return declaration + .as_any() + .downcast_ref::() + .map(collect_class_events) + .unwrap_or_default(); } - false -} -fn class_property_matches(class_declaration: &ClassDeclaration, name: &str) -> bool { - if class_declaration.properties().iter().any(|p| p.name() == name) { return true; } + Vec::new() +} - if let Some(di) = class_declaration.default_interface() { - if di.properties().iter().any(|p| p.name() == name) { return true; } +fn resolve_return_declaration(return_sig: &str) -> Option>> { + if let Some(declaration) = MetadataReader::find_by_name(return_sig) { + return Some(declaration); } - for iface in class_declaration.implemented_interfaces() { - if iface.properties().iter().any(|p| p.name() == name) { return true; } - } + if return_sig.contains('`') { + let mut generic_name = return_sig.to_string(); + if let Some(backtick_index) = generic_name.rfind('<') { + generic_name.truncate(backtick_index); + } - if !class_declaration.base_full_name().is_empty() { - if let Some(base_decl) = MetadataReader::find_by_name(class_declaration.base_full_name()) { - let lock = base_decl.read(); - if let Some(base) = lock.as_any().downcast_ref::() { - return class_property_matches(base, name); - } + if let Some(declaration) = MetadataReader::find_by_name(generic_name.as_str()) { + let closed_declaration = { + let lock = declaration.read(); + if lock.kind() == DeclarationKind::GenericInterface { + lock.as_any() + .downcast_ref::() + .map(|interface| { + Arc::new(RwLock::new( + GenericInterfaceInstanceDeclaration::new_with_full_name( + interface.base().metadata(), + interface.base().token(), + return_sig.to_string(), + ), + )) as Arc> + }) + } else { + None + } + }; + + return closed_declaration.or(Some(declaration)); } } - false + + None } fn class_has_member_named(class_declaration: &ClassDeclaration, name: &str) -> bool { - class_method_matches(class_declaration, name) || class_property_matches(class_declaration, name) + collect_class_methods(class_declaration) + .iter() + .any(|method| { + let overload_name = method.overload_name(); + (!overload_name.is_empty() && overload_name == name) || method.name() == name + }) + || collect_class_properties(class_declaration) + .iter() + .any(|property| property.name() == name) + || collect_class_events(class_declaration) + .iter() + .any(|event| event.name() == name) } fn runtime_method_metadata_from_method(method: &MethodDeclaration) -> RuntimeMethodMetadata { @@ -487,31 +793,8 @@ fn throw_js_error(scope: &mut v8::PinScope<'_, '_>, message: &str) { } fn class_activation_factory(full_name: &str) -> windows::core::Result { - let clazz_name = HSTRING::from(full_name); - unsafe { RoGetActivationFactory::(&clazz_name) } -} - -fn resolve_class_factory_from_parent(dec: &DeclarationFFI) -> windows::core::Result { - if let Some(instance) = dec.instance.clone() { - return Ok(instance); - } - - let Some(parent) = dec.parent.as_ref() else { - return Err(Error::new( - HRESULT(0x80004005u32 as i32), - "Static WinRT member is missing its owning class declaration", - )); - }; - - let parent = parent.read(); - let Some(clazz) = parent.as_any().downcast_ref::() else { - return Err(Error::new( - HRESULT(0x80004005u32 as i32), - "Static WinRT member parent is not a class declaration", - )); - }; - - class_activation_factory(clazz.full_name()) + let class_name = HSTRING::from(full_name); + unsafe { RoGetActivationFactory::(&class_name) } } fn try_get_async_status( @@ -594,14 +877,6 @@ fn handle_host_wait_for_async( } else { 0 }; - - // Fast-path: already completed before we enter the loop. - match try_get_async_status(scope, op_value) { - Ok(0) => {} // still running — proceed to wait - Ok(_) => { retval.set(op_value); return; } - Err(msg) => { throw_js_error(scope, msg.as_str()); return; } - } - let deadline = if timeout_ms == 0 { None } else { @@ -622,23 +897,21 @@ fn handle_host_wait_for_async( match try_get_async_status(scope, op_value) { Ok(0) => { - // Still running: drain the message queue then block for 5 ms. - // Using 5 ms instead of 1 ms reduces CPU burn 5× while keeping - // perceived latency well below a UI frame budget. while unsafe { PeekMessageW(&mut message, None, 0, 0, PM_REMOVE) }.into() { unsafe { TranslateMessage(&message); DispatchMessageW(&message); } } - std::thread::sleep(Duration::from_millis(5)); + std::thread::sleep(Duration::from_millis(1)); + std::thread::yield_now(); } Ok(_) => { retval.set(op_value); return; } - Err(msg) => { - throw_js_error(scope, msg.as_str()); + Err(message) => { + throw_js_error(scope, message.as_str()); return; } } @@ -2300,7 +2573,9 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par object_tmpl.set_internal_field_count(1); - let declaration_ffi = Box::into_raw(Box::new(DeclarationFFI::new_with_instance(declaration.clone(), instance.clone()))); + let mut declaration_ffi = DeclarationFFI::new_with_instance(declaration.clone(), instance.clone()); + declaration_ffi.parent = parent; + let declaration_ffi = Box::into_raw(Box::new(declaration_ffi)); let ext = v8::External::new(scope, declaration_ffi as _); object_tmpl.set_named_property_handler( @@ -2324,16 +2599,24 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par let dec = unsafe { &*dec }; let lock = dec.read(); - let Some(clazz) = lock.as_any().downcast_ref::() else { + let methods = collect_declaration_methods(&*lock); + let properties = collect_declaration_properties(&*lock); + if methods.is_empty() && properties.is_empty() { return v8::Intercepted::kNo; - }; + } - for property in collect_class_properties(clazz) { + for property in properties { if property.name() != name { continue; } - let mut property_call = PropertyCall::new(&property, false, dec.instance.clone().unwrap(), false); + let mut property_call = PropertyCall::new_with_parent( + &property, + false, + dec.instance.clone().unwrap(), + false, + dec.parent.clone().or_else(|| Some(dec.inner.clone())), + ); let (ret, result) = property_call.call_with_values(scope, &[]); if ret.is_err() { @@ -2351,15 +2634,7 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par let return_sig = property_call.return_type().to_string(); if return_sig.contains('.') { let instance = unsafe { IUnknown::from_raw(result) }; - let declaration = if return_sig.contains('`') { - let mut generic_name = return_sig.clone(); - if let Some(backtick_index) = generic_name.rfind('<') { - generic_name.truncate(backtick_index); - } - MetadataReader::find_by_name(generic_name.as_str()) - } else { - MetadataReader::find_by_name(return_sig.as_str()) - }; + let declaration = resolve_return_declaration(return_sig.as_str()); if let Some(declaration) = declaration { let ret: Local = create_ns_ctor_instance_object(return_sig.as_str(), None, None, declaration, Some(instance), scope).into(); @@ -2376,7 +2651,7 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par return v8::Intercepted::kNo; } - for method in collect_class_methods(clazz) { + for method in methods { let mut method_name = method.overload_name(); if method_name.is_empty() { method_name = method.name(); @@ -2387,7 +2662,9 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par } let declaration = Arc::new(RwLock::new(method.clone())); - let declaration = Box::into_raw(Box::new(DeclarationFFI::new_with_instance(declaration, dec.instance.clone()))); + let mut declaration = DeclarationFFI::new_with_instance(declaration, dec.instance.clone()); + declaration.parent = Some(dec.inner.clone()); + let declaration = Box::into_raw(Box::new(declaration)); let ext = v8::External::new(scope, declaration as _); let builder = v8::Function::builder(|scope: &mut v8::PinScope<'_, '_>, @@ -2398,7 +2675,13 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par let dec = unsafe { &*dec }; let lock = dec.read(); let method = lock.as_any().downcast_ref::().unwrap(); - let mut method = MethodCall::new(method, method.is_sealed(), dec.instance.clone().unwrap(), false); + let mut method = MethodCall::new_with_parent( + method, + method.is_sealed(), + dec.instance.clone().unwrap(), + false, + dec.parent.clone(), + ); let (ret, result) = method.call(scope, &args); if ret.is_err() { @@ -2416,15 +2699,7 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par let return_sig = method.return_type().to_string(); if return_sig.contains('.') { let instance = unsafe { IUnknown::from_raw(result) }; - let declaration = if return_sig.contains('`') { - let mut generic_name = return_sig.clone(); - if let Some(backtick_index) = generic_name.rfind('<') { - generic_name.truncate(backtick_index); - } - MetadataReader::find_by_name(generic_name.as_str()) - } else { - MetadataReader::find_by_name(return_sig.as_str()) - }; + let declaration = resolve_return_declaration(return_sig.as_str()); if let Some(declaration) = declaration { let ret: Local = create_ns_ctor_instance_object(return_sig.as_str(), None, dec.parent.clone(), declaration, Some(instance), scope).into(); @@ -2462,11 +2737,14 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par let dec = unsafe { &*dec }; let lock = dec.read(); - let Some(clazz) = lock.as_any().downcast_ref::() else { + let properties = collect_declaration_properties(&*lock); + let events = collect_declaration_events(&*lock); + + if properties.is_empty() && events.is_empty() { return v8::Intercepted::kNo; - }; + } - for property in collect_class_properties(clazz) { + for property in properties { if property.name() != name { continue; } @@ -2475,7 +2753,13 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par return v8::Intercepted::kNo; } - let mut property_call = PropertyCall::new(&property, true, dec.instance.clone().unwrap(), false); + let mut property_call = PropertyCall::new_with_parent( + &property, + true, + dec.instance.clone().unwrap(), + false, + dec.parent.clone().or_else(|| Some(dec.inner.clone())), + ); let (ret, _) = property_call.call_with_values(scope, &[value]); if ret.is_err() { let message = v8::String::new(scope, &ret.message().to_string()).unwrap(); @@ -2485,6 +2769,137 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par return v8::Intercepted::kYes; } + for event in events { + if event.name() != name { + continue; + } + + // into a real native COM delegate so the WinRT event raise + // path has a valid IUnknown to dereference. Without this, + // assigning `obj.SomeEvent = fn` would forward a null/garbage + // pointer to add_*, and XAML would AV when the event fires. + let wrapped_value: Local = { + // Extract a callable, if the user supplied one. + let mut callback_fn: Option> = None; + let mut needs_wrap = false; + + if let Ok(func) = v8::Local::::try_from(value) { + callback_fn = Some(func); + needs_wrap = true; + } else if value.is_object() { + if let Some(obj) = value.to_object(scope) { + // If the object already carries a native COM + // instance (constructed via `new DelegateType(..)`), + // do nothing — pass through as-is. + let mut has_native_instance = false; + if obj.internal_field_count() > 0 { + if let Some(field) = obj.get_internal_field(scope, 0) { + let ext = unsafe { field.cast::() }; + let ptr = ext.value() as *mut DeclarationFFI; + if !ptr.is_null() { + let dec_ref = unsafe { &*ptr }; + if dec_ref.instance.is_some() { + has_native_instance = true; + } + } + } + } + + if !has_native_instance { + if let Some(invoke_key) = v8::String::new(scope, "Invoke") { + if let Some(invoke_val) = obj.get(scope, invoke_key.into()) { + if let Ok(func) = + v8::Local::::try_from(invoke_val) + { + callback_fn = Some(func); + needs_wrap = true; + } + } + } + } + } + } + + if needs_wrap { + if let Some(callback) = callback_fn { + let delegate_full_name = event.delegate_type_full_name(); + let delegate_iid = event.delegate_type_id(); + + if let (Some(delegate_full_name), Some(delegate_iid)) = + (delegate_full_name.as_ref(), delegate_iid) + { + // For generic delegate instances (e.g. + // `TypedEventHandler`) the + // closed name is not in the metadata index; + // look up the open generic by truncating + // at the first `<`. + let lookup_name: String = if let Some(idx) = + delegate_full_name.find('<') + { + delegate_full_name[..idx].to_string() + } else { + delegate_full_name.clone() + }; + + if let Some(delegate_arc) = + MetadataReader::find_by_name(lookup_name.as_str()) + { + let cb_global = v8::Global::new(scope, callback); + let handler_id = + register_js_delegate_handler(cb_global); + let base_delegate_iid = + get_generic_delegate_base_iid(delegate_full_name); + let native = create_native_delegate_instance( + delegate_iid, + base_delegate_iid, + handler_id, + ); + let delegate_name = { + let dlock = delegate_arc.read(); + dlock.name().to_string() + }; + create_ns_ctor_instance_object( + delegate_name.as_str(), + None, + None, + delegate_arc.clone(), + Some(native), + scope, + ) + } else { + return v8::Intercepted::kYes; + } + } else { + return v8::Intercepted::kYes; + } + } else { + value + } + } else { + value + } + }; + + let add_method = event.add_method(); + + let mut method = MethodCall::new( + add_method, + add_method.is_sealed(), + dec.instance.clone().unwrap(), + false, + ); + + let (ret, _) = method.call_with_values(scope, &[wrapped_value]); + + if ret.is_err() { + let message = v8::String::new(scope, &ret.message().to_string()).unwrap(); + let error = v8::Exception::error(scope, message); + scope.throw_exception(error); + } + + return v8::Intercepted::kYes; + } + v8::Intercepted::kNo }) .data(ext.into()) @@ -2509,16 +2924,7 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par let clazz = lock.as_any().downcast_ref::().unwrap(); let class_methods = collect_class_methods(clazz); let class_properties = collect_class_properties(clazz); - - if matches!(name, "StackPanel" | "ListView" | "ComboBox" | "ScrollViewer" | "Border") { - eprintln!( - "[runtime] class {} resolved {} methods and {} properties", - name, - class_methods.len(), - class_properties.len() - ); - } - + let class_events = collect_class_events(clazz); let to_string_func = FunctionTemplate::builder(|scope: &mut v8::PinScope<'_, '_>, args: v8::FunctionCallbackArguments, @@ -2606,15 +3012,8 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par // .Status, .ErrorCode, .GetResults, .Completed, .Cancel, // .Close — the dev wraps it in a JS Promise if they want // (matching the iOS & Android NativeScript runtimes). - let declaration = if return_sig.contains('`') { - let mut name = return_sig.to_string(); - if let Some(backtick_index) = name.rfind('<') { - name.truncate(backtick_index); - } - MetadataReader::find_by_name(name.as_str()).unwrap() - } else { - dec.inner.clone() - }; + let declaration = resolve_return_declaration(return_sig.as_str()) + .unwrap_or_else(|| dec.inner.clone()); let ret: Local = create_ns_ctor_instance_object(return_sig.as_str(), None, dec.parent.clone(), declaration, Some(instance), scope).into(); retval.set(ret.into()); @@ -2689,15 +3088,7 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par let return_sig = method.return_type().to_string(); if return_sig.contains('.') { let instance = unsafe { IUnknown::from_raw(result) }; - let declaration = if return_sig.contains('`') { - let mut name = return_sig.clone(); - if let Some(backtick_index) = name.rfind('<') { - name.truncate(backtick_index); - } - MetadataReader::find_by_name(name.as_str()) - } else { - MetadataReader::find_by_name(return_sig.as_str()) - }; + let declaration = resolve_return_declaration(return_sig.as_str()); if let Some(declaration) = declaration { let ret: Local = create_ns_ctor_instance_object(return_sig.as_str(), None, None, declaration, Some(instance), scope).into(); @@ -2761,6 +3152,62 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par object_tmpl.set_accessor_property(name.into(), Some(getter), setter, v8::PropertyAttribute::NONE); } } + + for event in class_events.iter() { + let name = v8::String::new(scope, event.name()).unwrap(); + let is_static = event.is_static(); + + let declaration = DeclarationFFI::new_with_instance( + Arc::new(RwLock::new(event.add_method().clone())), + if is_static { factory.clone() } else { instance.clone() }, + ); + + let setter_declaration = Box::into_raw(Box::new(declaration)); + let setter_declaration_ext = v8::External::new(scope, setter_declaration as _); + + let setter = FunctionTemplate::builder( + |scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + _retval: v8::ReturnValue| { + let dec = unsafe { args.data().cast::() }; + let dec = dec.value() as *mut DeclarationFFI; + let dec = unsafe { &*dec }; + let lock = dec.read(); + let add_method = lock.as_any().downcast_ref::().unwrap(); + + let mut method = MethodCall::new( + add_method, + add_method.is_sealed(), + dec.instance.clone().unwrap(), + false, + ); + let (ret, _) = method.call(scope, &args); + if ret.is_err() { + let msg = v8::String::new(scope, &ret.message().to_string()).unwrap(); + let err = v8::Exception::error(scope, msg); + scope.throw_exception(err); + } + }, + ) + .data(setter_declaration_ext.into()) + .build(scope); + + if is_static { + tmpl.set_accessor_property( + name.into(), + None, + Some(setter), + v8::PropertyAttribute::DONT_DELETE, + ); + } else { + object_tmpl.set_accessor_property( + name.into(), + None, + Some(setter), + v8::PropertyAttribute::NONE, + ); + } + } } DeclarationKind::Interface | DeclarationKind::GenericInterface @@ -3411,15 +3858,8 @@ fn create_ns_ctor_instance_object<'a>(name: &str, factory: Option, par let instance = unsafe { IUnknown::from_raw(*(result as *mut *mut c_void)) }; if return_sig.contains('`') { - let mut name = return_sig.to_string(); - - if let Some(backtick_index) = name.rfind('<') { - name.truncate(backtick_index); - } - - // use the generic name - let declaration = MetadataReader::find_by_name(name.as_str()).unwrap(); - + let declaration = resolve_return_declaration(return_sig) + .unwrap_or_else(|| dec.inner.clone()); let ret: Local = create_ns_ctor_instance_object(return_sig, None, dec.parent.clone(), declaration, Some(instance), scope).into(); retval.set(ret.into()); @@ -3497,7 +3937,6 @@ fn create_ns_ctor_object<'a>(name: &str, parent: Option { let clazz = lock.as_any().downcast_ref::().unwrap(); - let clazz_factory = match class_activation_factory(clazz.full_name()) { Ok(factory) => factory, Err(error) => { @@ -3513,21 +3952,87 @@ fn create_ns_ctor_object<'a>(name: &str, parent: Option { + let result: IUnknown = result.into(); + + // For console apps: attach to the console window so that + // UI dialogs (e.g. MessageDialog) know which window to use. + if let Ok(init) = result.cast::() { + let hwnd = unsafe { GetConsoleWindow() }; + if !hwnd.is_invalid() { + let _ = unsafe { init.Initialize(hwnd) }; + } + } + + let instance = create_ns_ctor_instance_object( + clazz.name(), + Some(clazz_factory.clone()), + None, + dec.inner.clone(), + Some(result), + scope, + ); + retval.set(instance); + return; + } + Err(_) => { + // Zero-arg activation should be handled via WinRT activation factory. + // Calling metadata constructor stubs through the generic FFI path here can + // crash the process for some XAML controls (e.g. NavigationView). + if let Ok(factory) = clazz_factory.cast::() { + if let Ok(instance_obj) = factory.ActivateInstance() { + let result: IUnknown = instance_obj.into(); + + if let Ok(init) = result.cast::() { + let hwnd = unsafe { GetConsoleWindow() }; + if !hwnd.is_invalid() { + let _ = unsafe { init.Initialize(hwnd) }; + } + } + + let instance = create_ns_ctor_instance_object( + clazz.name(), + Some(clazz_factory.clone()), + None, + dec.inner.clone(), + Some(result), + scope, + ); + retval.set(instance); + return; + } + } + + // Activation failed; continue into constructor fallback path. + } + } + } + let mut method = MethodCall::new( - ctor, is_sealed, clazz_factory.clone(), true, + ctor, ctor.is_sealed(), clazz_factory.clone(), true, ); let (ret, result) = method.call(scope, &args); if ret.is_ok() { + if result.is_null() { + let message = v8::String::new(scope, "Constructor returned null instance pointer").unwrap(); + let error = v8::Exception::error(scope, message.into()); + scope.throw_exception(error); + return; + } + let result = unsafe { IUnknown::from_raw(result) }; let vtable = result.vtable(); @@ -3541,7 +4046,6 @@ fn create_ns_ctor_object<'a>(name: &str, parent: Option(name: &str, parent: Option { + if length >= 1 { + let arg = args.get(0); + let callback = if let Ok(func) = v8::Local::::try_from(arg) { + Some(func) + } else if arg.is_object() { + arg.to_object(scope) + .and_then(|obj| v8::String::new(scope, "Invoke").and_then(|key| obj.get(scope, key.into()))) + .and_then(|value| v8::Local::::try_from(value).ok()) + } else { + None + }; + + if let Some(callback) = callback { + let callback = v8::Global::new(scope, callback); + let handler_id = register_js_delegate_handler(callback); + if let Some(delegate_iid) = get_delegate_iid(lock.deref()) { + let base_delegate_iid = + get_generic_delegate_base_iid(lock.full_name()); + let instance = create_native_delegate_instance( + delegate_iid, + base_delegate_iid, + handler_id, + ); + let instance = create_ns_ctor_instance_object( + lock.name(), + None, + dec.parent.clone(), + dec.inner.clone(), + Some(instance), + scope, + ); + retval.set(instance); + return; + } + } + } + } DeclarationKind::Struct => {} _ => {} } @@ -3634,6 +4178,11 @@ fn create_ns_ctor_object<'a>(name: &str, parent: Option().unwrap(); + let Ok(clazz_factory) = class_activation_factory(clazz.full_name()) else { + let func = tmpl.get_function(scope).unwrap(); + return func.into(); + }; + for method in clazz.methods().iter() { let name = v8::String::new(scope, method.name()); let is_static = method.is_static(); @@ -3650,7 +4199,7 @@ fn create_ns_ctor_object<'a>(name: &str, parent: Option(name: &str, parent: Option factory, - Err(error) => { - throw_js_error( - scope, - format!( - "Failed to resolve WinRT static method factory for {}: {}", - method.name(), - error.message() - ) - .as_str(), - ); - return; - } - }; - let mut method = MethodCall::new( - method, method.is_sealed(), factory, false, + method, method.is_sealed(), dec.instance.clone().unwrap(), false, ); let (ret, result) = method.call(scope, &args); @@ -3773,6 +4306,120 @@ fn create_ns_ctor_object<'a>(name: &str, parent: Option, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue| { + let dec = unsafe { args.data().cast::() }; + let dec = dec.value() as *mut DeclarationFFI; + let dec = unsafe { &*dec }; + let lock = dec.read(); + let prop = lock.as_any().downcast_ref::().unwrap(); + + let mut method = PropertyCall::new(prop, false, dec.instance.clone().unwrap(), false); + let (ret, result) = method.call(scope, &args); + + if ret.is_err() { + let msg = v8::String::new(scope, &ret.message().to_string()).unwrap(); + let err = v8::Exception::error(scope, msg); + scope.throw_exception(err); + return; + } + + if method.is_void() { + retval.set_undefined(); + return; + } + + let return_sig = method.return_type().to_string(); + if return_sig.contains('.') { + let instance = unsafe { IUnknown::from_raw(result) }; + let declaration = if return_sig.contains('`') { + let mut name = return_sig.clone(); + if let Some(backtick_index) = name.rfind('<') { + name.truncate(backtick_index); + } + MetadataReader::find_by_name(name.as_str()) + } else { + MetadataReader::find_by_name(return_sig.as_str()) + }; + + if let Some(declaration) = declaration { + let ret: Local = create_ns_ctor_instance_object( + return_sig.as_str(), + None, + None, + declaration, + Some(instance), + scope, + ) + .into(); + retval.set(ret.into()); + return; + } + } + + if let Ok(return_type) = NativeType::try_from(return_sig.as_str()) { + unsafe { set_ret_val(result, scope, retval, return_type); } + } + }, + ) + .data(getter_declaration_ext.into()) + .build(scope); + + let mut setter: Option> = None; + if property.setter().is_some() { + let setter_declaration = Box::into_raw(Box::new(declaration)); + let setter_declaration_ext = v8::External::new(scope, setter_declaration as _); + + setter = Some( + v8::FunctionTemplate::builder( + |scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + _retval: v8::ReturnValue| { + let dec = unsafe { args.data().cast::() }; + let dec = dec.value() as *mut DeclarationFFI; + let dec = unsafe { &*dec }; + let lock = dec.read(); + let prop = lock.as_any().downcast_ref::().unwrap(); + + let mut method = PropertyCall::new(prop, true, dec.instance.clone().unwrap(), false); + let (ret, _) = method.call(scope, &args); + if ret.is_err() { + let msg = v8::String::new(scope, &ret.message().to_string()).unwrap(); + let err = v8::Exception::error(scope, msg); + scope.throw_exception(err); + } + }, + ) + .data(setter_declaration_ext.into()) + .build(scope), + ); + } + + tmpl.set_accessor_property( + name.into(), + Some(getter), + setter, + v8::PropertyAttribute::DONT_DELETE, + ); + } } let func = tmpl.get_function(scope).unwrap(); @@ -4703,6 +5350,14 @@ impl Runtime { } pub fn run_script(&mut self, script: &str) { + ACTIVE_RUNTIME_PTR.store(self as *mut Runtime, Ordering::SeqCst); + + let thread_lock = ACTIVE_RUNTIME_THREAD.get_or_init(|| Mutex::new(None)); + let mut thread_guard = thread_lock.lock(); + if thread_guard.is_none() { + *thread_guard = Some(std::thread::current().id()); + } + v8::scope!(scope, &mut self.isolate); let context = v8::Local::new(scope, &self.global_context); let scope = &mut v8::ContextScope::new(scope, context); @@ -4719,11 +5374,10 @@ impl Runtime { }; let Some(code) = v8::String::new(tc, source_to_run.as_str()) else { return }; - if let Some(compiled) = v8::Script::compile(tc, code, None) { - compiled.run(tc); + if let Some(script) = v8::Script::compile(tc, code, None) { + script.run(tc); } - // Log and rethrow any JS exception back into V8 (mirrors NativeScript behaviour). if tc.has_caught() { if let Some(msg) = tc.message() { let text = msg.get(tc).to_rust_string_lossy(tc); @@ -4738,8 +5392,13 @@ impl Runtime { let file = frame.get_script_name(tc) .map(|s| s.to_rust_string_lossy(tc)) .unwrap_or_else(|| "".to_string()); - eprintln!(" at {} ({}:{}:{})", fn_name, file, - frame.get_line_number(), frame.get_column()); + eprintln!( + " at {} ({}:{}:{})", + fn_name, + file, + frame.get_line_number(), + frame.get_column() + ); } } } @@ -4761,4 +5420,44 @@ impl Drop for Runtime { unsafe { RoUninitialize() }; } } +} + +fn extend_class_events(class_declaration: &ClassDeclaration, events: &mut Vec, seen: &mut HashSet) { + for event in class_declaration.events() { + if seen.insert(event.name().to_string()) { + events.push(event.clone()); + } + } + + if let Some(default_interface) = class_declaration.default_interface() { + for event in default_interface.events() { + if seen.insert(event.name().to_string()) { + events.push(event.clone()); + } + } + } + + for interface in class_declaration.implemented_interfaces() { + for event in interface.events() { + if seen.insert(event.name().to_string()) { + events.push(event.clone()); + } + } + } + + if !class_declaration.base_full_name().is_empty() { + if let Some(base_declaration) = MetadataReader::find_by_name(class_declaration.base_full_name()) { + let base_lock = base_declaration.read(); + if let Some(base_class) = base_lock.as_any().downcast_ref::() { + extend_class_events(base_class, events, seen); + } + } + } +} + +fn collect_class_events(class_declaration: &ClassDeclaration) -> Vec { + let mut events = Vec::new(); + let mut seen = HashSet::new(); + extend_class_events(class_declaration, &mut events, &mut seen); + events } \ No newline at end of file diff --git a/runtime/src/method_call.rs b/runtime/src/method_call.rs index 8c43b31..b51537d 100644 --- a/runtime/src/method_call.rs +++ b/runtime/src/method_call.rs @@ -2,10 +2,12 @@ use std::ffi::c_void; use std::sync::Arc; use libffi::middle::*; use parking_lot::RwLock; +use metadata::declarations::declaration::Declaration; use windows::core::{GUID, HRESULT, Interface, IUnknown}; use windows::Win32::System::WinRT::IActivationFactory; use metadata::declarations::base_class_declaration::BaseClassDeclarationImpl; use metadata::declarations::declaration::DeclarationKind; +use metadata::declarations::interface_declaration::generic_interface_declaration::GenericInterfaceDeclaration; use metadata::declarations::interface_declaration::generic_interface_instance_declaration::GenericInterfaceInstanceDeclaration; use metadata::declarations::interface_declaration::InterfaceDeclaration; use metadata::declarations::method_declaration::MethodDeclaration; @@ -13,8 +15,11 @@ use metadata::declarations::parameter_declaration::ParameterDeclaration; use metadata::declaring_interface_for_method::Metadata; use metadata::signature::Signature; use crate::error::AnyError; -use crate::helpers::ffi_native_type_from_signature; -use crate::value::{ffi_parse_bool_arg, ffi_parse_buffer_arg, ffi_parse_f32_arg, ffi_parse_f64_arg, ffi_parse_function_arg, ffi_parse_i16_arg, ffi_parse_i32_arg, ffi_parse_i64_arg, ffi_parse_i8_arg, ffi_parse_isize_arg, ffi_parse_pointer_arg, ffi_parse_string_arg, ffi_parse_struct_arg, ffi_parse_u16_arg, ffi_parse_u32_arg, ffi_parse_u64_arg, ffi_parse_u8_arg, ffi_parse_usize_arg, NativeType, NativeValue}; +use crate::helpers::{ + call_failure, ffi_native_type_from_signature, inherited_interface_method_count, + normalize_parameter_signature, parse_native_type_from_signature, +}; +use crate::value::{ffi_parse_bool_arg, ffi_parse_buffer_arg, ffi_parse_f32_arg, ffi_parse_f64_arg, ffi_parse_function_arg, ffi_parse_i16_arg, ffi_parse_i32_arg, ffi_parse_i64_arg, ffi_parse_i8_arg, ffi_parse_isize_arg, ffi_parse_pointer_arg_with_signature, ffi_parse_string_arg, ffi_parse_struct_arg, ffi_parse_u16_arg, ffi_parse_u32_arg, ffi_parse_u64_arg, ffi_parse_u8_arg, ffi_parse_usize_arg, NativeType, NativeValue}; pub struct MethodCall { index: usize, @@ -32,6 +37,7 @@ pub struct MethodCall { parameters: Vec, return_type: String, pub(crate) declaration: Option>>, + is_valid: bool, /// Scratch buffer used when a WinRT method returns a value type (e.g. GUID, Rect) /// that is larger than, or cannot be safely aliased through, a single pointer slot. return_value_buf: [u8; 128], @@ -39,12 +45,6 @@ pub struct MethodCall { argument_buf: Vec, } -#[inline] -fn call_failure() -> HRESULT { - // E_FAIL - HRESULT(0x8000_4005u32 as i32) -} - impl MethodCall { pub fn is_void(&self) -> bool { self.is_void @@ -60,6 +60,17 @@ impl MethodCall { interface: IUnknown, is_initializer: bool ) -> Self { + Self::new_with_parent(method, is_sealed, interface, is_initializer, None) + } + + pub fn new_with_parent( + method: &MethodDeclaration, + is_sealed: bool, + interface: IUnknown, + is_initializer: bool, + parent_declaration: Option>>, + ) -> Self { + let mut is_valid = true; let signature = method.return_type(); @@ -71,6 +82,85 @@ impl MethodCall { let mut index = 0 as usize; let mut declaration: Option>> = None; + let mut allow_qi_fallback = false; + + if !is_initializer { + if let Some(parent_declaration) = parent_declaration.as_ref() { + let parent = parent_declaration.read(); + match parent.kind() { + DeclarationKind::Interface => { + if let Some(parent_interface) = parent.as_any().downcast_ref::() { + let parent_index = Metadata::find_method_index( + method.metadata().unwrap(), + parent_interface.base().token(), + method.token(), + ); + let inherited_count = inherited_interface_method_count( + parent_interface.implemented_interfaces().as_slice(), + ); + declaration = None; + return Self::new_bound_to_iid( + method, + is_sealed, + interface, + is_initializer, + parent_interface.id(), + parent_index.saturating_add(6 + inherited_count), + declaration, + ); + } + } + DeclarationKind::GenericInterface => { + if let Some(parent_interface) = parent.as_any().downcast_ref::() { + let parent_index = Metadata::find_method_index( + method.metadata().unwrap(), + parent_interface.base().token(), + method.token(), + ); + let inherited_count = inherited_interface_method_count( + parent_interface.implemented_interfaces().as_slice(), + ); + declaration = None; + return Self::new_bound_to_iid( + method, + is_sealed, + interface, + is_initializer, + parent_interface.id(), + parent_index.saturating_add(6 + inherited_count), + declaration, + ); + } + } + DeclarationKind::GenericInterfaceInstance => { + if let Some(parent_interface) = parent + .as_any() + .downcast_ref::() + { + let parent_index = Metadata::find_method_index( + method.metadata().unwrap(), + parent_interface.base().token(), + method.token(), + ); + let inherited_count = inherited_interface_method_count( + parent_interface.implemented_interfaces().as_slice(), + ); + declaration = None; + return Self::new_bound_to_iid( + method, + is_sealed, + interface, + is_initializer, + parent_interface.id(), + parent_index.saturating_add(6 + inherited_count), + declaration, + ); + } + } + _ => {} + } + } + } let iid = match Metadata::find_declaring_interface_for_method(method, &mut index) { None => { @@ -85,6 +175,15 @@ impl MethodCall { let kind = ii_lock.base().kind(); match kind { + DeclarationKind::GenericInterface => { + let ii = ii_lock + .as_declaration() + .as_any() + .downcast_ref::(); + let ii = ii.unwrap(); + iid = ii.id(); + allow_qi_fallback = true; + } DeclarationKind::GenericInterfaceInstance => { let ii = ii_lock .as_declaration() @@ -92,6 +191,7 @@ impl MethodCall { .downcast_ref::(); let ii = ii.unwrap(); iid = ii.id(); + allow_qi_fallback = true; } _ => { let ii = ii_lock @@ -124,10 +224,16 @@ impl MethodCall { ) }; - assert!(result.is_ok()); - assert!(!interface_ptr.is_null()); + if result.is_err() || interface_ptr.is_null() { + if !(allow_qi_fallback && !is_initializer) { + is_valid = false; + } + interface_ptr = interface.as_raw() as *mut c_void; + } - let is_composition = !is_sealed; + // For constructor calls, composition outer/inner parameters are required only + // when metadata resolved a specific initializer factory interface. + let is_composition = is_initializer && declaration.is_some(); let is_void = method.is_void(); @@ -156,11 +262,17 @@ impl MethodCall { let metadata = parameter.metadata().unwrap(); let signature = Signature::to_string(metadata, &type_); + let signature = normalize_parameter_signature(signature.as_str()).to_string(); - let parse_native_type = NativeType::try_from(signature.as_str()); - assert!(parse_native_type.is_ok()); - parse_parameter_types.push(parse_native_type.unwrap()); - parameter_types.push(ffi_native_type_from_signature(signature.as_str())); + let parse_native_type = parse_native_type_from_signature(signature.as_str()); + parse_parameter_types.push(parse_native_type.clone()); + + let abi_type = if parse_native_type != NativeType::Pointer { + parse_native_type.clone() + } else { + ffi_native_type_from_signature(signature.as_str()) + }; + parameter_types.push(abi_type); } if is_initializer { @@ -182,16 +294,159 @@ impl MethodCall { .map(libffi::middle::Type::try_from) .collect::, AnyError>>(); - assert!(params.is_ok()); + let params = if let Ok(params) = params { + params + } else { + is_valid = false; + vec![Type::pointer()] + }; let cif = Cif::new( - params.unwrap(), + params, Type::i32(), ); let interface = unsafe { IUnknown::from_raw(interface_ptr as *mut c_void) }; let vtable: *mut *mut c_void = unsafe { std::mem::transmute(interface.vtable()) }; - let func = unsafe { *vtable.offset(index as isize) }; + let func = if is_valid { + unsafe { *vtable.offset(index as isize) } + } else { + std::ptr::null_mut() + }; + + if func.is_null() { + is_valid = false; + } + + Self { + cif, + func, + index, + number_of_parameters, + number_of_abi_parameters, + is_initializer, + is_sealed, + is_void: method.is_void(), + iid, + interface, + parameter_types, + parse_parameter_types, + parameters: method.parameters().to_vec(), + declaration, + return_type, + is_valid, + return_value_buf: [0u8; 128], + argument_buf: Vec::with_capacity(number_of_abi_parameters), + } + } + + fn new_bound_to_iid( + method: &MethodDeclaration, + is_sealed: bool, + interface: IUnknown, + is_initializer: bool, + iid: GUID, + index: usize, + declaration: Option>>, + ) -> Self { + let mut is_valid = true; + + let signature = method.return_type(); + let return_type = Signature::to_string(method.metadata().unwrap(), &signature); + let number_of_parameters = method.number_of_parameters(); + + let mut interface_ptr: *mut c_void = std::ptr::null_mut(); + let vtable = interface.vtable(); + + let result = unsafe { + ((*vtable).QueryInterface)( + interface.as_raw(), + &iid, + &mut interface_ptr as *mut _ as *mut *mut c_void, + ) + }; + + if result.is_err() || interface_ptr.is_null() { + if is_initializer { + is_valid = false; + } + interface_ptr = interface.as_raw() as *mut c_void; + } + + let is_composition = is_initializer && declaration.is_some(); + let is_void = method.is_void(); + let other_params: usize = if is_initializer { + if is_sealed { + 2 + } else { + 4 + } + } else if is_void { + 1 + } else { + 2 + }; + + let number_of_abi_parameters = number_of_parameters + other_params; + + let mut parameter_types: Vec = Vec::with_capacity(number_of_abi_parameters); + let mut parse_parameter_types: Vec = Vec::with_capacity(number_of_parameters); + parameter_types.push(NativeType::Pointer); + + for parameter in method.parameters().iter() { + let type_ = parameter.type_(); + let metadata = parameter.metadata().unwrap(); + + let signature = Signature::to_string(metadata, &type_); + let signature = normalize_parameter_signature(signature.as_str()).to_string(); + + let parse_native_type = parse_native_type_from_signature(signature.as_str()); + parse_parameter_types.push(parse_native_type.clone()); + + let abi_type = if parse_native_type != NativeType::Pointer { + parse_native_type.clone() + } else { + ffi_native_type_from_signature(signature.as_str()) + }; + parameter_types.push(abi_type); + } + + if is_initializer { + if is_composition { + parameter_types.push(NativeType::Pointer); + parameter_types.push(NativeType::Pointer); + } + parameter_types.push(NativeType::Pointer); + } else if !is_void { + parameter_types.push(NativeType::Pointer); + } + + let params = parameter_types + .iter() + .cloned() + .map(libffi::middle::Type::try_from) + .collect::, AnyError>>(); + + let params = if let Ok(params) = params { + params + } else { + is_valid = false; + vec![Type::pointer()] + }; + + let cif = Cif::new(params, Type::i32()); + + let interface = unsafe { IUnknown::from_raw(interface_ptr as *mut c_void) }; + let vtable: *mut *mut c_void = unsafe { std::mem::transmute(interface.vtable()) }; + let func = if is_valid { + unsafe { *vtable.offset(index as isize) } + } else { + std::ptr::null_mut() + }; + + if func.is_null() { + is_valid = false; + } Self { cif, @@ -209,6 +464,7 @@ impl MethodCall { parameters: method.parameters().to_vec(), declaration, return_type, + is_valid, return_value_buf: [0u8; 128], argument_buf: Vec::with_capacity(number_of_abi_parameters), } @@ -220,7 +476,12 @@ impl MethodCall { fn is_value_type_return(&self) -> bool { matches!( self.return_type.as_str(), - "Guid" | "Rect" | "Matrix3x2" | "Matrix4x4" + "Guid" + | "Rect" + | "Matrix3x2" + | "Matrix4x4" + | "EventRegistrationToken" + | "Windows.Foundation.EventRegistrationToken" ) } @@ -229,24 +490,32 @@ impl MethodCall { scope: &mut v8::PinScope<'_, '_>, args: &v8::FunctionCallbackArguments, ) -> (HRESULT, *mut c_void) { + let mut values = Vec::with_capacity(self.parse_parameter_types.len()); + for index in 0..self.parse_parameter_types.len() { + values.push(args.get(index as i32)); + } - // Snapshot fields before the mutable borrow of argument_buf begins. - let number_of_abi_parameters = self.number_of_abi_parameters; - let is_initializer = self.is_initializer; - let is_sealed = self.is_sealed; - let is_void = self.is_void; - let is_value_type = matches!( - self.return_type.as_str(), - "Guid" | "Rect" | "Matrix3x2" | "Matrix4x4" - ); + self.call_with_values(scope, &values) + } + + pub fn call_with_values( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + values: &[v8::Local], + ) -> (HRESULT, *mut c_void) { + if !self.is_valid || self.func.is_null() { + return (call_failure(), std::ptr::null_mut()); + } - // Reuse the pre-allocated buffer — avoids a heap allocation on every call. self.argument_buf.clear(); + let mut pointer_keepalive: Vec = Vec::new(); unsafe { self.argument_buf.push(NativeValue { pointer: std::mem::transmute_copy(&self.interface) }) }; for (i, native_type) in self.parse_parameter_types.iter().enumerate() { - let value = args.get(i as i32); + let Some(value) = values.get(i).copied() else { + return (call_failure(), std::ptr::null_mut()); + }; let value = match *native_type { NativeType::Void => { @@ -292,7 +561,31 @@ impl MethodCall { ffi_parse_f64_arg(value) } NativeType::Pointer => { - ffi_parse_pointer_arg(scope, value) + let expected_signature = self + .parameters + .get(i) + .and_then(|parameter| { + parameter + .metadata() + .map(|metadata| { + let signature = Signature::to_string(metadata, ¶meter.type_()); + normalize_parameter_signature(signature.as_str()).to_string() + }) + }); + + match ffi_parse_pointer_arg_with_signature( + scope, + value, + expected_signature.as_deref(), + ) { + Ok((native_value, keepalive)) => { + if let Some(keepalive) = keepalive { + pointer_keepalive.push(keepalive); + } + Ok(native_value) + } + Err(error) => Err(error), + } } NativeType::Buffer => { ffi_parse_buffer_arg(scope, value) @@ -317,36 +610,44 @@ impl MethodCall { } let mut result: *mut c_void = std::ptr::null_mut(); - let mut composition_outer: *mut c_void = std::ptr::null_mut(); let mut composition_inner: *mut c_void = std::ptr::null_mut(); - if is_initializer { - if !is_sealed { - // WinRT composition constructors receive separate outer/inner pointers. - unsafe { - self.argument_buf.push(NativeValue { - pointer: &mut composition_outer as *mut _ as *mut c_void, - }) - }; + + if self.is_initializer { + + if !self.is_sealed { + // Composable ctor CreateInstance takes: + // (outer: IInspectable*, inner: IInspectable**) + // For non-aggregated construction from JS, pass null outer. + self.argument_buf.push(NativeValue { + pointer: std::ptr::null_mut(), + }); unsafe { self.argument_buf.push(NativeValue { pointer: &mut composition_inner as *mut _ as *mut c_void, }) }; } + unsafe { self.argument_buf.push(NativeValue { pointer: &mut result as *mut _ as *mut c_void }) }; - } else if !is_void { - if is_value_type { - // Value structs (GUID=16B, Rect=16B, …) are written directly into the - // out-param location — not through a pointer-to-pointer. Use the - // pre-allocated scratch buffer so we don't overflow a pointer-sized slot. - let buf_ptr = self.return_value_buf.as_mut_ptr() as *mut c_void; - self.argument_buf.push(NativeValue { pointer: buf_ptr }); - } else { - self.argument_buf.push(NativeValue { pointer: &mut result as *mut _ as *mut c_void }); + } else { + if !self.is_void { + if self.is_value_type_return() { + // Value structs (GUID=16B, Rect=16B, …) are written directly into the + // out-param location — not through a pointer-to-pointer. Use the + // pre-allocated scratch buffer so we don't overflow a pointer-sized slot. + let buf_ptr = self.return_value_buf.as_mut_ptr() as *mut c_void; + self.argument_buf.push(NativeValue { pointer: buf_ptr }); + } else { + self.argument_buf.push(NativeValue { pointer: &mut result as *mut _ as *mut c_void }); + } } } + //let mut func = std::ptr::null_mut(); + + // get_method(&self.interface, self.index, addr_of_mut!(func)); + let mut call_args: Vec = Vec::with_capacity(self.argument_buf.len()); for (i, v) in self.argument_buf.iter().enumerate() { // SAFETY: Creating a `Arg` from a `NativeValue` is safe when the parallel type vector matches. @@ -356,6 +657,7 @@ impl MethodCall { call_args.push(unsafe { v.as_arg(native_type) }); } + let ret = unsafe { self.cif.call( CodePtr::from_ptr(self.func), @@ -363,19 +665,17 @@ impl MethodCall { ) }; - if is_initializer && !is_sealed && result.is_null() { + if self.is_initializer && !self.is_sealed && result.is_null() { if !composition_inner.is_null() { result = composition_inner; - } else if !composition_outer.is_null() { - result = composition_outer; } } - if !is_initializer && !is_void && is_value_type { + if !self.is_initializer && !self.is_void && self.is_value_type_return() { // Point result at the scratch buffer so the caller can read the bytes. result = self.return_value_buf.as_mut_ptr() as *mut c_void; } (HRESULT(ret), result) } -} \ No newline at end of file +} diff --git a/runtime/src/property_call.rs b/runtime/src/property_call.rs index 2309cd6..6466a3b 100644 --- a/runtime/src/property_call.rs +++ b/runtime/src/property_call.rs @@ -5,7 +5,8 @@ use parking_lot::RwLock; use windows::core::{GUID, HRESULT, Interface, IUnknown}; use windows::Win32::System::WinRT::IActivationFactory; use metadata::declarations::base_class_declaration::BaseClassDeclarationImpl; -use metadata::declarations::declaration::DeclarationKind; +use metadata::declarations::declaration::{Declaration, DeclarationKind}; +use metadata::declarations::interface_declaration::generic_interface_declaration::GenericInterfaceDeclaration; use metadata::declarations::interface_declaration::generic_interface_instance_declaration::GenericInterfaceInstanceDeclaration; use metadata::declarations::interface_declaration::InterfaceDeclaration; use metadata::declarations::parameter_declaration::ParameterDeclaration; @@ -13,8 +14,11 @@ use metadata::declarations::property_declaration::PropertyDeclaration; use metadata::declaring_interface_for_method::Metadata; use metadata::signature::Signature; use crate::error::AnyError; -use crate::helpers::ffi_native_type_from_signature; -use crate::value::{ffi_parse_bool_arg, ffi_parse_buffer_arg, ffi_parse_f32_arg, ffi_parse_f64_arg, ffi_parse_function_arg, ffi_parse_i16_arg, ffi_parse_i32_arg, ffi_parse_i64_arg, ffi_parse_i8_arg, ffi_parse_isize_arg, ffi_parse_pointer_arg, ffi_parse_string_arg, ffi_parse_struct_arg, ffi_parse_u16_arg, ffi_parse_u32_arg, ffi_parse_u64_arg, ffi_parse_u8_arg, ffi_parse_usize_arg, NativeType, NativeValue}; +use crate::helpers::{ + call_failure, ffi_native_type_from_signature, inherited_interface_method_count, + normalize_parameter_signature, parse_native_type_from_signature, +}; +use crate::value::{ffi_parse_bool_arg, ffi_parse_buffer_arg, ffi_parse_f32_arg, ffi_parse_f64_arg, ffi_parse_function_arg, ffi_parse_i16_arg, ffi_parse_i32_arg, ffi_parse_i64_arg, ffi_parse_i8_arg, ffi_parse_isize_arg, ffi_parse_pointer_arg_with_signature, ffi_parse_string_arg, ffi_parse_struct_arg, ffi_parse_u16_arg, ffi_parse_u32_arg, ffi_parse_u64_arg, ffi_parse_u8_arg, ffi_parse_usize_arg, NativeType, NativeValue}; pub struct PropertyCall { index: usize, @@ -34,16 +38,11 @@ pub struct PropertyCall { parameters: Vec, return_type: String, pub(crate) declaration: Option>>, + is_valid: bool, /// Pre-allocated argument buffer reused on every call to avoid per-call heap allocation. argument_buf: Vec, } -#[inline] -fn call_failure() -> HRESULT { - // E_FAIL - HRESULT(0x8000_4005u32 as i32) -} - impl PropertyCall { pub fn is_void(&self) -> bool { self.is_void @@ -59,6 +58,17 @@ impl PropertyCall { interface: IUnknown, is_initializer: bool, ) -> Self { + Self::new_with_parent(property, is_setter, interface, is_initializer, None) + } + + pub fn new_with_parent( + property: &PropertyDeclaration, + is_setter: bool, + interface: IUnknown, + is_initializer: bool, + parent_declaration: Option>>, + ) -> Self { + let mut is_valid = true; let method = if is_setter { property.setter().unwrap() } else { @@ -68,6 +78,82 @@ impl PropertyCall { let number_of_parameters = method.number_of_parameters(); let mut index = 0 as usize; + let mut allow_qi_fallback = false; + + if !is_initializer { + if let Some(parent_declaration) = parent_declaration.as_ref() { + let parent = parent_declaration.read(); + match parent.kind() { + DeclarationKind::Interface => { + if let Some(parent_interface) = parent.as_any().downcast_ref::() { + let parent_index = Metadata::find_method_index( + method.metadata().unwrap(), + parent_interface.base().token(), + method.token(), + ); + let inherited_count = inherited_interface_method_count( + parent_interface.implemented_interfaces().as_slice(), + ); + return Self::new_bound_to_iid( + property, + is_setter, + interface, + is_initializer, + parent_interface.id(), + parent_index.saturating_add(6 + inherited_count), + None, + ); + } + } + DeclarationKind::GenericInterface => { + if let Some(parent_interface) = parent.as_any().downcast_ref::() { + let parent_index = Metadata::find_method_index( + method.metadata().unwrap(), + parent_interface.base().token(), + method.token(), + ); + let inherited_count = inherited_interface_method_count( + parent_interface.implemented_interfaces().as_slice(), + ); + return Self::new_bound_to_iid( + property, + is_setter, + interface, + is_initializer, + parent_interface.id(), + parent_index.saturating_add(6 + inherited_count), + None, + ); + } + } + DeclarationKind::GenericInterfaceInstance => { + if let Some(parent_interface) = parent + .as_any() + .downcast_ref::() + { + let parent_index = Metadata::find_method_index( + method.metadata().unwrap(), + parent_interface.base().token(), + method.token(), + ); + let inherited_count = inherited_interface_method_count( + parent_interface.implemented_interfaces().as_slice(), + ); + return Self::new_bound_to_iid( + property, + is_setter, + interface, + is_initializer, + parent_interface.id(), + parent_index.saturating_add(6 + inherited_count), + None, + ); + } + } + _ => {} + } + } + } let mut declaration: Option>> = None; @@ -84,6 +170,15 @@ impl PropertyCall { let kind = ii_lock.base().kind(); match kind { + DeclarationKind::GenericInterface => { + let ii = ii_lock + .as_declaration() + .as_any() + .downcast_ref::(); + let ii = ii.unwrap(); + iid = ii.id(); + allow_qi_fallback = true; + } DeclarationKind::GenericInterfaceInstance => { let ii = ii_lock .as_declaration() @@ -91,6 +186,7 @@ impl PropertyCall { .downcast_ref::(); let ii = ii.unwrap(); iid = ii.id(); + allow_qi_fallback = true; } _ => { let ii = ii_lock @@ -123,8 +219,12 @@ impl PropertyCall { ) }; - assert!(result.is_ok()); - assert!(!interface_ptr.is_null()); + if result.is_err() || interface_ptr.is_null() { + if !(allow_qi_fallback && !is_initializer) { + is_valid = false; + } + interface_ptr = interface.as_raw() as *mut c_void; + } let is_sealed = method.is_sealed(); @@ -162,11 +262,18 @@ impl PropertyCall { let metadata = parameter.metadata().unwrap(); let signature = Signature::to_string(metadata, &type_); + let signature = normalize_parameter_signature(signature.as_str()).to_string(); + + let parse_native_type = parse_native_type_from_signature(signature.as_str()); + parse_parameter_types.push(parse_native_type.clone()); + + let abi_type = if parse_native_type != NativeType::Pointer { + parse_native_type.clone() + } else { + ffi_native_type_from_signature(signature.as_str()) + }; - let parse_native_type = NativeType::try_from(signature.as_str()); - assert!(parse_native_type.is_ok()); - parse_parameter_types.push(parse_native_type.unwrap()); - parameter_types.push(ffi_native_type_from_signature(signature.as_str())); + parameter_types.push(abi_type); } if is_initializer { @@ -190,10 +297,15 @@ impl PropertyCall { .map(libffi::middle::Type::try_from) .collect::, AnyError>>(); - assert!(params.is_ok()); + let params = if let Ok(params) = params { + params + } else { + is_valid = false; + vec![Type::pointer()] + }; let cif = Cif::new( - params.unwrap(), + params, Type::i32(), ); @@ -201,8 +313,15 @@ impl PropertyCall { let interface = unsafe { IUnknown::from_raw(interface_ptr as *mut c_void) }; let vtable: *mut *mut c_void = unsafe { std::mem::transmute(interface.vtable()) }; - let func = unsafe { *vtable.offset(index as isize) }; + let func = if is_valid { + unsafe { *vtable.offset(index as isize) } + } else { + std::ptr::null_mut() + }; + if func.is_null() { + is_valid = false; + } Self { index, @@ -222,6 +341,130 @@ impl PropertyCall { declaration, return_type, is_setter, + is_valid, + argument_buf: Vec::with_capacity(number_of_abi_parameters), + } + } + + fn new_bound_to_iid( + property: &PropertyDeclaration, + is_setter: bool, + interface: IUnknown, + is_initializer: bool, + iid: GUID, + index: usize, + declaration: Option>>, + ) -> Self { + let mut is_valid = true; + let method = if is_setter { + property.setter().unwrap() + } else { + property.getter() + }; + + let number_of_parameters = method.number_of_parameters(); + let mut interface_ptr: *mut c_void = std::ptr::null_mut(); + let vtable = interface.vtable(); + + let result = unsafe { + ((*vtable).QueryInterface)( + interface.as_raw(), + &iid, + &mut interface_ptr as *mut _ as *mut *mut c_void, + ) + }; + + if result.is_err() || interface_ptr.is_null() { + if is_initializer { + is_valid = false; + } + interface_ptr = interface.as_raw() as *mut c_void; + } + + let is_sealed = method.is_sealed(); + let is_void = method.is_void(); + let signature = method.return_type(); + let return_type = Signature::to_string(method.metadata().unwrap(), &signature); + let other_params: usize = if is_initializer { + if is_sealed { 2 } else { 4 } + } else if is_void { + 1 + } else { + 2 + }; + + let number_of_abi_parameters = number_of_parameters + other_params; + let mut parameter_types: Vec = Vec::with_capacity(number_of_abi_parameters); + let mut parse_parameter_types: Vec = Vec::with_capacity(number_of_parameters); + parameter_types.push(NativeType::Pointer); + + for parameter in method.parameters().iter() { + let type_ = parameter.type_(); + let metadata = parameter.metadata().unwrap(); + let signature = Signature::to_string(metadata, &type_); + let signature = normalize_parameter_signature(signature.as_str()).to_string(); + + let parse_native_type = parse_native_type_from_signature(signature.as_str()); + parse_parameter_types.push(parse_native_type.clone()); + let abi_type = if parse_native_type != NativeType::Pointer { + parse_native_type.clone() + } else { + ffi_native_type_from_signature(signature.as_str()) + }; + parameter_types.push(abi_type); + } + + if !is_initializer && !is_void { + parameter_types.push(NativeType::Pointer); + } + + let params = parameter_types + .iter() + .cloned() + .map(libffi::middle::Type::try_from) + .collect::, AnyError>>(); + + let params = if let Ok(params) = params { + params + } else { + is_valid = false; + vec![Type::pointer()] + }; + + let cif = Cif::new(params, Type::i32()); + let parent_interface = interface.clone(); + let interface = unsafe { IUnknown::from_raw(interface_ptr as *mut c_void) }; + let vtable: *mut *mut c_void = unsafe { std::mem::transmute(interface.vtable()) }; + let func = if is_valid { + unsafe { *vtable.offset(index as isize) } + } else { + std::ptr::null_mut() + }; + + let mut final_valid = is_valid; + if func.is_null() { + final_valid = false; + } + + Self { + index, + number_of_parameters, + number_of_abi_parameters, + is_initializer, + is_sealed, + is_void: method.is_void(), + iid, + cif, + func, + parent_interface, + interface, + parameter_types, + parse_parameter_types, + parameters: method.parameters().to_vec(), + declaration, + return_type, + is_setter, + is_valid: final_valid, argument_buf: Vec::with_capacity(number_of_abi_parameters), } } @@ -244,10 +487,12 @@ impl PropertyCall { scope: &mut v8::PinScope<'_, '_>, values: &[v8::Local], ) -> (HRESULT, *mut c_void) { - let is_void = self.is_void; + if !self.is_valid || self.func.is_null() { + return (call_failure(), std::ptr::null_mut()); + } - // Reuse the pre-allocated buffer — avoids a heap allocation on every call. self.argument_buf.clear(); + let mut pointer_keepalive: Vec = Vec::new(); unsafe { self.argument_buf.push(NativeValue { pointer: std::mem::transmute_copy(&self.interface) }) }; @@ -256,6 +501,18 @@ impl PropertyCall { return (call_failure(), std::ptr::null_mut()); }; + let expected_signature = self + .parameters + .get(i) + .and_then(|parameter| { + parameter + .metadata() + .map(|metadata| { + let signature = Signature::to_string(metadata, ¶meter.type_()); + normalize_parameter_signature(signature.as_str()).to_string() + }) + }); + let value = match *native_type { NativeType::Void => { return (call_failure(), std::ptr::null_mut()) @@ -300,7 +557,19 @@ impl PropertyCall { ffi_parse_f64_arg(value) } NativeType::Pointer => { - ffi_parse_pointer_arg(scope, value) + match ffi_parse_pointer_arg_with_signature( + scope, + value, + expected_signature.as_deref(), + ) { + Ok((native_value, keepalive)) => { + if let Some(keepalive) = keepalive { + pointer_keepalive.push(keepalive); + } + Ok(native_value) + } + Err(error) => Err(error), + } } NativeType::Buffer => { ffi_parse_buffer_arg(scope, value) @@ -327,10 +596,19 @@ impl PropertyCall { let mut result: *mut c_void = std::ptr::null_mut(); - if !self.is_initializer && !is_void { - self.argument_buf.push(NativeValue { pointer: &mut result as *mut _ as *mut c_void }); + + if self.is_initializer { + // arguments.push(result_ptr as *mut c_void); + } else { + if !self.is_void { + self.argument_buf.push(NativeValue { pointer: &mut result as *mut _ as *mut c_void }); + } } + // let mut func = std::ptr::null_mut(); + // + // get_method(&self.interface, self.index, addr_of_mut!(func)); + let mut call_args: Vec = Vec::with_capacity(self.argument_buf.len()); for (i, v) in self.argument_buf.iter().enumerate() { // SAFETY: Creating a `Arg` from a `NativeValue` is safe when the parallel type vector matches. @@ -349,4 +627,4 @@ impl PropertyCall { (HRESULT(ret), result) } -} \ No newline at end of file +} diff --git a/runtime/src/value.rs b/runtime/src/value.rs index 70e008d..0eff51c 100644 --- a/runtime/src/value.rs +++ b/runtime/src/value.rs @@ -10,8 +10,12 @@ use crate::DeclarationFFI; use libffi::low::*; use libffi::raw::ffi_call; use metadata::declarations::base_class_declaration::BaseClassDeclarationImpl; +use metadata::declarations::class_declaration::ClassDeclaration; use metadata::declarations::declaration::{Declaration, DeclarationKind}; +use metadata::declarations::delegate_declaration::DelegateDeclaration; +use metadata::declarations::delegate_declaration::DelegateDeclarationImpl; use metadata::declarations::delegate_declaration::generic_delegate_declaration::GenericDelegateDeclaration; +use metadata::declarations::delegate_declaration::generic_delegate_instance_declaration::GenericDelegateInstanceDeclaration; use metadata::declarations::interface_declaration::generic_interface_declaration::GenericInterfaceDeclaration; use metadata::declarations::interface_declaration::generic_interface_instance_declaration::GenericInterfaceInstanceDeclaration; use metadata::declarations::interface_declaration::InterfaceDeclaration; @@ -19,6 +23,7 @@ use metadata::declarations::method_declaration::MethodDeclaration; use metadata::declarations::parameter_declaration::ParameterDeclaration; use metadata::declarations::property_declaration::PropertyDeclaration; use metadata::declaring_interface_for_method::Metadata; +use metadata::meta_data_reader::MetadataReader; use metadata::prelude::cor_sig_uncompress_element_type; use metadata::signature::Signature; use parking_lot::RwLock; @@ -27,6 +32,7 @@ use byteorder::ByteOrder; use libffi::middle::{Arg, Cif}; use v8::FunctionCallbackArguments; use windows::core::{IInspectable, IUnknown, Interface, GUID, HRESULT, HSTRING, IUnknown_Vtbl, IInspectable_Vtbl}; +use windows::Foundation::PropertyValue; use windows::Win32::System::Com::IDispatch; use windows::Win32::System::WinRT::IActivationFactory; use windows::Win32::System::WinRT::Metadata::{CorElementType, ELEMENT_TYPE_CLASS}; @@ -600,7 +606,13 @@ fn try_get_external_handle( return Some( dec.instance .as_ref() - .map(|instance| unsafe { mem::transmute_copy(instance) }) + .map(|instance| { + if let Ok(inspectable) = instance.cast::() { + inspectable.as_raw() as *mut c_void + } else { + instance.as_raw() as *mut c_void + } + }) .unwrap_or(std::ptr::null_mut()), ); } @@ -608,6 +620,141 @@ fn try_get_external_handle( None } +#[inline] +fn expected_iid_for_signature(signature: &str) -> Option { + if signature.is_empty() || !signature.contains('.') { + return None; + } + + let declaration = MetadataReader::find_by_name(signature).or_else(|| { + let mut lookup = signature.to_string(); + if let Some(generic_index) = lookup.find('<') { + lookup.truncate(generic_index); + } + MetadataReader::find_by_name(lookup.as_str()) + })?; + let lock = declaration.read(); + + match lock.kind() { + DeclarationKind::Interface => { + let interface = lock.as_any().downcast_ref::()?; + Some(interface.id()) + } + DeclarationKind::GenericInterface => { + let interface = lock + .as_any() + .downcast_ref::()?; + Some(interface.id()) + } + DeclarationKind::GenericInterfaceInstance => { + let interface = lock + .as_any() + .downcast_ref::()?; + Some(interface.id()) + } + DeclarationKind::Class => { + let class = lock.as_any().downcast_ref::()?; + class.default_interface().map(|interface| interface.id()) + } + DeclarationKind::Delegate => { + let delegate = lock.as_any().downcast_ref::()?; + Some(delegate.id()) + } + DeclarationKind::GenericDelegate => { + let delegate = lock + .as_any() + .downcast_ref::()?; + Some(delegate.id()) + } + DeclarationKind::GenericDelegateInstance => { + let delegate = lock + .as_any() + .downcast_ref::()?; + Some(delegate.id()) + } + _ => None, + } +} + +#[inline] +fn try_query_instance_for_signature(instance: &IUnknown, signature: &str) -> Option { + let iid = expected_iid_for_signature(signature)?; + let vtable = instance.vtable(); + + let mut queried: *mut c_void = std::ptr::null_mut(); + let hr = unsafe { + ((*vtable).QueryInterface)( + instance.as_raw(), + &iid, + &mut queried as *mut _ as *mut *mut c_void, + ) + }; + + if hr.is_ok() && !queried.is_null() { + Some(unsafe { IUnknown::from_raw(queried) }) + } else { + None + } +} + +#[inline] +fn try_box_js_value_for_object_signature( + scope: &mut v8::PinScope<'_, '_>, + arg: v8::Local, + expected_signature: Option<&str>, +) -> Option { + let signature = expected_signature?; + if !matches!( + signature, + "Object" | "IInspectable" | "Windows.Foundation.IInspectable" + ) { + return None; + } + + if arg.is_null_or_undefined() { + return None; + } + + if arg.is_string() { + let value = arg.to_string(scope)?.to_rust_string_lossy(scope); + if let Ok(inspectable) = PropertyValue::CreateString(&HSTRING::from(value)) { + return Some(inspectable.into()); + } + } + + if arg.is_boolean() { + if let Ok(inspectable) = PropertyValue::CreateBoolean(arg.boolean_value(scope)) { + return Some(inspectable.into()); + } + } + + if arg.is_number() { + if let Some(value) = arg.number_value(scope) { + if let Ok(inspectable) = PropertyValue::CreateDouble(value) { + return Some(inspectable.into()); + } + } + } + + None +} + +#[inline] +fn try_get_inspectable_for_object_signature( + instance: &IUnknown, + expected_signature: Option<&str>, +) -> Option { + let signature = expected_signature?; + if !matches!( + signature, + "Object" | "IInspectable" | "Windows.Foundation.IInspectable" + ) { + return None; + } + + instance.cast::().ok().map(Into::into) +} + #[inline] pub fn ffi_parse_pointer_arg( scope: &mut v8::PinScope<'_, '_>, @@ -632,6 +779,44 @@ pub fn ffi_parse_pointer_arg( Ok(NativeValue { pointer }) } +#[inline] +pub fn ffi_parse_pointer_arg_with_signature( + scope: &mut v8::PinScope<'_, '_>, + arg: v8::Local, + expected_signature: Option<&str>, +) -> std::result::Result<(NativeValue, Option), AnyError> { + if let Some(boxed) = try_box_js_value_for_object_signature(scope, arg, expected_signature) { + let pointer = boxed.as_raw() as *mut c_void; + return Ok((NativeValue { pointer }, Some(boxed))); + } + + if arg.is_object() { + let arg_obj = arg.to_object(scope).unwrap(); + + if let Some(dec) = arg_obj.get_internal_field(scope, 0) { + let dec = unsafe { dec.cast::() }; + let dec = dec.value() as *mut DeclarationFFI; + let dec = unsafe { &*dec }; + + if let (Some(signature), Some(instance)) = (expected_signature, dec.instance.as_ref()) { + if let Some(inspectable) = + try_get_inspectable_for_object_signature(instance, Some(signature)) + { + let pointer = inspectable.as_raw() as *mut c_void; + return Ok((NativeValue { pointer }, Some(inspectable))); + } + + if let Some(typed) = try_query_instance_for_signature(instance, signature) { + let pointer = typed.as_raw() as *mut c_void; + return Ok((NativeValue { pointer }, Some(typed))); + } + } + } + } + + ffi_parse_pointer_arg(scope, arg).map(|value| (value, None)) +} + #[inline] pub fn ffi_parse_buffer_arg( scope: &mut v8::PinScope<'_, '_>,