Implement cross platform API selection
Closed this issue · 5 comments
Current plan
No features, such as winapi
. Features suffer from the inheritance problem with crates. If crate B wants to use Intercom to invoke COM component, written with Intercom, without winapi
and B is a dependency of A that specifies winapi
feature for Intercom then B will end up using winapi
as well.
Instead we will have two separate mod, with (draft) names:
intercom::platform::native
intercom::platform::generic
Effect | ...::native + Windows |
...::native + Linux/OSX/etc. |
...::generic + Any OS |
---|---|---|---|
extern |
"stdcall" |
"C" |
"C" |
BSTR alloc |
SysAllocString* |
Rust heap | Rust heap |
BSTR dealloc |
SysFreeString or #6 |
#6 | #6 |
IErrorInfo |
GetLastError |
??? | <- Same ??? |
ComRc::create() |
CoCreateInstance |
Not available | Not affected by mod |
This allows the Intercom user to specify per interface whether that interface should use the platform conventions or "cross-platform" conventions, which is essentially whatever we need to use on non-Windows platforms anyway as they don't have the COM infrastructure built in.
create()
and CoCreateInstance
We will still have differences like ComRc::create()
that will be only available on Windows. This won't be affected by the chosen intercom::compat
module as ComRc
is defined under the root intercom
module. We could define the create()
as a ComRcExt
trait similar to OsStringExt, but I don't see any reason for this for now.
What makes the create()
-case different from extern
is that create()
is purely new implementation. It doesn't change anything, like extern "C"
does for extern "stdcall"
. As such, cross platform projects can just opt to not use it. Compiling it in on Windows doesn't prevent these projects from working on Linux if they never end up calling it.
For now we'll just #[cfg(windows)] impl
the create()
on ComRc
and be done with it.
Naming
Parent module name?
intercom::compat
intercom::abi
intercom::contract
intercom::platform
(Obviously the mod name wouldn't beplatform
in this case)intercom::convention
intercom::attrs
orintercom::attributes
- being explicit that it's the attributes that are affected by this.intercom::extern
extern
is a reserved word.
Platform module names?
platform
/crossplatform
platform
/neutral
platform
/c
native
/neutral
native
/generic
Currently I'm leaning towards:
use intercom::platform::native::*;
use intercom::platform::generic::*;
Implementation
And then the difficult bits: How does this work technically.
intercom-attributes
For each attribute, we currently have com_whatever
and expand_com_whatever
functions defined. The com_...
is the actual #[proc_macro_attribute]
function, while the expand_...
is the implementation, using -> Result<..>
type to allow quickly aborting with ?
. So even for now we have a thin #[proc_macro_attribute]
function and a fat implementing method.
We need to change this in the following way:
- Introduce
enum Platform { Native, Generic }
- Add the platform as a parameter to
expand_com_...
functions. - Duplicate the
#[proc_macro_attribute] fn com_...
functions forNative
andGeneric
platforms. As we can't change the function name (as that would change the attribute name) we'll do this by introducingpub mod native
andpub mod generic
. These functions will be otherwise identical, save for thePlatform::?
value they pass on to theexpand_com_...
function. - Carry the
Platform
value through to all the bits that need it. At least the following places need it:TyHandler
/BStr
needs to know how to allocate theBSTR
s. This probably must be done inTyHandler
as that's responsible for generating the code for the delegating methods that needs to change. We can't alterBStr
as that is a static type (as opposed to attribute-expanded type) and it isn't part of the platform module.ReturnHandler
/error
needs to know how the error handling is done. We might want to keep the currently static code static, which means we'll end up passing thePlatform
enum over to theerror
module at runtime. This means theenum Platform
needs to be implemented in theintercom
crate as well.
In addition to the attributes, we will probably need to move IUnknown
, etc. into the platform folder as well (which will be fun \:D/). This will actually make all of this more complicated.
Currently we implement IUnknown
through the #[com_interface]
attribute on the trait. However as we will want to keep IUnknown
in the root of the intercom
crate (if at all possible), we might need to remove #[com_interface]
on it and instead declare the virtual tables, delegating methods, etc. by hand in the platform
modules to follow the correct method invocation.
On the other hand as ComRc
/etc. will want to call query_interface
this will end up exploding in our face as now ComRc
starts depending on the platform specific bits and needs to know what platform it is used with.
We might be able to work around this by adding trait ComInterface
, which we impl
on all #[com_interface]
types and this trait defines which IUnknown
the interface inherits from - the one using stdcall
or the one using C
calls.
... This just went from a simple "Just add a feature" into "Touch all parts of attribute expansion and then figure out if we can even finish this". ;_;
IUnknown
the trait needs to be platform specific.
let x = ComRc<IUnknown>;;attach( raw_com_ptr );
x.query_interface(...); // <- ComRc needs to know how IUnknown is called.
// Is it extern "C" or "stdcall"
So I guess we will end up moving IUnknown
to the platform specific module in intercom
. This means that trait ComInterface
will need to include fn platform() -> Platform
member so ComRc
can query the interface for its platform and thus select the correct IUnknown
.
Now the question becomes whether we want platform::common
, which defines IUnknown
trait so that we can do the following:
mod common {
pub trait IUnknown { ... }
}
mod native {
#[com_interface]
pub trait IUnknown : common::IUnknown { ... }
impl common::IUnknown for IUnknown { /* delegates */ }
}
Which would allow us to maybe do things like impl AsRef<common::IUnknown> for ComItf<T>
. This would provide IUnknown
"API" in a way that isn't tied to a specific platform as the Rust code calling this doesn't care about the call conventions.
However this would only be useful in places that handle mixed platforms - which (hopefully) should be limited to Intercom internals, such as ComRc
, ComItf
, etc. We'll see whether this is needed when we tackle the implementation of these types with the new platform specific IUnknown
traits present.
Think this is something we just need to dive into instead of trying to specify all the problems beforehand. I still feel the separate mods approach is the way to go even if a feature option would be more straight forward and simpler (as it changes the call convention in the whole crate instead of per attribute usage).
.. on the other hand, the real question after all of this is:
Do we still want to implement the choice? The reason why this became relevant is unit tests which we would want to result in the same code on both Travis and AppVeyor.
However we could just fix the unit tests in such a way that we specify them with "stdcall" and depending on the host machine, we'll just do string replace for "stdcall" -> "C" before running the comparison if the host is one where we use "C" by default.
It's far from elegant, but it's a lot quicker and our unit tests are the only thing that would ever use intercom::platform::generic
, I don't really see enough reason to complicate the code base because of it.
Conclusion
We will not have "Cross-platform COM" and "Microsoft COM". Instead we will have "Platform COM" everywhere. This means, that unless an actual need arises, we will have:
extern "stdcall"
on Windows - always.extern "C"
on other platforms - always.
Since the binaries are platform specific anyway, as long as we use a consistent call convention on each platform, the call convention shouldn't become a problem.
This leaves things like string allocation and CoCreateInstance
. For this we have specific guidelines for cross-platform code. These guidelines will specify a subset of Intercom functionality that we will guarantee to work on every platform:
BSTR
s are allocated using #6 and they must be de-allocated using #6.- Non-
BSTR
pointer values throughIMalloc
.- In practice this means the COM clients will need to CreateInstance
CLSID_Allocator
or similar type that they can then use to alloc/dealloc any pointers they receive from the same Intercom typelib.
- In practice this means the COM clients will need to CreateInstance
- Do not use
CoCreateInstance
(or::create()
in Rust)
As long as people follow these guidelines, they will be able to write components using Intercom (Intercomponents!) that work cross-platform without code changes.
Note that these guidelines should not prevent us from making these exact same components work well in platform specific environments. For example BSTR
s allocated using #6 does not mean that #6 can't allocate them using SysAllocString
on Windows, which allows the same components to be used with Windows-specific COM clients, such as C# projects.
In addition we can provide CoCreateInstance
based ::create( clsid )
method in Intercom, but using this means the code will work only on platforms that support CoCreateInstance
.
So in short: Instead of having "native COM" and "generic COM" as two differing implementations, our "generic COM" will be a subset of "native COM". In addition we will give up "generic COM" being the same on all platforms. It will have the same API on all platforms, but may have different ABI thanks to compiler differences.
I believe for now we have a rather good understanding of what cross-platform Intercom means, so I'm also closing #23.