Giter Site home page Giter Site logo

com-rs's Introduction

COM

Build Status crates.io Docs.rs

A one stop shop for all things related to COM programming in Rust.

This library exposes various macros, structs and functions to the user for both producing and consuming COM components in an idiomatic manner.

๐Ÿšจ ๐Ÿšจ ๐Ÿšจ NOTE This crate is currently in heavy development as we decide on a stable API. ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ

What is COM?

COM is a platform-independent, distributed, object-oriented system for creating binary software components that can interact.

COM has been superseded by WinRT which builds on COM to provide even more guarantees about the binary interface. As such, if you're not sure if you need to use COM, you probably shouldn't.

Usage

Defining a COM interface

To both consume or produce a COM component through an interface, you will first need to generate the Rust representation of said interface. The interfaces macro is the main tool for automatically generating this Rust representation.

com::interfaces! {
    #[uuid("00000000-0000-0000-C000-000000000046")]
    pub unsafe interface IUnknown {
        fn QueryInterface(
            &self,
            riid: *const IID,
            ppv: *mut *mut c_void
        ) -> HRESULT;
        fn AddRef(&self) -> u32;
        fn Release(&self) -> u32;
    }

    #[uuid("EFF8970E-C50F-45E0-9284-291CE5A6F771")]
    pub unsafe interface IAnimal: IUnknown {
        fn Eat(&self) -> HRESULT;
    }
}

Short explanation: This generates the VTable layout for IUnknown and IAnimal as well as the correct Clone and Drop implementations.

Consuming a COM component

Interaction with COM components are always through an Interface Pointer (a pointer to a pointer to a VTable).

use com::run_time::{create_instance, init_runtime};

// Initialises the COM library
init_runtime().expect("Failed to initialize COM Library");

// Get a COM instance's interface pointer, by specifying
// - The CLSID of the COM component
// - The interface of the COM component that you want
// create_instance returns an IAnimal interface in this case.
let mut cat = create_instance::<IAnimal>(&CLSID_CAT_CLASS).expect("Failed to get a cat");

// All IAnimal methods will be available.
// Because we are crossing an FFI boundary, all COM interfaces are marked as unsafe.
// It is the job of the programmer to ensure that invariants beyond what the COM library guarantees are upheld.
unsafe { cat.Eat(); }

For more information on usage and safety, take a look at the docs.

Producing a COM component

Producing a COM component is relatively complicated compared to consumption, due to the many features available that we must support. Here, we will walk you through producing one of our examples, the BritishShortHairCat.

  1. Define the class containing all the user fields you want.
  • Specify each of the interfaces the class implements. You must list the interface's parent interface in paraenthesis with the exception of IUnknown which is assumed when no parent is specified (e.g., : MyInterface(MyParentInterface(MyGrandParentInterface))), MyOtherInterface
  1. Implement the necessary interfaces on the class.
use com::class;

com::class! {
    pub class BritishShortHairCat: ICat(IAnimal), IDomesticAnimal(IAnimal) {
        num_owners: u32,
    }
    
    impl IDomesticAnimal for BritishShortHairCat {
        fn Train(&self) -> HRESULT {
            println!("Training...");
            NOERROR
        }
    }
    
    impl ICat for BritishShortHairCat {
        fn IgnoreHumans(&self) -> HRESULT {
            println!("Ignoring Humans...");
            NOERROR
        }
    }
    
    impl IAnimal for BritishShortHairCat {
        fn Eat(&self) -> HRESULT {
            println!("Eating...");
            NOERROR
        }
    }
}

For more information on usage and safety, take a look at the docs.

Safety

While COM specifies details about the ABI of method calls, it does little in terms of guranteeing the safety of those method calls. As such, it is left up to the programmer to verify the safety of COM APIs and to write safe wrappers for those APIs.

You can read more about what gurantees this library makes in the guide to safety.

Existing crates

There are many existing Rust crates that help with COM interactions. Depending on your use case, you may find these crates more suited to your needs. For example, we have

  • Intercom, which focuses on providing support for writing cross-platform COM components in Rust.
  • winapi-rs, which provides a straightforward macro that allows you to easily consume COM interfaces.

Building

This library is Windows only, so the easiest way to contribute will be on a Windows machine. You can execute the examples like so:

cd examples\basic
cargo run --release

If you are on a Mac or Linux machine, you should still be able to make changes and check that they compile by running the following from the root of the project:

cargo check --target=x86_64-pc-windows-msvc

Contributing

For further information on contributing, please take a look at the contributing doc

Code of Conduct

This project has adopted the Microsoft Open Source Code of Conduct. You can find out more in the code of conduct doc.

FAQ

Is there IDL support?

As a foundation, we are attempting to create a library that doesn't necessarily rely on having an IDL file. However, it is in the pipeline for future improvements. We will have a command-line tool that will parse the IDL into the required macros.

Is there out-of-process COM support?

Currently, we only support production of in-process COM components. Also, production of a COM component can only be in the DLL format. There will be plans to enable out-of-process COM production as well as producing in the .EXE format.

com-rs's People

Contributors

adrianwithah avatar basixkor avatar fkaa avatar laegluin avatar m-ou-se avatar marijns95 avatar mcgoo avatar microsoft-github-policy-service[bot] avatar microsoftopensource avatar mominul avatar msftgits avatar nelsonjchen avatar plecra avatar rantanen avatar richardhozak avatar russcam avatar rylev avatar sivadeilra avatar snf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

com-rs's Issues

Consider moving the class factory generation from [co_class] to inproc_dll_module

I was looking into adding support for generic types into [co_class] attribute to enable easy way for a generic class factory implementation (for #32). Ironically this causes problems, because it's impossible to implement a class factory for such classes.

Ignoring the generic type support, The use case for [co_class] without a class factory is COM classes that the library wants to implement for the purpose of implementing an interface but which do not need to be available for construction through CoCreateInstance (they don't even need to have CLSID).

Currently com-rs is a bit ambiguous in this situation with part of the class factory infrastructure being implemented in [co_class] while the CLSIDs aren't defined until the class is exposed through inproc_dll_module.

Although excluding the generic type scenario, the current co_class-generates-class-factory implementation doesn't prevent implementing co_classes that are never intended to be constructed by a COM client. The class objects are just a bit unneeded in such case. So the only scenario that I can think of, which is blocked by the current implementation, is the generic type support.

And even that can technically be implemented by conditionally avoiding the class object generation if there are type parameters in the struct.

Production macros scope

Currently, developers will have to manually import all the gen_vtable macros for each interface in the inheritance hierarchy. This needs to be improved. #39

Are [out] pointers safe or unsafe to read from the caller

The problem is reduced to if we can trust the callee with initializing the [out] parameters.

The two possible routes are:

  1. Trust that the callee initializes all the [out] parameters if it returns SUCCESS
  2. Never trust that the callee has initialized the [out] parameters

If we go with (1), we can add a safe unwrap method to ComOutPtr that is safe to call. Otherwise, reading any output parameter will be unsafe causing some ergonomic issues.

Consider replacing `extern "stdcall" fn` with `extern "system" fn` where appropriate

On Win32 targets extern "system" is the same as extern "stdcall", whereas on MacOS/Linux targets it's the same as extern "C". The reason behind this change is that there are cross platform COM APIs in the wild (eg, VST3 plugins for audio apps) that use the native calling convention, not stdcall.

Another option: use a feature gate and default to stdcall, in case for some reason people want to use the COM paradigm with a different calling convention outside of Windows.

Receiving COM interface pointers as parameters

The following use case:

// C++ COM client

CComPtr<ILogic> logic = ...;
CComPtr<IData> data = ...;
logic.DoLogic(data);  // C++ holds reference count. This is essentially "a borrow".
// Rust COM server

impl ILogic for CoLogic {
    fn do_logic(&self, data: *mut c_void) {
        // Safety requirements for InterfacePtr::new.
        // - ptr is valid pointer of 'T'
        // - ptr must live as long as InterfacePtr
        let ptr = unsafe {
            InterfacePtr<IData>::new(data);
        }

        // Rust wants ownership and turns the 'ptr' into a Rc.

        // This should increment reference count (but doesn't currently).
        let rc = InterfaceRc::new(data);

        // This would increment reference count, but is not implemented for InterfacePtr.
        let rc = InterfacePtr::get_interface<IOtherData>(ptr);
    }
}

So couple of needs:

  • Provide unsafe "InterfacePtr into InterfaceRc without add_ref" method.
    • Currently InterfaceRc::new does this, but isn't marked as unsafe, thus leading to easy double-release, if the user forgets to manually call add_ref.
  • Provide safe "InterfacePtr into InterfaceRc with add_ref" method.
    • As long as InterfaceRc does add_ref, there should be no other safety concerns, assuming the pointer itself is valid (which InterfacePtr seems to imply based on the safety requirements on its new method).
  • Implement get_interface on InterfacePtr.
    • Currently the user must turn any pointer into Rc type just to call get_interface

Maybe look into implementing a lifetime scoped variant of InterfacePtr:

#[repr(transparent)]
ScopedInterfacePtr<'a, TInterface> {
    ptr: *mut c_void,
    phantom: &'a PhantomData<TInterface>
}

This should prevent accidentally moving the pointer somewhere that would outlive the function call.

Interfaces can still be accessed after the runtime has been uninitialized

Continuing conversation that started in #97, it is currently possible to call interface methods after CoUninitialize has been called. It is uncertain whether this is actually unsafe or whether the program will be properly aborted once an interface method is actually called.

One possible solution is to tie the lifetime of the runtime to the lifetime of each interface but this makes the crate much less ergonomic. One other solution is to get rid of the explicit runtime struct and simply expose an unsafe CoUninitialize function that requires the user verifies that no active interfaces are still alive when it is called.

On top of this other constructs could be built that make things easier to handle but might require some sort of runtime cost (e.g., a reference counter construct that only call CoUninitialize when all interface references have been dropped).

co_class does not properly generate struct

When I try to create COM component that has multiple user defined fields, then the generated code does not properly initialize it.
I tested this with published com crate (version 0.1) and master on git (sha 25851eedad035239062f07f4016126bb073821ab), both do not work.

My code is:

use com::{co_class, interfaces::iunknown::IUnknown};

#[co_class(implements(IUnknown))]
struct Test {
    field1: u32,
    field2: u32,
}

impl Test {
    fn new() -> Box<Self> {
        Test::allocate(0, 0)
    }
}

fn main() {
    let _ = Test::new();
}

When I delete field2 and just call Test::allocate(0), everything works.
I ran this through cargo expand to see the generated code, the output can be seen here.
The code where it should be generated is on line 19 here, that's where Test gets constructed.
I also tried making the fields public, does not work either.

In contrast, there's the expanded code which has one field, correctly generated and field correctly initialized on line 18.

You can find the complete example in repository here.

I found this bug as part of a different project, I am trying to consume mshtml WebBrowser2 COM component, project can be found here.

Currently unable to make query_interface work in multiple inheritance

The current implementation of query_interface always assumes that they are receiving a pointer to the first vptr of the object. This may not always be the case. In AddRef and Release, the cast to the object will fail if the RawIUnknown passed in is a pointer to the second vptr.

String IID does not compile

Cargo.toml dependencies:

[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winuser", "winreg", "winerror", "winnt"] }
com = "0.1.0"

main.rs:

use com::{com_interface, interfaces::iunknown::IUnknown};
use winapi::um::winnt::HRESULT;

#[com_interface("EFF8970E-C50F-45E0-9284-291CE5A6F771")]
pub trait IAnimal: IUnknown {
    fn eat(&self) -> HRESULT;
}

fn main() {
    println!("Hello, world!");
}

Result:

error: custom attribute panicked
 --> src\main.rs:4:1
  |
4 | #[com_interface("EFF8970E-C50F-45E0-9284-291CE5A6F771")]
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = help: message: IIDs must be exactly 36 characters long

Note: Removing the quotes from the GUID in the com_interface attribute causes it to compile successfully.

ComOut content

Tracking issue for ComOut improvements

Currently, ComOut exposes an unsafe trait ComTrivialStruct in order to impose certain restrictions on types it can handle.

  • Structs that are written into the memory referenced by a ComOut has to be readable from other languages. Thus, it is necessary for them to have repr(c), to ensure consistent memory layout.

  • Secondly, in accordance to the COM specifications, [out] and [in, out] in particular have to use the COM allocator for embedded pointers (pointers inside a struct). This trait is marked as unsafe in order to signal to the implementer of this specification. In due time, we could abstract away this knowledge once ComOut is able to handle such cases.

Improvements to the API

Progress on this crate has been fairly quite as we've been focusing on getting winrt-rs in working order. Now that that crate has solidified somewhat, it's time to return to this crate with the lessons learned from winrt. The following is a suggestion on the way we will proceed forward based on conversations I've had with @kennykerr. Please let us know if you have any feedback.

Traits vs Structs

Currently, COM interfaces are modeled as traits that must be used in conjunction with a ComPtr (for non-reference counting) or ComRc (for reference counting). This has certain advantages and disadvantages:

Pros

  • a familiar syntax for defining COM interfaces
  • built in support for ensuring that all methods are implemented for a given interface when building a COM server

Both Pro and Con

  • requires use of dyn keyword when dealing with interfaces. This is good in that it's a gentle reminder the user is performing dynamic dispatch, but it is extra syntax when presumably the user knows COM is dynamic dispatch.

Cons

  • extra syntax when taking a normal COM pointer as an argument: ComPtr<dyn IAnimal> vs IAnimal
  • strengths are for the less common case of producing COM interfaces rather than consuming
  • Not how COM works in other languages and thus might be slightly confusing to learn.

While switching to structs would not only be wins, there are enough here that it seems like the correct thing to do.

This would require a new syntax for defining COM interfaces. Something like the following:

#[com_interface("00000000-0000-0000-C000-000000000046")]
interface IUnknown {
    fn query_interface(
        &self,
        riid: *const IID,
        ppv: *mut *mut c_void
    ) -> HRESULT;
    fn add_ref(&self) -> u32;
    fn release(&self) -> u32;
}

and for implementing those interfaces:

#[com_interface_impl]
impl IDomesticAnimal for BritishShortHairCat {
    unsafe fn train(&self) -> HRESULT {
        println!("Training...");
        NOERROR
    }
}

Consumption of those interfaces would then look like this:

let factory = get_class_object(&CLSID_CAT_CLASS).unwrap();
println!("Got cat class object");

let unknown = factory
  .query_interface::<IUnknown>()
   .unwrap()
println!("Got IUnknown");

let animal = unknown
  .query_interface::<IAnimal>()
  .unwrap();
println!("Got IAnimal");

unsafe { animal.eat() };

Nullability

Currently ComPtr is used only for non-null pointers.

Pros

  • no null checks required for the user when the user has a ComPtr meaning they can never make a mistake and use a ComPtr that has not actually been initialized. This is the very reason Option<T> exists in Rust.

Cons

  • null COM pointers are used in many common API patterns including out params. Currently this requires the user to first initialize a raw pointer, pass that pointer as the out param and then initialize the ComPtr struct with the raw pointer which can be awkward.

This is being discussed already in #141

Method names

Currently there is a safe variant of query_interface that is called get_interface. The name difference is due to query_interface already existing and there being no method overloads. This is perhaps confusing, but the only two choices are to give the safe variant a different name (as is the case now), or give the unsafe variant a different name.

Consider writing GUIDs as strings in attribute macros

#[com_interface(EFF8970E-C50F-45E0-9284-291CE5A6F771)]
pub trait IAnimal: IUnknown { โ€ฆ }

This makes me uncomfortable. Iโ€™m half-expecting a formatter to reformat the GUID as EFF8970E - C50F -45E0 -9284 -291CE5A6F771 or similar (rustfmt wonโ€™t now but could conceivably in the future), and some syntax highlighters will highlight parts of it differently, e.g. โ€œ9284โ€ as a number but the rest not. Also, if lower-case hex is used (no idea whether itโ€™s supported in theory or not), then some GUIDs will fail to tokenise, e.g. segments 0bd5 and 1ef3 wonโ€™t compile (invalid binary literal and invalid exponent, respectively).

I would prefer the GUID to be a string:

#[com_interface("EFF8970E-C50F-45E0-9284-291CE5A6F771")]
pub trait IAnimal: IUnknown { โ€ฆ }

#[com_interface = "โ€ฆ"] would also work.

"Module not found" error when using the Rust COM class

I'm getting an error when I try to create an instance of a class using Automation which as far as I can tell should be possible with what I've provided so far. Because the com-rs does not support IDispatch, we have to make sure we can do everything with IUnknown.

When I new it up, I get the error:

-2147024770   (0x8007007E)
Automation error
The specified module could not be found. 

I've verified that I can inspect the type library created, and it can be loaded. However, the ability to inspect the type library says nothing about the instantiation or marshaling. I've tried my best to follow the examples provided in the /examples, though the part about IClassFactory is little uncertain to me.

Here are the files I used to create a "M"VCE:

Cargo.toml

[package]
name = "test"
version = "0.1.0"
authors = ["test"]
edition = "2018"

[lib]
name = "test"
crate-type = ["cdylib"]

[dependencies]
com = "0.2.0"
winreg = "0.7"

[build-dependencies]
embed-resource = "1.3.3"

lib.rs

use com::{IID, co_class, com_interface, interfaces::iunknown::IUnknown, interfaces::iclass_factory::IClassFactory, sys::HRESULT, sys::NOERROR};

// {7A6D2346-8870-456D-A528-F4D20E2806B1}
pub const CLSID_TEST: IID = IID {
    data1: 0x7a6d2346,
    data2: 0x8870,
    data3: 0x456d,
    data4: [0xa5, 0x28, 0xf4, 0xd2, 0xe, 0x28, 0x6, 0xb1]
};

#[com_interface("3BE9ED6B-E820-4A76-BBDC-59BB508396F7")]
pub trait ITest: IUnknown {
    unsafe fn ping(&self, result: u32) -> HRESULT;
}

#[com_interface("3BE9ED6B-E820-4A76-BBDC-59BB508396F7")]
pub trait ITestClass: IClassFactory {}

#[repr(C)]
#[co_class(implements(ITest))]
pub struct Test { }

impl ITest for Test {
    unsafe fn ping(&self, mut result: u32) -> HRESULT {
        result = 1;
        NOERROR
    }
}

impl Test {
    pub(crate) fn new() -> Box<Test> {
        Test::allocate()
    }
}

//com::inproc_dll_module![(CLSID_TEST, Test),];

#[no_mangle]
pub extern "stdcall" fn DllGetClassObject(class_id: *const com::sys::CLSID, iid: *const com::sys::IID, result: *mut *mut std::ffi::c_void) -> com::sys::HRESULT {
    let class_id = unsafe { &*class_id };
    if class_id == &CLSID_TEST {
        let mut instance = <Test>::get_class_object();
        com::registration::initialize_class_object(instance, iid, result)
    } else {
        com::sys::CLASS_E_CLASSNOTAVAILABLE
    }
}

#[no_mangle]
pub extern "stdcall" fn DllRegisterServer() -> com::sys::HRESULT {
    use winreg::*;
    use winreg::enums::HKEY_CURRENT_USER;

    let hkcu = RegKey::predef(HKEY_CURRENT_USER);

    let (key, _disp) = hkcu.create_subkey(&"Software\\Classes\\CLSID\\{7A6D2346-8870-456D-A528-F4D20E2806B1}").expect("error creating CLSID");
    key.set_value("", &"Test").expect("error creating CLSID default value");
    
    let (key, _disp) = hkcu.create_subkey(&"Software\\Classes\\CLSID\\{7A6D2346-8870-456D-A528-F4D20E2806B1}\\InprocServer32").expect("error creating CLSID\\InprocServer32");
    key.set_value("", &"test.dll").expect("error creating CLSID\\InprocServer32 default value");
    key.set_value("ThreadingModel", &"Both").expect("error creating CLSID\\InprocServer32 ThreadingModel value");
    
    let (key, _disp) = hkcu.create_subkey(&"Software\\Classes\\Interface\\{3BE9ED6B-E820-4A76-BBDC-59BB508396F7}").expect("error creating IID");
    key.set_value("", &"IClient").expect("error creating Interface default value");

    let (key, _disp) = hkcu.create_subkey(&"Software\\Classes\\Interface\\{3BE9ED6B-E820-4A76-BBDC-59BB508396F7}\\ProxyStubClsid32").expect("error creating Interface\\ProxyStubClsid32");
    key.set_value("", &"{00020424-0000-0000-C000-000000000046}").expect("error creating Interface\\ProxyStubClsid32 default value");

    let (key, _disp) = hkcu.create_subkey(&"Software\\Classes\\Interface\\{3BE9ED6B-E820-4A76-BBDC-59BB508396F7}\\TypeLib").expect("error creating Interface\\TypeLib");
    key.set_value("", &"{{7899DEB3-6D4F-4520-9CF0-ED0D24E6D0CC}}").expect("error creating Interface\\TypeLib default value");
    key.set_value("Version", &"1.0").expect("error creating Interface\\TypeLib Version value");
    
    let (key, _disp) = hkcu.create_subkey(&"Software\\Classes\\TypeLib\\{{7899DEB3-6D4F-4520-9CF0-ED0D24E6D0CC}}").expect("error creating TypeLib");
    key.set_value("", &"{{7899DEB3-6D4F-4520-9CF0-ED0D24E6D0CC}}").expect("error creating TypeLib default value");
    
    let (key, _disp) = hkcu.create_subkey(&"Software\\Classes\\TypeLib\\{{7899DEB3-6D4F-4520-9CF0-ED0D24E6D0CC}}\\1.0").expect("error creating TypeLib\\Version");
    key.set_value("", &"Test").expect("error creating TypeLib\\Version default value");

    let (key, _disp) = hkcu.create_subkey(&"Software\\Classes\\TypeLib\\{{7899DEB3-6D4F-4520-9CF0-ED0D24E6D0CC}}\\1.0\\0").expect("error creating TypeLib\\Version\\Locale");
    
    let (key, _disp) = hkcu.create_subkey(&"Software\\Classes\\TypeLib\\{{7899DEB3-6D4F-4520-9CF0-ED0D24E6D0CC}}\\1.0\\0\\win64").expect("error creating TypeLib\\Version\\Locale\\Platform");
    key.set_value("", &"C:\\Users\\test\\test\\target\\debug\\test.dll").expect("error creating TypeLib\\Version\\Locale\\Platform default value");

    let (key, _disp) = hkcu.create_subkey(&"Software\\Classes\\TypeLib\\{{7899DEB3-6D4F-4520-9CF0-ED0D24E6D0CC}}\\1.0\\FLAGS").expect("error creating TypeLib\\Version\\Locale\\FLAGS");
    key.set_value("", &"0").expect("error creating TypeLib\\Version\\Locale\\FLAGS default value");

    NOERROR
}

#[no_mangle]
pub extern "stdcall" fn DllUnregisterServer() -> com::sys::HRESULT {
    use winreg::*;
    use winreg::enums::HKEY_CURRENT_USER;

    let hkcu = RegKey::predef(HKEY_CURRENT_USER);
    
    hkcu.delete_subkey_all(&"Software\\Classes\\CLSID\\{7A6D2346-8870-456D-A528-F4D20E2806B1}").expect("unable to delete CLSID registry branch");
    hkcu.delete_subkey_all(&"Software\\Classes\\Interface\\{3BE9ED6B-E820-4A76-BBDC-59BB508396F7}").expect("unable to delete Interface registry branch");
    hkcu.delete_subkey_all(&"Software\\Classes\\TypeLib\\{{7899DEB3-6D4F-4520-9CF0-ED0D24E6D0CC}}").expect("unable to delete TypeLib registry branch");

    NOERROR
}

Note: I commented out the com::inproc_dll_module![(CLSID_TEST, Test),]; as it does not seem to work as expected. It would build, but when I run regsvr32 I don't get the successful message nor are the registry entries created. Besides, I needed to use HKCU, not HKLM as the macro would have used. Furthermore, uncommenting the inproc_dll_module and commenting my manual implementations of Dll*** methods does not work around the module not found error.

test.idl

[
  uuid(7899DEB3-6D4F-4520-9CF0-ED0D24E6D0CC),
  version(1.0),
  helpstring("Test Type Library")
]
library test
{
    importlib("stdole2.tlb");

    [
        oleautomation,
        uuid(3BE9ED6B-E820-4A76-BBDC-59BB508396F7)
    ]
    interface ITest : IUnknown {
        HRESULT _stdcall Ping([out, retval] long* result);
    };

    [
        uuid(7A6D2346-8870-456D-A528-F4D20E2806B1)
    ]
    coclass Test {
        [default] interface ITest;
    }
}

At the moment, the IDL file is hand-crafted but it should be compatible with the implementation. Furthermore, I manually run via command line midl /win64 test.idl to compile it into a test.tlb which is later used in the RC script below.

lib.rc

1 TYPELIB "typelib/test.tlb" 

build.rs

extern crate embed_resource;

fn main() {    
    embed_resource::compile("src/lib.rc");
}

As far as I can tell, this part where we embed the type library into the resulting DLL seems to work well. the OLE viewer is able to load and read the type library.

I should also note that I tried adding breakpoint to the Visual Studio in the DllGetClassObject and it was never hit.

According to rustup, I'm using stable-x86_64-pc-windows-msvc (default). I understand that means this is 64-bit implementation. Originally, I did forgot to include the /win64 flag when running the midl compiler. I've checked on that but that did not change the error at all. The metadata is still readable, however.

For the purpose of testing the Automation, I'm using 64-bit Excel with the following VBA code with the reference set to the generated DLL:

Public Sub x()
  Dim t As test.test
  Set t = New test.test
End Sub

The error is raised on the line Set t =....

Make the CI public

The Azure Pipelines CI is currently private, which makes CI errors difficult to resolve.

#107 is building just fine without warnings or errors on my machine, but failing in Azure Pipelines. I'm sure it's some stupid mistake on my part somewhere, but I can't work out what.

Non-reference counted interface pointers

The current implementation of aggregation utilises non-reference counted interface pointers. We will need to find a way to wrap this in a safe manner. For now, theres a memory leak involved in creating ComPtrs and then forgetting them.

Wrap registration in transaction

Currently the registration code does not happen in a transaction. If any of the code fails, the registry can end up in an invalid state. Using transactions can fix this.

Using com-rs on Windows 7 (CoIncrementMTAUsage)

I recently discovered CoIncrementMTAUsage is only available on Windows 8.0 and up itchio/itch#2454

I don't mind finding another solution on my own but my worry is that even then, any application that uses com-rs simply won't load on WIndows 7 because of the missing symbol.

Which means I'd have to maintain a fork that does not rely on CoIncrementMTAUsage - so I was just wondering if it could be behind a cargo feature, perhaps part of the default feature set?

I'm going to test my theory that CoIncrementMTAUsage indeed does not exist on Windows 7 and that a com-rs program as-is will never build with it, I'll update this issue with my findings, but I'd love to hear about someone who knows a lot more about COM than me on this.

LockServer

Currently, IClassFactory::LockServer is currently unimplemented for all our generated class factories. This should be implemented even for in-process servers.

It's primary purpose is to ensure that the server DLL is not unloaded prematurely, while clients still hold references to objects from the DLL. This usually occurs when clients call CoFreeUnusedLibraries

As it stands now, this premature unloading will never happen, as we do not expose DllCanUnloadNow for our in-process DLLs, meaning our DLL can only be unloaded when the client calls CoUninitialize. (this behaviour is stated here)

However, implementing this is crucial as it will allow developers to be efficient with memory usage.

Example for creating objects locally and returning these by interface

I was trying to look through the examples, but couldn't find a way to implement something similar to:

https://github.com/Rantanen/intercom/blob/15d8af0d22114ebf619513293b79dfa1e7613973/test/testlib/src/return_interfaces.rs#L18-L20

I got as far as constructing the CreatedClass with CreatedClass::allocate(..), but got stuck turning it into ICreatedClass in a safe way. The only approach I found was going through the raw query_interface API, such as:

let s = CreatedClass::allocate(..);
s.add_ref();
let rc = unsafe {
    let mut ptr : *mut winapi::ctypes::c_void = std::ptr::null_mut();
    s.query_interface(
        &IID_ICREATEDCLASS as *const _,
        &mut ptr as *mut _,);

    com::InterfaceRc::<dyn ICreatedClass>::new(
        unsafe { com::InterfacePtr::new(ptr) }
    )
};

Encountered this as part of my benchmarking effort for Intercom. Current results show Intercom as an order of magnitude slower for simple method calls, with com-rs managing four COM calls (and their simple implementation) in 10 ns/iteration while Intercom spends 66 ns/iteration on them (135ns/iter after introducing logging, but not using it). Makes me re-consider whether I should put logging in Intercom behind a non-default feature.

Then again, a struct that does nothing but set/add/get a u32 value into a Cell<u32> is a bit extreme benchmark... :)

Passing com interfaces through ffi

I have c/c++ code that I want to convert gradually to Rust, the C/C++ code uses windows headers and uses com interfaces heavily, how do you pass compatible pointers to com interfaces between Rust and C/C++? When I try to do this the naive way, just passing *mut dyn IOleObject through ffi boundary, then rustc warns that interface is not ffi safe:

warning: extern block uses type dyn mshtml::IOleObject which is not FFI-safe: trait objects have no C equivalent

Everything still compiles but my program crashes.

How do you do this correctly?

Non consistent interface method names

I am trying to use com-rs with system provided com interfaces, but the interface methods are not consistently named.

For example there are methods named:
GetUserClassID
get_Name
put_Visible
QueryStatusWB

As i see in com-rs, I need to define the interfaces in Rust with snake case and they get converted to camel case with com_interface macro, so they match their c/c++ counterparts. But if the snake case method would get converted as they are, they would not match:
get_user_class_id would get converted to GetUserClassId and not GetUserClassID
get_name would get converted to GetName and not get_Name
and so on...

Is there something I am missing? Does it even matter how they get generated?

Change winapi references in proc macros to use the exported com::_winapi

The proc macros currently depend on the user crate specifying winapi as a dependency and things like NOERROR being in scope. com-rs already exports winapi as com::winapi_. The use of winapi-types in the proc macro expansion should be fixed to use absolute paths to the types through the re-exported path.

Crate Safe Wrapper for CoGetInstanceFromFile

I would like to use COM to create a plugin system for my project, and I was considering using this crate. My plan is to have users drop plugins within a directory, so I will need to be able to load class objects by path and query them for relevant interfaces. I would like to see a method added to ApartmentThreadedRuntime in order to do so. Ideally, this method would accept pointer to self and a Path.

EDIT: It appears that I misunderstood the function of CoGetInstanceFromFile; I thought that it was a way of getting class objects by path. Rather, it seems to be used for object persistence. My apologies.

COM trait methods should probably always be unsafe

#[com_interface(00000000-0000-0000-C000-000000000046)]
pub trait IUnknown {
    unsafe fn query_interface(
        &self,
        riid: winapi::shared::guiddef::REFIID,
        ppv: *mut *mut winapi::ctypes::c_void
    ) -> winapi::shared::winerror::HRESULT;
    fn add_ref(&self) -> u32;
    unsafe fn release(&self) -> u32;
}

This way of declaring COM interfaces is really convenient, but IMO it provides a wrong feeling of safety. An erroneous COM server may cause memory errors, even in simple functions like add_ref. Further I would argue this functions are all special kinds of FFI calls (using the COM ABI), so like every normal FFI function they should always be unsafe. Currently the easiest way to use the com_interface macro (to not use unsafe at all) is probably the most dangerous one.

This extends to the com::InterfaceRc type. I think constructing this struct should also be unsafe. The user should be forced to verify the called COM server or signal their trust by constructing this type within an unsafe block.

This would lead to a less convenient usage, but I think this would still be a better approach, because most of the time the COM interfaces will probably not be used directly in idiomatic Rust code. Most of the time their will probably be behind another layer of abstraction and I think this should also be the layer providing the safe interface for the COM types.

However I see that especially in cases where COM is used as a system for plugin-like components it is in general impossible to verify the correctness, so their is probably a practical compromise needed.

Aggregation Design improvements

Currently, the aggregation API is not as idiomatic. Some problems include:

  • Ability to get the aggregated object's interface pointer is not yet implemented.

  • If an aggregate exposes IAnimal, ICat, IDomesticAnimal, the user is exposed to 3 different set_aggregate functions. Ideally we want to cut down to 1 per aggregated object.

  • The top level macro #[co_class] and #[aggr_co_class] must be the first macro defined, before any com_implements or aggr attributes. Ideally, we want to combine all into a single macro.

  • Create safe wrapper for instantiating an aggregated object through CoCreateInstance. Currently, there's no way of obtaining the existing Runtime instance.

  • Store aggregated object as a ComPtr, instead of a *mut IUnknownVPtr

Edit: Adding on from #58 , it is not immediately obvious what you need to pass as the second type parameter in create_aggregated_instance.

Make user defined functions safe by default

Ideally, we should make the user defined functions safe by default when implementing a COM interface. By doing this, we can leverage Rust's static analyzer on the code.

Full disclosure: I haven't thought about how to implement this but didn't want to forget.

Using com-rs on a non-windows platform

I'd very much like to use the "COM-lite" interface of another C++ library on macOS, but it seems this crate hasn't taken other OS'es into account besides Windows.

Namely, when I try to compile the IAnimal interface example given on the documentation landing page, I'm running into linking issues:

#[com::com_interface("EFF8970E-C50F-45E0-9284-291CE5A6F771")]
pub trait IAnimal: com::interfaces::IUnknown {
    unsafe fn eat(&self) -> com::sys::HRESULT;
}
  = note: ld: library not found for -lole32
          clang: error: linker command failed with exit code 1 (use -v to see invocation)
          

error: aborting due to previous error; 1 warning emitted

I understand OLE is a big part of COM on Windows, but I'd prefer it if I could use just a subset of this crate that does just vtable generation and interfacing.

ApartmentThreadedRuntime calls CoUninitialize even if CoInitialize failed

com-rs/src/runtime.rs

Lines 34 to 39 in 3a3d96b

if hr != S_OK && hr != S_FALSE {
// `runtime` is dropped here calling `CoUninitialize` which needs to happen no matter if
// `CoInitializeEx` is successful or not.
// https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-couninitialize
return Err(hr);
}

The comment implies that allowing the Drop implementation to call CoUninitialize is intentional. It links to the documentation of CoUninitialize, which has the following statement (emphasis mine):

A thread must call CoUninitialize once for each successful call it has made to the CoInitialize or CoInitializeEx function, including any call that returns S_FALSE.

This seems to indicate that CoUninitialize should be called only if CoInitialize was successful, which contradicts the comment and the implementation.

Generation of Class Objects

As it stands now, users will need to write another struct as the Class Object (BritishShortHairCatClass, etc.) which implements IClassFactory. This class object should be automatically generated, as it has a standard IUnknown implementation as well as deterministic IClassFactory implementation (once we know whether its aggregable/aggregates/normal)

co_class cannot implement more than three interfaces

When I try to implement more than three interfaces like this:

#[co_class(implements(IOleClientSite, IOleInPlaceSite, IStorage, IDispatch))]
struct WebBrowser { }

I get this error:

error: no rules expected the token `3usize`
  --> src\main.rs:64:1
   |
   | #[co_class(implements(IOleClientSite, IOleInPlaceSite, IStorage, IDispatch))]
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no rules expected this token in macro call

I thought the error would be in offset.rs, so I tried changing

declare_offset!(Zero => 0, One => 1, Two => 2);

to declare_offset!(Zero => 0, One => 1, Two => 2, Three => 3); with no success.

Help: Implementing Windows Shell Extension (IThumbnailProvider)

I'm attempting to implement IThumbnailProvider, but with limited knowledge of Rust FFI and C/C++ interop.

  1. When creating a new COM server, do I need to create a GUID for it? See example:
// Create GUID (bit of a hack), using `guid` crate for macro.
const CLSID_ICNSPROVIDER: com::IID = com::IID {
    data1: guid! {"1abaf814-4632-41bd-a528-88deac2ff752"}.Data1,
    data2: guid! {"1abaf814-4632-41bd-a528-88deac2ff752"}.Data2,
    data3: guid! {"1abaf814-4632-41bd-a528-88deac2ff752"}.Data3,
    data4: guid! {"1abaf814-4632-41bd-a528-88deac2ff752"}.Data4,
};

// Registration.
inproc_dll_module![(CLSID_ICNSPROVIDER, ICNSProvider),];

// Implement the interface for ICNS icons (MacOS png container).
#[co_class(implements(IThumbnailProvider))]
pub struct ICNSProvider {}

impl ICNSProvider {
    fn new() -> Box<Self> {
        ICNSProvider::allocate()
    }
}
  1. When defining the interfaces, how do I refer to traits? For example with IStream, it refers to itself in the copy method:
#[com_interface("0000000c-0000-0000-C000-000000000046")]
pub trait IStream: IUnknown {
    unsafe fn clone(ppstm: dyn IStream) -> HRESULT;
    unsafe fn commit(commit_flags: DWORD) -> HRESULT;
    unsafe fn copy_to(
        ppstm: dyn IStream,
        cb: ULARGE_INTEGER_u,
        pcb_read: *mut ULARGE_INTEGER_u,
        pcb_written: *mut ULARGE_INTEGER_u,
    ) -> HRESULT;
    unsafe fn lock_region(
        lib_offset: ULARGE_INTEGER_u,
        cb: ULARGE_INTEGER_u,
        lock_type: DWORD,
    ) -> HRESULT;
    unsafe fn revert() -> HRESULT;
    unsafe fn seek(
        dlib_move: LARGE_INTEGER,
        dw_origin: DWORD,
        plib_new_position: *mut ULARGE_INTEGER_u,
    ) -> HRESULT;
    unsafe fn set_size(lib_new_size: ULARGE_INTEGER_u) -> HRESULT;
    unsafe fn stat(pstatstg: *mut STATSTG, grf_stat_flag: DWORD) -> HRESULT;
    unsafe fn unlock_region(
        lib_offset: ULARGE_INTEGER_u,
        cb: ULARGE_INTEGER_u,
        lock_type: DWORD,
    ) -> HRESULT;
}

I get an error: "TraitObject type unhandled!". This error seems to refer to unsafe fn clone(ppstm: dyn IStream) -> HRESULT;.
How am I supposed to reference rust traits in an FFI context? Obviously dyn IStream is not valid.

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.