Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions dotnet-bridge/Bridge.BinaryDispatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,12 @@ TypeCode.SByte or TypeCode.Byte or TypeCode.Int16 or TypeCode.UInt16 or
{
return original;
}
// The C#/WinRT projection (FileOpenPicker, StorageFile, ...) when the app loads
// C#/WinRT, so the object casts to its projected type and works with
// WinRT.Interop.InitializeWithWindow.
var projected = TryProjectWinRT(nativePtr);
if (projected is not null && targetType.IsInstanceOfType(projected))
return projected;
// 1. Typed QI first: works for COM/CsWinRT interface types that carry a
// [Guid] attribute. More precise than a generic RCW for strongly-typed
// parameters such as Windows.UI.Xaml.UIElement.
Expand Down
24 changes: 24 additions & 0 deletions dotnet-bridge/Bridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,30 @@ private static IntPtr ObtainNativePtr(object obj)
}
}

private static MethodInfo? s_marshalInspectableFromAbi;

// WinRT.MarshalInspectable<object>.FromAbi(ptr), found by reflection: the C#/WinRT projection
// of a native WinRT object. Null when C#/WinRT isn't loaded or can't project it.
private static object? TryProjectWinRT(IntPtr ptr)
{
try
{
if (s_marshalInspectableFromAbi is null)
{
var open = AppDomain.CurrentDomain.GetAssemblies()
.Select(a => a.GetType("WinRT.MarshalInspectable`1"))
.FirstOrDefault(t => t is not null);
s_marshalInspectableFromAbi = open?.MakeGenericType(typeof(object)).GetMethod(
"FromAbi", BindingFlags.Public | BindingFlags.Static, [typeof(IntPtr)]);
}
return s_marshalInspectableFromAbi?.Invoke(null, [ptr]);
}
catch
{
return null;
}
}

[UnmanagedCallersOnly(EntryPoint = "Invoke",
CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
public static unsafe int Invoke(
Expand Down
16 changes: 16 additions & 0 deletions runtime/src/global_fns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3374,6 +3374,14 @@ fn bin_write_v8_arg(

if arg.is_object() {
if let Some(obj) = arg.to_object(scope) {
// A WinRT object: tag 0x0A + its IUnknown. Borrowed: the wrapper keeps it alive
// while the bridge wraps it, and the managed wrapper takes its own reference.
if let Some(instance) = crate::ns_proxy::this_instance(scope, obj) {
let ptr = instance.as_raw() as u64;
buf.push(0x0A);
buf.extend_from_slice(&ptr.to_le_bytes());
return;
}
// Dotnet bridge handle: { __handle: i32 }
if let Some(key) = v8::String::new(scope, "__handle") {
if let Some(hval) = obj.get(scope, key.into()) {
Expand Down Expand Up @@ -3645,6 +3653,14 @@ fn bin_write_v8_value(

if arg.is_object() {
if let Some(obj) = arg.to_object(scope) {
// A WinRT object: tag 0x0A + its IUnknown. Borrowed: the wrapper keeps it alive
// while the bridge wraps it, and the managed wrapper takes its own reference.
if let Some(instance) = crate::ns_proxy::this_instance(scope, obj) {
let ptr = instance.as_raw() as u64;
buf.push(0x0A);
buf.extend_from_slice(&ptr.to_le_bytes());
return;
}
// Dotnet bridge handle: { __handle: i32 }
if let Some(key) = v8::String::new(scope, "__handle") {
if let Some(hval) = obj.get(scope, key.into()) {
Expand Down
143 changes: 102 additions & 41 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,12 @@ use windows::core::{IUnknown, Interface, GUID, HSTRING, PCWSTR};
use windows::core::{Error, HRESULT};
#[cfg(feature = "classic")]
use windows::core::IInspectable;
#[cfg(feature = "classic")]
use windows::Win32::System::Console::GetConsoleWindow;
use windows::Win32::System::Diagnostics::Debug::OutputDebugStringW;
use windows::Win32::System::WinRT::RoGetActivationFactory;
#[cfg(feature = "classic")]
use windows::Win32::System::WinRT::{
IActivationFactory, RoInitialize, RoUninitialize, RO_INIT_SINGLETHREADED,
};
#[cfg(feature = "classic")]
use windows::Win32::UI::Shell::IInitializeWithWindow;
use windows::Win32::UI::WindowsAndMessaging::{
DispatchMessageW, PeekMessageW, TranslateMessage, MSG, PM_REMOVE,
Expand Down Expand Up @@ -870,27 +867,32 @@ fn fill_class_members(
}
}
};
let mut absorb_methods = |list: &[MethodDeclaration]| {
for m in list {
let key = if !m.overload_name().is_empty() {
m.overload_name()
} else {
m.name()
};
if !methods.contains_key(key) {
methods.insert(key.to_string(), m.clone());
}
}
};
let mut lists: Vec<&[MethodDeclaration]> = vec![class_declaration.methods()];
absorb_props(class_declaration.properties());
absorb_methods(class_declaration.methods());
if let Some(di) = class_declaration.default_interface() {
absorb_props(di.properties());
absorb_methods(di.methods());
lists.push(di.methods());
}
for iface in class_declaration.implemented_interfaces() {
absorb_props(iface.properties());
absorb_methods(iface.methods());
lists.push(iface.methods());
}
for m in lists.iter().flat_map(|l| l.iter()) {
let key = if !m.overload_name().is_empty() {
m.overload_name()
} else {
m.name()
};
if !methods.contains_key(key) {
methods.insert(key.to_string(), m.clone());
}
}
// The public name must reach overloads that all carry an `[Overload]` name
// (`StorageFile.CopyAsync` is `CopyOverload*`); callers switch arity from there.
for m in lists.iter().flat_map(|l| l.iter()) {
if !methods.contains_key(m.name()) {
methods.insert(m.name().to_string(), m.clone());
}
}
if !class_declaration.base_full_name().is_empty() {
if let Some(base_decl) = MetadataReader::find_by_name(class_declaration.base_full_name()) {
Expand Down Expand Up @@ -1186,10 +1188,34 @@ pub(crate) fn return_value_from_kind<'a>(
ReturnKind::Struct(declaration) => {
create_struct_object_from_raw(declaration.clone(), result, scope).into()
}
ReturnKind::InterfaceObject { decl, type_name } => {
if result.is_null() {
v8::null(scope).into()
} else {
let instance = unsafe { IUnknown::from_raw(result) };
// Wrap by the runtime class: an interface such as IPropertySet declares no
// members itself and only requires the generic IMap/IIterable instances.
match crate::ns_proxy::resolve_runtime_class(&instance, type_name.as_ref()) {
Some((name, class)) => {
let instance = Some(instance);
create_ns_ctor_instance_object(&name, None, None, class, instance, scope)
.into()
}
None => create_ns_ctor_instance_object(
type_name.as_ref(),
None,
parent,
decl.clone(),
Some(instance),
scope,
)
.into(),
}
}
}
ReturnKind::Object {
decl, type_name, ..
}
| ReturnKind::InterfaceObject { decl, type_name } => {
} => {
if result.is_null() {
v8::null(scope).into()
} else {
Expand Down Expand Up @@ -1733,18 +1759,60 @@ pub(crate) fn throw_js_error(scope: &mut v8::PinScope<'_, '_>, message: &str) {
}
}

/// `IInitializeWithWindow` support probe for a freshly constructed object. A raw QueryInterface:
/// `cast` builds a `windows_core::Error` (which calls GetErrorInfo) for the E_NOINTERFACE
/// outcome that almost every class produces, on every construct.
#[cfg(feature = "classic")]
fn query_initialize_with_window(unk: &IUnknown) -> Option<IInitializeWithWindow> {
/// Gives a freshly constructed object that supports `IInitializeWithWindow` (pickers, dialogs)
/// the app's window as its owner. A raw QueryInterface: `cast` builds a `windows_core::Error`
/// (which calls GetErrorInfo) for the E_NOINTERFACE outcome that almost every class produces,
/// on every construct.
pub(crate) fn initialize_with_owner_window(unk: &IUnknown) {
let mut out: *mut c_void = std::ptr::null_mut();
let hr = unsafe { unk.query(&IInitializeWithWindow::IID, &mut out) };
if hr.is_ok() && !out.is_null() {
Some(unsafe { IInitializeWithWindow::from_raw(out) })
} else {
None
if hr.is_err() || out.is_null() {
return;
}
let init = unsafe { IInitializeWithWindow::from_raw(out) };
if let Some(hwnd) = owner_window() {
let _ = unsafe { init.Initialize(hwnd) };
}
}

/// A visible, unowned top-level window of this thread, else of this process, else the console.
fn owner_window() -> Option<windows::Win32::Foundation::HWND> {
use windows::Win32::Foundation::{HWND, LPARAM};
use windows::Win32::System::Threading::{GetCurrentProcessId, GetCurrentThreadId};
use windows::Win32::UI::WindowsAndMessaging::{
EnumThreadWindows, EnumWindows, GetWindow, GetWindowThreadProcessId, IsWindowVisible,
GW_OWNER,
};

unsafe extern "system" fn pick(hwnd: HWND, found: LPARAM) -> windows::core::BOOL {
let unowned = GetWindow(hwnd, GW_OWNER).map_or(true, |owner| owner.is_invalid());
if IsWindowVisible(hwnd).as_bool() && unowned {
*(found.0 as *mut HWND) = hwnd;
return false.into();
}
true.into()
}
unsafe extern "system" fn pick_in_process(hwnd: HWND, found: LPARAM) -> windows::core::BOOL {
let mut pid = 0u32;
GetWindowThreadProcessId(hwnd, Some(&mut pid));
if pid != GetCurrentProcessId() {
return true.into();
}
pick(hwnd, found)
}

let mut found = HWND::default();
let slot = LPARAM(&mut found as *mut HWND as isize);
unsafe {
let _ = EnumThreadWindows(GetCurrentThreadId(), Some(pick), slot);
if found.is_invalid() {
let _ = EnumWindows(Some(pick_in_process), slot);
}
if found.is_invalid() {
found = windows::Win32::System::Console::GetConsoleWindow();
}
}
(!found.is_invalid()).then_some(found)
}

pub(crate) fn class_activation_factory(full_name: &str) -> windows::core::Result<IUnknown> {
Expand Down Expand Up @@ -6196,12 +6264,7 @@ fn create_ns_ctor_object<'a>(
}
};

if let Some(init) = query_initialize_with_window(&result) {
let hwnd = unsafe { GetConsoleWindow() };
if !hwnd.is_invalid() {
let _ = unsafe { init.Initialize(hwnd) };
}
}
crate::initialize_with_owner_window(&result);

{
let declaration = Arc::clone(&class_declaration);
Expand Down Expand Up @@ -6259,12 +6322,7 @@ fn create_ns_ctor_object<'a>(
}
let result = unsafe { IUnknown::from_raw(qi_ptr) };

if let Some(init) = query_initialize_with_window(&result) {
let hwnd = unsafe { GetConsoleWindow() };
if !hwnd.is_invalid() {
let _ = unsafe { init.Initialize(hwnd) };
}
}
crate::initialize_with_owner_window(&result);

{
let declaration = Arc::clone(&class_declaration);
Expand Down Expand Up @@ -9089,6 +9147,9 @@ mod instance_cache_test;
#[cfg(all(test, feature = "classic"))]
mod interop_test;

#[cfg(all(test, feature = "classic"))]
mod projection_test;

#[cfg(all(test, feature = "classic"))]
mod js_delegate_tests {
use super::{
Expand Down
23 changes: 23 additions & 0 deletions runtime/src/napi_engine/dotnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ unsafe fn addref_native_ptr(ptr: u64) {
((*vtable).AddRef)(unknown.as_raw());
}

/// The COM instance of a WinRT wrapper (host object or proxy), borrowed: the wrapper keeps it
/// alive while the bridge wraps it, and the managed wrapper takes its own reference.
fn winrt_instance_ptr(env: &Env, obj: &JsObject) -> Option<u64> {
if let Ok(dec) = env.unwrap::<crate::DeclarationFFI>(obj) {
return dec.instance.as_ref().map(|i| i.as_raw() as u64);
}
let handle = obj.get_named_property::<JsUnknown>("handle").ok()?;
let ptr = crate::napi_engine::value::ptr_from_external(env, &handle)?;
(!ptr.is_null()).then_some(ptr as u64)
}

/// Pins `func` as a callback the bridge can invoke; returns its id.
fn pin_js_callback(env: &Env, func: sys::napi_value) -> Option<i32> {
let mut func_ref: sys::napi_ref = std::ptr::null_mut();
Expand Down Expand Up @@ -371,6 +382,12 @@ fn napi_bin_write_arg(env: &Env, buf: &mut Vec<u8>, arg: &JsUnknown) {
return;
}
}
// A WinRT object: tag 0x0A + its IUnknown (borrowed, see `winrt_instance_ptr`).
if let Some(ptr) = winrt_instance_ptr(env, &obj) {
buf.push(0x0A);
buf.extend_from_slice(&ptr.to_le_bytes());
return;
}
if let Ok(pval) = obj.get_named_property::<JsUnknown>("__native_ptr") {
if let Ok(ValueType::BigInt) = pval.get_type() {
let b: JsBigInt = unsafe { pval.cast() };
Expand Down Expand Up @@ -456,6 +473,12 @@ fn napi_bin_write_value(env: &Env, buf: &mut Vec<u8>, arg: &JsUnknown) {
return;
}
}
// A WinRT object: tag 0x0A + its IUnknown (borrowed, see `winrt_instance_ptr`).
if let Some(ptr) = winrt_instance_ptr(env, &obj) {
buf.push(0x0A);
buf.extend_from_slice(&ptr.to_le_bytes());
return;
}
if let Ok(pval) = obj.get_named_property::<JsUnknown>("__native_ptr") {
if let Ok(ValueType::BigInt) = pval.get_type() {
let b: JsBigInt = unsafe { pval.cast() };
Expand Down
21 changes: 16 additions & 5 deletions runtime/src/napi_engine/ns_hostobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,24 @@ pub fn is_interface(declaration: &Decl) -> bool {
)
}

/// The metadata class named by `instance`'s GetRuntimeClassName.
pub fn runtime_class(instance: &IUnknown) -> Option<(String, Decl)> {
let inspectable = instance.cast::<IInspectable>().ok()?;
let name = unsafe { inspectable.GetRuntimeClassName() }.ok()?.to_string();
let declaration = MetadataReader::find_by_name(&name)?;
if !matches!(declaration.read().kind(), DeclarationKind::Class) {
return None;
}
Some((name, declaration))
}

/// For an interface-typed instance, ask the concrete object for its runtime class and, if that class
/// is host-eligible, return its (name, declaration) so it can be host-wrapped instead of Proxy'd.
pub fn runtime_class_host_target(instance: &IUnknown) -> Option<(String, Decl)> {
if !host_enabled() {
return None;
}
let inspectable = instance.cast::<IInspectable>().ok()?;
let name = unsafe { inspectable.GetRuntimeClassName() }.ok()?.to_string();
let declaration = MetadataReader::find_by_name(&name)?;
let (name, declaration) = runtime_class(instance)?;
let eligible = {
let lock = declaration.read();
match lock.as_any().downcast_ref::<ClassDeclaration>() {
Expand Down Expand Up @@ -324,9 +333,11 @@ fn activate_prepared(
};
let inspectable: IInspectable = unsafe { af.ActivateInstance() }
.map_err(|e| generic_error(format!("ActivateInstance failed for {class_name}: {e}")))?;
inspectable
let instance = inspectable
.cast::<IUnknown>()
.map_err(|e| generic_error(format!("activated-instance cast failed: {e}")))
.map_err(|e| generic_error(format!("activated-instance cast failed: {e}")))?;
crate::initialize_with_owner_window(&instance);
Ok(instance)
}

/// Parameterized construction (`new Class(args)`): pick the initializer by arity, prepare it on
Expand Down
Loading