Skip to content

Commit

Permalink
Implement static COM objects
Browse files Browse the repository at this point in the history
  • Loading branch information
Arlie Davis committed Jul 3, 2024
1 parent 9f96662 commit 54fb91e
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 9 deletions.
65 changes: 65 additions & 0 deletions crates/libs/core/src/com_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,68 @@ impl<T: ComObjectInner> Borrow<T> for ComObject<T> {
self.get()
}
}

/// Enables applications to define COM objects using static storage. This is useful for factory
/// objects, stateless objects, or objects which use need to contain or use mutable global state.
///
/// COM objects that are defined using `StaticComObject` have their storage placed directly in
/// static storage; they are not stored in the heap.
///
/// COM objects defined using `StaticComObject` do have a reference count and this reference
/// count is adjusted when owned COM interface references (e.g. `IFoo` and `IUnknown`) are created
/// for the object. The reference count is initialized to 1.
///
/// # Example
///
/// ```rust,no_test
/// #[implement(IFoo)]
/// struct MyApp {
/// // ...
/// }
///
/// static MY_STATIC_APP: StaticComObject<MyApp> = MyApp { ... }.into_static();
///
/// fn get_my_static_ifoo() -> IFoo {
/// MY_STATIC_APP.to_interface()
/// }
/// ```
pub struct StaticComObject<T>
where
T: ComObjectInner,
{
outer: T::Outer,
}

// IMPORTANT: Do not expose any methods that return mutable access to the contents of StaticComObject.
// Doing so would violate our safety invariants. For example, we provide a Deref impl but it would
// be unsound to provide a DerefMut impl.
impl<T> StaticComObject<T>
where
T: ComObjectInner,
{
/// Wraps `outer` in a `StaticComObject`.
pub const fn from_outer(outer: T::Outer) -> Self {
Self { outer }
}
}

impl<T> StaticComObject<T>
where
T: ComObjectInner,
{
/// Gets access to the contained value.
pub const fn get(&'static self) -> &'static T::Outer {
&self.outer
}
}

impl<T> core::ops::Deref for StaticComObject<T>
where
T: ComObjectInner,
{
type Target = T::Outer;

fn deref(&self) -> &Self::Target {
&self.outer
}
}
2 changes: 1 addition & 1 deletion crates/libs/core/src/imp/ref_count.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub struct RefCount(pub(crate) AtomicI32);

impl RefCount {
/// Creates a new `RefCount` with an initial value of `1`.
pub fn new(count: u32) -> Self {
pub const fn new(count: u32) -> Self {
Self(AtomicI32::new(count as i32))
}

Expand Down
2 changes: 1 addition & 1 deletion crates/libs/core/src/imp/weak_ref_count.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use core::sync::atomic::{AtomicIsize, Ordering};
pub struct WeakRefCount(AtomicIsize);

impl WeakRefCount {
pub fn new() -> Self {
pub const fn new() -> Self {
Self(AtomicIsize::new(1))
}

Expand Down
2 changes: 1 addition & 1 deletion crates/libs/core/src/weak.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub struct Weak<I: Interface>(Option<imp::IWeakReference>, PhantomData<I>);

impl<I: Interface> Weak<I> {
/// Creates a new `Weak` object without any backing object.
pub fn new() -> Self {
pub const fn new() -> Self {
Self(None, PhantomData)
}

Expand Down
39 changes: 33 additions & 6 deletions crates/libs/implement/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,38 @@ pub fn implement(
const IDENTITY: ::windows_core::IInspectable_Vtbl = ::windows_core::IInspectable_Vtbl::new::<Self, #identity_type, 0>();
}

impl #generics #original_ident::#generics where #constraints {
/// This converts a partially-constructed COM object (in the sense that it contains
/// application state but does not yet have vtable and reference count constructed)
/// into a `StaticComObject`. This allows the COM object to be stored in static
/// (global) variables.
pub const fn into_static(self) -> ::windows_core::StaticComObject<Self> {
::windows_core::StaticComObject::from_outer(self.into_outer())
}

// This constructs an "outer" object. This should only be used by the implementation
// of the outer object, never by application code.
//
// The callers of this function (`into_static` and `into_object`) are both responsible
// for maintaining one of our invariants: Application code never has an owned instance
// of the outer (implementation) type. into_static() maintains this invariant by
// returning a wrapped StaticComObject value, which owns its contents but never gives
// application code a way to mutably access its contents. This prevents the refcount
// shearing problem.
//
// TODO: Make it impossible for app code to call this function, by placing it in a
// module and marking this as private to the module.
#[inline(always)]
const fn into_outer(self) -> #impl_ident::#generics {
#impl_ident::#generics {
identity: &#impl_ident::#generics::IDENTITY,
vtables: (#(&#impl_ident::#generics::VTABLES.#offset,)*),
this: self,
count: ::windows_core::imp::WeakRefCount::new(),
}
}
}

impl #generics ::windows_core::ComObjectInner for #original_ident::#generics where #constraints {
type Outer = #impl_ident::#generics;

Expand All @@ -191,12 +223,7 @@ pub fn implement(
// This is why this function returns ComObject<Self> instead of returning #impl_ident.

fn into_object(self) -> ::windows_core::ComObject<Self> {
let boxed = ::windows_core::imp::Box::new(#impl_ident::#generics {
identity: &#impl_ident::#generics::IDENTITY,
vtables: (#(&#impl_ident::#generics::VTABLES.#offset,)*),
this: self,
count: ::windows_core::imp::WeakRefCount::new(),
});
let boxed = ::windows_core::imp::Box::<#impl_ident::#generics>::new(self.into_outer());
unsafe {
let ptr = ::windows_core::imp::Box::into_raw(boxed);
::windows_core::ComObject::from_raw(
Expand Down
1 change: 1 addition & 0 deletions crates/tests/implement_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@

mod com_chain;
mod com_object;
mod static_com_object;
77 changes: 77 additions & 0 deletions crates/tests/implement_core/src/static_com_object.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! Unit tests for `windows_core::StaticComObject`
use std::sync::atomic::{AtomicU32, Ordering::SeqCst};
use windows_core::{
implement, interface, ComObject, IUnknown, IUnknownImpl, IUnknown_Vtbl, InterfaceRef,
StaticComObject,
};

#[interface("818f2fd1-d479-4398-b286-a93c4c7904d1")]
unsafe trait INumberFactory: IUnknown {
fn next(&self) -> u32;

fn add(&self, x: u32, y: u32) -> u32;
}

#[implement(INumberFactory)]
struct MyFactory {
x: AtomicU32,
}

impl INumberFactory_Impl for MyFactory_Impl {
unsafe fn next(&self) -> u32 {
self.x.fetch_add(1, SeqCst)
}

unsafe fn add(&self, x: u32, y: u32) -> u32 {
x + y
}
}

static NUMBER_FACTORY_INSTANCE: StaticComObject<MyFactory> = MyFactory {
x: AtomicU32::new(100),
}
.into_static();

#[test]
fn as_interface() {
let factory_outer: &MyFactory_Impl = NUMBER_FACTORY_INSTANCE.get();
let ifactory: InterfaceRef<INumberFactory> = factory_outer.as_interface::<INumberFactory>();

// Produce the next number. We don't verify the value since tests are multi-threaded.
// This just demonstrates that you can have shared state with interior mutability (such as
// atomics) in a static COM object.
let n = unsafe { ifactory.next() };
println!("n = {n:?}");

assert_eq!(unsafe { ifactory.add(333, 444) }, 777);
}

// This tests that we can safely AddRef/Release a StaticComObject.
#[test]
fn to_interface() {
let factory_outer: &MyFactory_Impl = NUMBER_FACTORY_INSTANCE.get();
let ifactory: INumberFactory = factory_outer.to_interface::<INumberFactory>();
assert_eq!(unsafe { ifactory.add(333, 444) }, 777);
drop(ifactory);
}

#[test]
fn to_object() {
let factory_outer: &MyFactory_Impl = NUMBER_FACTORY_INSTANCE.get();
let factory_object: ComObject<MyFactory> = factory_outer.to_object();
assert_eq!(unsafe { factory_object.add(333, 444) }, 777);
}

// This tests the behavior when dropping a StaticComObject. Since static variables are never
// dropped, this isn't relevant to normal usage. However, if app code constructs a StaticComObject
// in local variables (not statics) and them drops them, then we still need well-defined behavior.
// Basically, we are testing that the refererence-count field does not panic when being dropped
// with a non-zero reference count.
#[test]
fn drop_half_constructed() {
let _static_com_object: StaticComObject<MyFactory> = MyFactory {
x: AtomicU32::new(0),
}
.into_static();
}

0 comments on commit 54fb91e

Please sign in to comment.