-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Derived Interface support in ComInterfaceGenerator #84271
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
f5f0868
Derived Interface support in ComInterfaceGenerator
jkoritzinsky b34f607
When calculating base info, determine if the interface is a marker in…
jkoritzinsky 6ca15d5
Merge branch 'main' of github.com:dotnet/runtime into iface-inherit
jkoritzinsky b97d619
PR feedback
jkoritzinsky File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
191 changes: 191 additions & 0 deletions
191
docs/design/libraries/ComInterfaceGenerator/DerivedComInterfaces.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
# Derived Interfaces and COM | ||
|
||
In the ComInterfaceGenerator, we want to improve the experience when writing COM interfaces that derive from other COM interfaces. The built-in system has some quirks due to differences between how interfaces work in C# in comparison to COM interfaces in C++. | ||
|
||
## COM Interface Inheritance in C++ | ||
|
||
In C++, developers can declare COM interfaces that derive from other COM interfaces as follows: | ||
|
||
```cpp | ||
struct IComInterface : public IUnknown | ||
{ | ||
STDMETHOD(Method)() = 0; | ||
STDMETHOD(Method2)() = 0; | ||
}; | ||
|
||
struct IComInterface2 : public IComInterface | ||
{ | ||
STDMETHOD(Method3)() = 0; | ||
}; | ||
``` | ||
|
||
This declaration style is regularly as a mechanism to add methods to COM objects without changing existing interfaces, which would be a breaking change. This inheritance mechanism results in the following vtable layouts: | ||
|
||
| `IComInterface` VTable slot | Method name | | ||
|-----------------------------|-------------| | ||
| 0 | `IUnknown::QueryInterface` | | ||
| 1 | `IUnknown::AddRef` | | ||
| 2 | `IUnknown::Release` | | ||
| 3 | `IComInterface::Method` | | ||
| 4 | `IComInterface::Method2` | | ||
|
||
|
||
| `IComInterface2` VTable slot | Method name | | ||
|-----------------------------|-------------| | ||
| 0 | `IUnknown::QueryInterface` | | ||
| 1 | `IUnknown::AddRef` | | ||
| 2 | `IUnknown::Release` | | ||
| 3 | `IComInterface::Method` | | ||
| 4 | `IComInterface::Method2` | | ||
| 5 | `IComInterface2::Method3` | | ||
|
||
As a result, it is very easy to call a method defined on `IComInterface` from an `IComInterface2*`. Specifically, calling a method on a base interface does not require a call to `QueryInterface` to get a pointer to the base interface. Additionally, C++ allows an implicit conversion from `IComInterface2*` to `IComInterface*`, which is well defined and allows avoiding a `QueryInterface` call again. As a result, in C or C++, you never need to call `QueryInterface` to get to the base type if you do not want to, which can allow some performance improvements. | ||
|
||
> Note: WinRT interfaces do not follow this inheritance model. They are defined to follow the same model as the `[ComImport]`-based COM interop model in .NET. | ||
|
||
## COM Interface Inheritance in .NET with `[ComImport]` | ||
|
||
In .NET, C# code that looks like interface inheritance isn't actually interface inheritance. Let's look at the following code: | ||
|
||
```csharp | ||
interface I | ||
{ | ||
void Method1(); | ||
} | ||
interface J : I | ||
{ | ||
void Method2(); | ||
} | ||
``` | ||
|
||
This code does not say that "`J` implements `I`." It actually says "any type that implements `J` must also implement `I`." This difference leads to the fundamental design decision that makes interface inheritance in `[ComImport]`-based interop unergonomic. In .NET's COM interop, interfaces are always considered on their own; an interface's base interface list has no impact on any calculations to determing a vtable for a given .NET interface. | ||
|
||
As a result, the natural equivalent of the above provided C++ COM interface example leads to a different vtable layout. | ||
|
||
C# code: | ||
```csharp | ||
[ComImport] | ||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] | ||
interface IComInterface | ||
{ | ||
void Method(); | ||
void Method2(); | ||
} | ||
|
||
[ComImport] | ||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] | ||
interface IComInterface2 : IComInterface | ||
{ | ||
void Method3(); | ||
} | ||
``` | ||
|
||
VTable layouts: | ||
|
||
| `IComInterface` VTable slot | Method name | | ||
|-----------------------------|-------------| | ||
| 0 | `IUnknown::QueryInterface` | | ||
| 1 | `IUnknown::AddRef` | | ||
| 2 | `IUnknown::Release` | | ||
| 3 | `IComInterface::Method` | | ||
| 4 | `IComInterface::Method2` | | ||
|
||
|
||
| `IComInterface2` VTable slot | Method name | | ||
|-----------------------------|-------------| | ||
| 0 | `IUnknown::QueryInterface` | | ||
| 1 | `IUnknown::AddRef` | | ||
| 2 | `IUnknown::Release` | | ||
| 3 | `IComInterface2::Method3` | | ||
|
||
As these vtables differ from the C++ example, this will lead to serious problems at runtime. The correct definition of these interfaces in C# for the `[ComImport]` interop system is as follows: | ||
|
||
```csharp | ||
[ComImport] | ||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] | ||
interface IComInterface | ||
{ | ||
void Method(); | ||
void Method2(); | ||
} | ||
|
||
[ComImport] | ||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] | ||
interface IComInterface2 : IComInterface | ||
{ | ||
new void Method(); | ||
new void Method2(); | ||
void Method3(); | ||
} | ||
``` | ||
|
||
Each method from the base interface types must be redeclared, as at the metadata level, `IComInterface2` does not implement `IComInterface`, but only specifies that implementors of `IComInterface2` must also implement `IComInterface`. This design decision is an ugly wart on the built-in COM interop system, so we want to improve this in the new source-generated COM interop system. | ||
|
||
## COM Interface Inheritance in Source-Generated COM | ||
|
||
In the new source-generated COM model, we will enable writing COM interfaces using natural C# interface "inheritance" with some restrictions to ensure a valid model. Here's an example: | ||
|
||
C# code: | ||
```csharp | ||
[GeneratedComInterface] | ||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] | ||
interface IComInterface | ||
{ | ||
void Method(); | ||
void Method2(); | ||
} | ||
|
||
[GeneratedComInterface] | ||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] | ||
interface IComInterface2 : IComInterface | ||
{ | ||
void Method3(); | ||
} | ||
``` | ||
|
||
VTable slots: | ||
|
||
| `IComInterface` VTable slot | Method name | | ||
|-----------------------------|-------------| | ||
| 0 | `IUnknown::QueryInterface` | | ||
| 1 | `IUnknown::AddRef` | | ||
| 2 | `IUnknown::Release` | | ||
| 3 | `IComInterface::Method` | | ||
| 4 | `IComInterface::Method2` | | ||
|
||
|
||
| `IComInterface2` VTable slot | Method name | | ||
|-----------------------------|-------------| | ||
| 0 | `IUnknown::QueryInterface` | | ||
| 1 | `IUnknown::AddRef` | | ||
| 2 | `IUnknown::Release` | | ||
| 3 | `IComInterface::Method` | | ||
| 4 | `IComInterface::Method2` | | ||
| 5 | `IComInterface2::Method3` | | ||
|
||
As you can see, this new model will allow the "natural" pattern for authoring .NET interfaces in C# result in the "natural" vtable layout as if the type was written in C++. There is a restriction on this pattern to ensure an easy-to-construct model: | ||
|
||
- A `[GeneratedComInterface]`-attributed type may inherit from at most one `[GeneratedComInterface]`-attribute type. | ||
|
||
This ensures that our COM-interop .NET interfaces will always follow the model of single inheritance, which is the only direct inheritance model that COM allows. | ||
|
||
### Designing for Performance | ||
|
||
This new model has really nice ergonomics for authoring, but it has some performance deficits compared to the built-in model. In particular, when running the following code: | ||
|
||
```csharp | ||
IComInterface2 obj = /* get the COM object */; | ||
obj.Method(); | ||
``` | ||
|
||
The `[ComImport]` code will not call `QueryInterface` for `IComInterface`, but the `[GeneratedComInterface]` model will. The `[ComImport]` pattern will not need to call `QueryInterface`, as the required shadowing method declarations means that the `.Method()` call resolves to `IComInterface2.Method`, whereas the `[GeneratedComInterface]`-based code resolves that call to `IComInterface.Method`. Since the `[GeneratedComInterface]` mechanism doesn't shadow the method, the runtime will try to cast `obj` to `IComInterface`, which will result in a `QueryInterface` call. | ||
|
||
To reduce the number of `QueryInterface` calls, the ComInterfaceGenerator will automatically emit shadowing method declarations and corresponding method implementations for all methods from any `[GeneratedComInterface]`-attributed base type and its attributed base types recursively that are visible. This way, we can ensure that the least number of `QueryInterface` calls are required when using the `[GeneratedComInterface]`-based COM interop. Additionally, we will disallow declaring any methods with the `new` modifier on a `[GeneratedComInterface]`-attributed interface to ensure that the user does not try to shadow any base interface members. | ||
|
||
What about when the marshallers used in a base interface method declaration are not accessible by the derived type? We can try to detect this case, but it makes it very fragile to determine which methods are shadowed and which are not. Additionally, removing a shadowing method is a binary breaking change. We have a few options: | ||
|
||
1. Always try to emit shadowing methods with marshalling and error out (either with C# compiler errors or in the generator itself) when the marshallers are not accessible. | ||
2. Don't emit shadowing methods when the marshallers aren't accessible. | ||
3. Emit shadowing stubs that call the base interface method when the marshallers are not accessible. | ||
|
||
Option 2 is very fragile and makes it really easy to accidentally cause a binary breaking change, so we should either go with Option 1 or 3. For simplicity of implementation, we will go with Option 1 until we get customer feedback that this is a serious issue, at which point we will consider switching to Option 3. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on adding the sections about how it works in C++ and .NET ComImport (and then GeneratedComInterface once it ships) to public docs? Probably somewhere under https://learn.microsoft.com/dotnet/standard/native-interop/cominterop
I think the descriptions you have here are clear and useful. I don't think we have this called out or explained anywhere in official docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good idea! I'll make sure to add it to the docs.