rantanen / intercom Goto Github PK
View Code? Open in Web Editor NEWObject based cross-language FFI for Rust
License: MIT License
Object based cross-language FFI for Rust
License: MIT License
There are a pile of expansion tests at intercom-attributes/tests. Currently the tests are validated using string comparison.
String comparison works fine for ensuring the result is expected, but unfortunately it requires the code to match down to whitespace.
The tests are meant to ensure the code is syntactically equivalent. The current code formatting is defined by rustc
's --pretty
option. It would be better to perform the comparison on AST level or after some normalization based on AST. This would allow the target code to be more readable without having to chase the code format style that rustc
results in.
There are few alternatives for implementing this:
syn
and do the equal checking using PartialEq
(==
) on the AST.target
source files to include comments that wouldn't be present in the expanded source.The trade-offs here are between ease of writing code that passes when it should versus ease of figuring out what is wrong when the code fails. The current string comparison using difference
crate makes the errors easy to decipher given the diff-formatting. Pure AST based ==
comparison would result in a rather bad error messages.
There's a stupid amount of internal error types. Ideally we should have one major error type that we expose from our public APIs (currently intercom-build and intercom-utils). All other types should be well managed, consider !crate failure.
CoCreateInstance
isn't that important for us, as the primary Windows interoperability API is winapi-rs
. However there are some Windows COM APIs that we would like to use internally and pulling the whole winapi-rs
as a dependency while we already implement most of the needed bits ourselves feels a bit silly.
The biggest issues for supporting external COM APIs are:
CoCreateInstance
support. This is rather trivial to implement for ComItf::new
for example.SAFEARRAY
(#8) and various string types (#27).Currently _MSC_VER is used as a substitute but it prevents the use of the library with e.g. Clang on Windows.
I was looking into enabling exceptions
and interface_wrappers
tests for Windows and I'm struggling to find a clear way to do this - being a bit afraid of the #import test_lib.dll
and #include "test_lib.h"
conflicting with each other.
While it would be possible to have all of these tests under the same project/solution/whatever you want to call it, I would prefer the clarity of having every test in cpp-raw
come with the same prelude and having the same support classes available to it - instead of opening a random.cpp
file and then having to scan includes to see whether that specific file has stuff like CreateInstance
available to it or whether it should use ClassFactory::create
.
I'm proposing the following:
cpp-raw
InitializeRuntime
, UninitializeRuntime
and CreateInstance
).HRESULT
values, out parameters, etc.#import
, Intercom generated headers with dynamic loading, Intercom generated headers with static lib
.GetErrorInfo
/SetErrorInfo
work on Windows, malloc
works on Linux.cpp-wrapper
intercom-cpp
dependency.BStr
s to wchar_t
arguments (if BStr
s are even supported through intercom-cpp
).cpp-raw
. Including a header shouldn't matter.intercom-cpp
is to work on every platform.
GetErrorInfo
/SetErrorInfo
should be hidden behind a cross platform API under intercom-cpp
which would use those methods for exceptions on Windows while something else would get used on Linux/elsewhere.@Fluxie Opinions?
Currently doing Linux builds will recompile the test lib each time as the headers get re-written even if they are not changing. We should add an option to the CLI tools to do a comparison between input and output and only write the files if they would actually change.
The #[com_library]
should be a crate level attribute. Currently it is not, because I had trouble making it such due to hygiene issues or some such.
Once we get syn 0.12 in, we should try to turn it into a crate level attribute again.
Decided to check the test output on TravisCI. To my surprise Valgrid reported:
==9307== LEAK SUMMARY:
==9307== definitely lost: 672 bytes in 12 blocks
==9307== indirectly lost: 43 bytes in 3 blocks
I was expecting leaks to fail the test run so haven't paid that much attention to memory management. There might be some missing Release
s somewhere. Hopefully that's all we are missing - although I wouldn't be surprised if some of the InterfacePtr
-ComItf
-ComRc
conversions end up leaking references.
We should fix those leaks and then try to get Valgrind to fail the test run if it finds leaks so we will find these issues faster the next time.
Currently the attribute expansion in intercom-attributes
stringifies the tokens and then generates new tokens from the strings. This loses the original span information and causes all error messages to point to the attribute instead of the original source code.
As the attribute expansion doesn't alter the existing items in any way it should be possible to maintain the original tokens in the output. This should in theory maintain the span information and result in better compilation errors.
https://github.com/Rantanen/intercom-site/blob/master/content/docs/types/strings.md
Currently String
is converted to BStr
.
It might be more sensible to reduce the automation in this sense. Perhaps use the following mappings:
BStr
- BSTR
OsString
- WCHAR*
on Windows, char*
on Linux.String
- char*
everywhere. Essentially being std::string
compatible.Eager implement for AsRef
/From
/etc. on BStr
would be needed for this to be sensible.
On Windows BStr
would be allocated with SysAllocString
while everything else would use the CoTaskMemAlloc
APIs. Not sure what the equivalent on Linux would be, if any. The fallback for both systems would be to use the #6 APIs once we get those.
Currently we only support registration free COM. This is mainly because we don't have code to implement the COM registration in registry.
#[com_library]
attribute should expand the DllRegisterServer
and DllUnregisterServer
implementations to write the necessary bits into registry during regsvr32
call.
DllRegisterServer
and DllUnregisterServer
during #[com_library]
expansion.#[com_library]
gains only the class names from the attribute list. If we need interface names as well during the DllRegisterServer
/DllUnregisterServer
then we need the #[com_class]
to expand into DllRegisterServer_Xyz
or similar functions that DllRegisterServer
delegates to.We need some kind of collection support. One option would be implementing COM SAFEARRAY
<-> Rust [T]
slice.
At least the following things should be possible:
// Collection parameters and return value.
fn method( &self, values : [u8] ) -> Vec<bool> { ... }
var arr = list.ToArray();
foreach( var item in foo.Method( arr ) ) { ... }
Even if we need to go with the SAFEARRAY
approach, we don't need to support the full SAFEARRAY
to begin with. We could even make the assumption that the SAFEARRAY
s can only be used in methods that return ComResult
(or similar) and thus the SAFEARRAY
conversion methods can yield E_INVALIDARG
in bad cases to report invalid use of SAFEARRAY
s.
Currently we have three "smart pointers": ComBox
, ComRc
, ComItf
ComBox<T>
Implements IUnknown
for the Rust structs. Contains vtables, reference counts and the Rust struct data. Essentially this is the memory layout of the full COM object we have in memory.
ComRc<T>
Reference countable handle to the ComBox
ComItf<T>
COM interface pointer for which we implement the delegating calls from Rust to COM methods.
ComItf
and ComBox
are clear in their responsibility. ComItf
is a pointer to a COM interface through which we can call the methods on the interface as Rust methods. ComBox
is the heap allocated memory layout for the COM object that is able to reference count itself and clean up if required.
ComRc
however is a bit more tricky. Its primary reason was to hide the ComBox
as an implementation detail. However now that we have two different COM object handles that both would need reference counting, the use of ComRc
needs to be re-evaluated. One option would be to expose the ComBox
as a public API type and instead of having ComRc<Foo>
we'd have ComRc<ComBox<Foo>>
or ComRc<ComItf<Foo>>
. However this results in rather verbose types.
We need to figure out if there is a way to alias both "reference to a ComBox<Foo>
" and "reference to a ComItf<Foo>
" into ComRc<Foo>
? If this isn't possible, then we need to figure out whether we need ComBox
as a public type at all or whether we could always use ComItf
in its place? Essentially merging ComRc
and ComItf
.
We also need to figure out how to get the underlying private data struct from ComItf
. This could be through a private interface that allows us to do something similar to ComItf::get_data::<RustStruct>( com_ptr )
Static linking would be nice in general, but the real reason I want it is supporting kcov on circleci.
Currently circleci has trouble measuring coverage results from our C++ integration tests. I suspect this is due to kcov not catching the dynamically loaded library: SimonKagstrom/kcov#230
An "easy" workaround for us would be to run the unit tests with static linkage for coverage. That would also allow testing dynamic linking on normal tests and static linking during coverage runs. (Of course the dynamic loader wouldn't get coverage results, but at least everything else would).
We could use midl to compile the generated idl into a type library.
Injecting the tlb into the final dll would at least a couple of neat possibilities:
IDispatch
using DispInvoke
and DispGetIDsOfNames
.Currently HRESULT
is just an alias for i32
. We should turn it into struct HRESULT( i32 )
instead. The same applies to RawComPtr
, etc.
This would allow us to define traits, such as From
, IUnknown
, etc. for these.
The current IDL/manifest story is the use of intercom-utils
binary to generate these and then use them in a stand alone fashion.
As we want to embed the IDL (in the form of typelib) and manifest in the final library binary, we would need to generate these two files in build.rs during compilation. This means turning the current intercom-utils
into a library crate that can be invoked from build.rs
.
We will still want to have the command line tool for things like generating cross language bindings (such as the C++ headers) that are not needed during build. We'll probably keep the idl/manifest generation there as well as there might be use cases for these and we'll have the code for that anyway.
(Unless we change manifest generation to use mt.exe
on Windows and base it on the idl. We don't need the manifest on other platforms anyway as far as I can tell.)
build.rs
for the test_lib
that builds the IDL, turns it into a typelib with midl.exe
and embeds this as a resource into the final dll.build.rs
into a new intercom-build
crate.intercom-utils
crate.
intercom
build both a lib and a binary instead.The primary Intercom crate should expose both a bin
and a lib
target. This allow us to get rid of the intercom-utils
crate completely, which would simplify our crates.
We could derive the support for IDispatch
interface when implementing COM interfaces. This would allow using Intercom components from dynamic scripting languages.
Unfortunately this requires a lot of other things, such as support for VARIANT
s, etc. Not something that will be arriving immediately.
Intercom attempts to parse the source code in various places. Both before the actual build in build.rs and during the build through the attributes.
If we fail to parse the source code, currently we panic, which prevents the compilation.
This also prevents any proper error messages.
It would be better to emit a warning/error and ignore the rest of the bits. We'll trust in rustc failing the compilation on its own.
The various #[com_...]
attributes require GUIDs as parameters. These are used to define the various IID_...
, CLSID_...
, etc. global GUIDs.
Currently the user needs to specify them here but they aren't seemingly used anywhere else.
It would be more convenient if the user was able to specify the GUIDs with something like;
#[com_class( AUTO_GUID, IComInterface )]
pub struct Foo { ... }
When the com_class
is expanded (intercom-attributes/src/lib.rs, expand_com_class
), instead of reading the GUID from the attribute as is, the GUID is generated in a deterministic way based on the current crate and the item name.
ie. something along the lines of:
AUTO_GUID => Guid::random_seed( env!("CARGO_PKG_NAME") + current_item_name )
Tasks:
intercom::Guid
.All of this should happen at runtime during attribute expansion or IDL generation. There should be no major changes in the generated code.
Similar to #1, we should allow specifying [com_class] with no CLSID. This would result in a full CoClass, etc. but withotu CLSID the class cannot be constructed through the class factory.
The type can still be used as a return type from functions, etc.
We should surface the ComError better in Rust. This means making it available as the Err-value in ComResult.
Though for performance reasons we will need to either...
The former would result in simpler intercom type system. However if the on-demand stuff happens much later the error message might have been overwritten already. The latter would allow us to resolve the ComError for ComResults immediately and also allow ComError to be send/sync given it would be immutable. (On-demand resolution would prevent send since the error message needs to be resolved in the thread where it happened).
Currently the COM calls are skipping borrow checkers and there are no real safeties against calling multiple &mut self methods in parallel.
We should try to achieve the following:
&self
methods, the underlying type will be ImmutableComClass
which has the type without a RefCell
. This allows more performant calls (no borrow checking). The user can use such types for immutable data or for performance critical mutable data at which point they are responsible for handling the mutable data with their own RefCells.&mut self
, the underlying type will be MutableComClass
which has the type within a RefCell
. This enables runtime borrow checking.This might need us to expand the #[com_class]
attribute with something like #[com_class_mut]
or #[com_class(IReadOnlyInterface, mut IWriteInterface)]
I don't have a preference at the moment. But we should choose one to guarantee uniform style and the ability to generate reasonable documentation.
Alternatives:
Doxygen, Doxygen Bootstrapped
Cldoc
Discussion in Stack Overflow:
https://stackoverflow.com/questions/2323074/doxygen-alternative-for-c
https://stackoverflow.com/questions/30954841/why-is-doxygen-so-unfriendly-to-mobiles
I'm the main author of the winrt-rust crate, which is somewhat closely related to COM (because WinRT is based on COM). There are some interesting relationships with this crate, for example I'm currently also offering a very simple BSTR wrapper (although that's basically never needed in WinRT and probably should not be part of the winrt-rust crate).
With winrt-rust it's currently not possible to write your own components, only consume the WinRT API (using wrapper code generated by a C# tool). Perhaps the necessary glue code for writing custom components could be (partially) shared by intercom and winrt-rust?
Unfortunately I don't really know if there are any actual use cases of interoperability between classic COM and WinRT (because I've never used classic COM), but I think it's worth discussing anyway ;)
#[com_interface(...)]
pub trait T { }
#[com_class(...)]
struct S;
Should result in:
pub IID_T
as T
is pub
priv CLSID_S
as S
is priv
Otherwise we'd need to make everything pub as the (currently) pub functions, vtables, etc. would end up exposing the private items, which Rust will complain about.
Instead of using RawComPtr
everywhere, we should probably have things like RawIUnknownPtr
or just IUnknownPtr
which wraps a RawComPtr
.
This would allow impling traits directly on these pointers. Currently we have ComItf
for this - but strong pointer types would reduce (or remov?) the need for that.
Currently the pointer wrappers: ComItf
and ComRc
have Rust traits as generic parameters but on the low level they carry COM pointers.
This worked while we had one-to-one mapping between Rust traits and COM interfaces. However now that we have multiple type systems our ComItf
might need to point to one of the two COM interfaces. This becomes problematic especially when handling ComItf
/ComRc
as return values/parameters.
Logically when a ComRc
is returned from a Rust method, the the caller would expect a COM interface pointer to the Automation interface of the returned object if the method they received it from uses Automation type system - on the other hand a C++ caller using the Raw type system would expect to receive a Raw type system interface pointer.
This means that the ComItf
we return from Rust needs to be a fat type which stores the object as is and is able to resolve either the automation type system or raw type system when we are converting the Rust return value into raw COM return value in the tyhandlers
.
Currently we have #[cfg(windows)]
and #[cfg(not(windows))]
in few places. This makes it impossible to exercise the non-Windows code when compiling on Windows.
It would be better if these were configured using features instead - this would allow building Intercom on Windows without the dependencies on Windows API and thus achieving the same compilation results as Linux got. Making it less likely to break Linux builds by accident.
Currently things like the IID is available only as a convention of having the IID_
prefix.
We could turn this into a trait instead where the IID of an interface was available through something similar to IInterface::iid_of()
or ( IInterface as IIDOf )::iid_of()
if the former doesn't work.
This would allow us to do all kinds of fancy things, such as providing a strongly typed version of query_interface
. Something similar to:
let new_interface = old_interface.query_interface::<INew>()
// Given
fn query_interface<T : IIDOf>( &self ) -> T {
self.query_interface( T::iid_of() )
}
We could model our exception as follows:
std::logic_error
->intercom::logic_error
std::runtime_exception
-> intercom::runtime_exception
This was the next task I was planning to do after getting the initial wrapper implemented. I saw that you already changed some of the exceptions to runtime_exception. However, I think that at least "No such interface" exception needs to be derived from std::logic_error.
For supporting dynamic_cast-like construct I would prefer a try-method: try_query_interface, for example. Especially in MS-COM calling Queryinterface and checking for possible E_NOINTERFACE is a valid method for checking if a COM object implements certain functionality which makes this feature pretty much mandatory.
stdcall
and C
because of compiler differences and stdcall
being required on Windows.If the binary interface doesn't need to change because of compiler differences, then it shouldn't change. At least there shouldn't be any functional changes in how the parameters and return values are handled. The traits-to-vtables translation logic is the most complex part of Intercom. It would be better if we didn't add more complexity into it by adding more ways to handle how Result<T,E>
s and such are converted from traits to extern-compatible method signatures.
For similar reasons we would keep the basic error values the same. Current S_OK
, E_NOINTERFACE
, E_POINTER
, E_INVALIDARG
and E_FAIL
would stay. Possibly E_NOTIMPL
as well. These are error values that we need for the base interfaces, such as DllGetClassObject
, CreateInstance
and QueryInterface
. Also numeric error codes as return values is a concept that is common for various platforms.
Currently we define some other error codes in addition to these. These include codes like E_ACCESSDENIED
but also STG_E_FILENOTFOUND
, etc. These we might want to make platform specific - maybe even remove the current definitions and rely on the raw OS errors as exposed by Rust. These are needed for converting errors anyway - not something that we rely on internally.
Finally, when it comes to naming, I would still keep referring to the communication protocol as COM. As I mentioned in #18, there is prior art for COM on other platforms in:
However this would be strictly limited to "COM protocol" or "COM interface" instead of COM platform/API/SDK/framework, which might imply runtime services which we do not provide. Also any terminology related to OLE, OLE Automation, Automation, ActievX, etc. should be avoided unless specifically describing compatibility solutions targeting these technologies.
If we accept COM to describe the C++-virtual-table-compatible-binary-protocol, then I don't see why we couldn't keep using Intercom as the crate name. It's ambiguous enough that even without COM I could make a case for it!
Tuples should map to multiple [out]
values. No [retval]
will be present in this case.
fn foo( &self ) -> Result<bool, E>;
// No tuple; Normal retval
// > HRESULT Foo( [out, retval] bool* __out );
fn foo( &self ) -> Result<(), E>;
// Unit tuple; No return values of any kind
// > HRESULT Foo();
fn foo( &self ) -> Result<( bool, u8, f32 ), E>;
// Tuple; Multiple out values
// > HRESULT Foo( [out] bool* __out1, [out] uint8_t* __out2, [out] float* __out3 );
Currently not supported. Should we support these?
Support would require multiple raw VARIANT structs, which differ on the string types - and possibly the array types in the future.
Currently all interface pointers within VARIANTs are of Automation type system, but this is somewhat easy to change.
One proposal is to have
VARIANT_raw
and VARIANT_automation
structs. VARIANT_automation
is the same as Microsoft's VARIANT struct. VARIANT_raw would replace BSTRs with char*s, etc. Rust would need to define both of these raw structs. The friendly intercom::Variant
would then implement conversion into and from both of these.
The low level parameter support would come just by having the tyhandler::VariantParam
defining the underlying com_ty as the respective raw struct. We'll use into
/from
and com_into
/com_from
for conversions anyway so that should magically just work once the com_ty
handles the type system.
The big downside here is that we'd need to come up with two different names for these in intercom-cpp. The global VARIANT
is already reserved in Windows programming so that kind of needs to stay. On the other hand Intercom-cpp might want to use intercom::VARIANT
as the raw version, which is more likely to be used with intercom C++ code anyway.
Or should we have intercom::VARIANT_raw
and intercom::VARIANT_automation
as the two concrete types and then some flags to define which typedef intercom::VARIANT
into one of those?
Currently we properly support only ComResult<T>
or Result<T, HRESULT>
as the return types from Rust functions.
We should also include support for Result<T, E>
through IErrorInfo
. We still need HRESULT
as the return value from the method so we will require HRESULT : From<E>
for the error types. Also given IErrorInfo
is able to return error description, we should require E: Fmt
or at the very least E: Debug
.
The technical details will probably include "error info slots" on ComBox
for each implemented interface - or at least those interfaces that include Result<T,E>
types.
Given we will need HRESULT: From<E>
, we probably also need to change HRESULT
from pub type HRESULT = i32
type alias into a proper pub struct HRESULT( i32 )
type to prevent the automatic i32
<-> HRESULT
aliasing.
Expected tasks:
ComBox
.
vtable_list
. We probably need to define ErrorInfoSlots
or similar dependent type in the CoClass
trait that gets defined in the #[com_class]
expansion.#[com_class]
expansion doesn't get to examine the actual methods, we'll define a slot per interface even if the interface doesn't include Result<T,E>
return values.Option<Box<Fmt>>
or similar structure. Whatever is the trait that we use to acquire the description if needed.#[com_impl]
expansion to reset the error info when the COM method is called and store the error info if the call to the Rust method was a failure.
ReturnHandler
in the returnhandlers.let &mut error_slot = ...
reference to the interface specific error slot in the #[com_impl]
method expansion and then considering the error_slot
a global value in the return handlers. Otherwise we'd need to pass the interface name to the ReturnHandler
so that they know which slot to write the possible error.#[com_class]
needs to include the IErrorInfo
support for query_interface
. This should be done in similar way to how we do IUnknown
implementation. Try to keep as much of the IErrorInfo
implementation static as possible so we could implement it in intercom
crate instead of having to rely on code generation to define it.If IErrorInfo
is properly implemented, we should get proper error messages in C#. This should be included in unit tests.
Currently ComResult<ComRc<T>>
fails as ComRc would need a default value for type handling.
Instead of using Default::default
we should define EmptyReturnValue
or similar trait. We can have a blanket impl that delegates to Default::default
if that is available. For other types this would allow us to implement a somewhat unsafe 'default value' for return values.
For example ComRc
might instantiate ComRc that contains a null pointer. This invalidates the ComRc contract (of never being null) so implementing such Default
value would be extremely unsafe - on the other hand if it is a very special EmptyReturnValue
trait that is used only for (somewhat sane) uninitialized values it makes more sense.
The value()
fn of that trait could also be unsafe
to further mark it as being outside the usual invariants.
Currently we need #[feature(fundamental)]
to be able to impl Deref for ComItf<IComInterface>
in the user crate as neither Deref
or ComItf
are user types.
I'm not even sure why we really need this impl as ideally we'd want to move that impl into Intercom crate so that the type system has some sort of an idea that any ComItf<T>
can act as if it was a trait object &T
.
I believe the following architecture might achieve that last part without needing fundamental
:
// Library crate:
type RawComPtr = *mut std::os::raw::c_void;
// The basic ComItf container.
struct ComItf<T : ?Sized> {
ptr : RawComPtr,
phantom : std::marker::PhantomData<T>,
}
// Trait for COM interface traits.
// Handles turning a ComItf<T> into a T reference.
trait ComDelegate {
fn wrap( ptr : &ComItf<Self> ) -> &Self;
}
// Allow Deref into trait for the ComItf.
// Works as long as the trait implements ComDelegate, which is used for wrapping.
impl<T: ComDelegate + ?Sized> std::ops::Deref for ComItf<T> {
type Target = T;
fn deref( &self ) -> &Self::Target {
T::wrap( &self )
}
}
// User crate:
// Normal trait.
trait IFoo {
fn bar( &self, a : u32 ) -> u32;
}
// Impl ComDelegate (intercom type) for user trait.
// No need for #[feature(fundamental)]
impl ComDelegate for IFoo {
fn wrap( ptr : &ComItf<IFoo> ) -> &(dyn IFoo + 'static) {
ptr
}
}
// Impl IFoo (user trait) for ComItf<IFoo>.
// No need for #[feature(fundamental)]
impl IFoo for ComItf<IFoo> {
fn bar( &self, a : u32 ) -> u32 {
( self.ptr as usize + a as usize ) as u32
}
}
fn main() {
let ptr : RawComPtr = 1234 as RawComPtr;
let itf = ComItf::<IFoo> { ptr, phantom: std::marker::PhantomData };
println!( "{}", itf.bar( 10 ) );
}
Currently our BSTR
implementation uses SysAllocString
, etc. methods from the Windows API. This is the de facto way to allocate/deallocate BSTRs on Windows.
For cross platform purposes we need an alternative way. One option would be to statically implement a IStringAllocator
interface with a known GUID. This allows the COM client to acquire a IStringAllocator
interface that provides FreeString(BSTR)
method. This method can then allocate/deallocate the BSTR properly.
The only important consideration with the implementation is that the allocation and deallocation is done in a compatible way with each other. Thus if we allocate the BSTR from Rust heap, the deallocate must free the BSTR into Rust heap. If we use SysAllocString
then the freeing must be done with SysFreeString
.
Tasks:
intercom::IStringAllocator
or similar trait. The implementation can be static intercom::StringAllocator
as well. We might not even need the trait but instead just use the implicit impl
instead.#[com_library]
code gen to include the StringAllocator
in the ClassFactory
constructor.SysAllocString
in Windows and something else (such as Rust heap/Box
) on other platforms.kcov
seems like the way to go. Few articles on the subject:
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.
There should be no technical reason to have 1:1 mapping between Rust and COM parameters. We are already coming up with COM OUT values based on Rust return types.
We could further skip some of the Rust method parameters on the COM level and fill these in by Intercom.
The primary (and for now only) use case for this would be the type system argument. Being able to tell what type system is being used to invoke a function has immediate use case in writing unit tests for Intercom itself. But it would also be required if someone wants to write methods by managing interface pointers, etc. themselves.
The big open question here is what kind of marker we'll want for such parameters. Below are some options in terms of the intercom::TypeSystem
enum.
/// Assume certain types are always contextual types.
fn foo( &self, ts : intercom::TypeSystem );
/// Require an attribute on the parameter.
fn foo( &self, #[intercom::context] ts : intercom::TypeSystem );
/// Use a generic type instead.
fn foo( &self, ts : intercom::CallContext<intercom::TypeSystem> );
Personally I'm in favor of the first one. Intercom needs to be aware of these types anyway and as such we can be certain that these types are not COM-compatible (no #[repr(C)]
).
The one dirty aspect with this is that when Rust wants to call such COM methods, there's no clean way to communicate to the caller that these things are not needed...
Although things like ts
should probably affect the preference of which TS to use in case the ComItf
we are using to call these methods happens to have both Automation and Raw interfaces available. Though is it an error if the asked interface is not available? There's no real TypeSystem::Any
variant and we don't want one for output purposes.
There are various internal error conditions that we would love to signal forward. These include things such as:
*mut
pointers pointing to same data and need to be converted to &mut
, which violates Rust reference rules.panic!
ed.If the method is one that returns HRESULT
(or specifically Result<...>
), Intercom can ride on top of this and convert these error conditions into HRESULT
s automatically. However if the method is not one that returns HRESULT
, there's no other option than to panic!
(well... the other option would be to ignore the errors, but that's even worse).
For this reason Intercom would need to recognize the method type and behave accordingly.
Maybe we could generate something similar to:
fn hresult(...) -> HRESULT {
type ErrType = HRESULT; // <- Depends on method return value.
if( some error happened ) {
return ErrType::From( E_FAIL as an example );
}
...
}
fn not_hresult(...) -> bool {
type ErrType = IntoPanic; // <- Depends on method return value.
if( some error happened ) {
return ErrType::From( E_FAIL as an example );
}
...
}
This would minimize the amount of changes the return value would cause for the function. It would need to be taken into account in one place only. IntoPanic
would implement:
impl<T> From<T> for IntoPanic {
fn from(t:T) -> IntoPanic {
panic!(..);
}
}`
Our current approach in intercom_utils::{idl, manifest, cpp}
is to generate the output through string concatenation and formatting.
If we could replace this with a proper templating crate, it would make for cleaner code. Rustache perhaps?
First issue that relates somewhat to winapi-rs
interoperability \o/
Intercom has always aimed for making COM code look rustful. This involves automatically converting between COM strings and Rust strings, mapping Result types to [out]
/[out, retval]
parameters and HRESULTs, etc.
This works great when you get to decide the interface. \o/
However... there are cases, where it would be nice to use Intercom with existing interfaces, which need to be defined as they are. This could involve things like having [out]
parameters in the middle of the parameter list, etc.
The problem with these bits is that they do not play well with the Intercom tyhandling
/returnhandling
. [out]
params for example, are currently defined with Result
return types, which automatically insert these at the end of the parameter list.
On the other hand, from Rust point of view (ignoring IDL bits), there is no real difference between a u32
return type and a *mut u32
parameter. As a result it should be possible to define the interfaces by hand using the raw pointer parameters, etc. and skipping the return handling by using HRESULT
as the return value instead of a Result<...>
of some kind.
Currently #[com_library]
attribute requires the type library name as the first parameter. It is strongly recommended that this library name matches the crate name - so strongly that it makes no sense for those two to differ.
It would be better to drop that parameter completely and just use the crate itself as the library name.
There is a slight issue with this though. We need a way to find out the name of the crate using the attributes. The usual way to find the crate name programmatically is to use the env variables with the env!
macro, which inspects these at compile time.
When it comes to attribute expansions, if we used the env!
macro, we'd end up with the intercom-attributes
crate name. However as the crate compilation is runtime for attribute expansions, we should be able to use the env variables as is through the std::env
module.
This same issue applies to #1.
Tasks:
com_library
attribute expansion.library_name
to LibraryName
for COM capitalization purposes. This conversion is already done in intercom-utils
for IDL generation. The conversion should be moved to intercom-common/utils.rs
so we can share the code.intercom-utils
handles the new com_library
attribute.Currently the com_library macro expects idents I believe. This prevents referring types in other modules.
See #89 for an ugly workaround.
On Windows platform unloading is handled with the combination of DllCanUnloadNow and CoFreeUnusedLibraries. Our Rust libraries should support this on Windows.
On other platforms we need to implement the required bookkeeping and unloading manually, If no references are kept open then we can call dlclose. Note that Posix does not require that dlclose actually unloads the shared library from the process.
In theory the COM libraries created using intercom
should be cross-platform compatible. There's no major Windows dependencies involved here.
The big issue is the lack of midl
for GCC/LLVM. As such the intercom-utils
would need a new cpp-classes
or similar command that processes the Rust sources similar to what idl
does, but instead of producing an idl file, the command produces C++ sources similar to the various _i.c
and _i.h
sources that midl
produces.
These sources need to implement C++ classes that result in compatible vtable definitions with the COM vtables. Ideally they should use the same "stdcall" calling convention that we already use in the intercom functions for Windows COM. However we do have the option of using "stdcall" on Windows and something else that behaves better on Linux as long as that "something else" is binary compatible between GCC/LLVM/Rustc.
Tasks:
intercom-utils
.cpp
unit tests for Linux builds on Travis. This involves wrapping the CoInitialize
/CoUninitialize
in some sort of IntercomInit
/Uninit
method that is able to call the CoInitialize
/etc. on Windows and loads the type library using dlopen
or similar on Linux. The CoCreateInstance
needs to be abstracted so that on Linux we acquire the ClassFactory
and construct the instance manually.On Windows we have GetErrorInfo
/SetErrorInfo
- however we want a cross platform way of doing this so we can support the ComError
(IErrorInfo
) - based error messages on other platforms as well.
One option is to implement this as a "static service class", similar to the string allocator of #6.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.