Skip to content
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

Sample for IDynamicInterfaceCastable #3744

Merged
merged 11 commits into from
Jul 28, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions core/interop/IDynamicInterfaceCastable/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project>
<!-- Shared properties -->
<PropertyGroup>
<SourceRoot>$(MSBuildThisFileDirectory)src</SourceRoot>
<BinRoot>$(MSBuildThisFileDirectory)bin</BinRoot>
</PropertyGroup>
</Project>
67 changes: 67 additions & 0 deletions core/interop/IDynamicInterfaceCastable/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
`IDynamicInterfaceCastable` Sample
================
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved

The [`IDynamicInterfaceCastable` API](https://github.com/dotnet/runtime/issues/36654) was introduced in .NET 5 as a way for creating a .NET class that supports interfaces which are not in its metadata.
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved

This sample provides an implementation of `IDynamicInterfaceCastable` that projects a native object as implementing different known managed interfaces. It uses COM conventions (e.g. `QueryInterface`) for interacting with the native object, but does not require the actual COM system.
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved

Note: The sample uses `Marshal` APIs as part of interacting with the native library. The introduction of [C# function pointers](https://github.com/dotnet/csharplang/blob/994c41586e07e38fb6b30902b1715b4025d80c52/proposals/function-pointers.md) will allow that interaction to occur in a more performant manner.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function pointers are shipping in previews already. Can the sample be modified to use them?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new syntax for specifying the calling convention (e.g. cdecl -> unmanaged[Cdecl]) isn't in the shipped previews yet. I was planning on updating the sample once they can use the new syntax.

elinor-fung marked this conversation as resolved.
Show resolved Hide resolved

Prerequisites
------------
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved

* .NET Core 5.0 Preview 7 or later

* C++ compiler
* Windows: `cl.exe`
* Linux/OSX: `g++`
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved

Build and Run
-------------
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved

1) In order to build and run, all prerequisites must be installed. The following are also required:

* The C++ compiler (`cl.exe` or `g++`) must be on the path.
* On Windows, a [Developer Command Prompt for Visual Studio](https://docs.microsoft.com/cpp/build/building-on-the-command-line#developer_command_prompt_shortcuts) should be used.
* The C++ compiler (`cl.exe` or `g++`) and `dotnet` must be the same bitness (32-bit versus 64-bit).
* On Windows, the default developer command prompt for VS uses the 32-bit compilers, but `dotnet` is typically 64-bit by default. Make sure to select the "x64 Native Tools Command Prompt for VS 2019" (or 2017).

1) Navigate to the root directory and run `dotnet build`
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved

1) Run the sample. Do one of the following:

* Use `dotnet run` (which will build and run at the same time).
* Use `dotnet build` to build the executable. The executable will be in `bin` under a subdirectory for the configuration (`Debug` is the default).
* Windows: `bin\Debug\IDynamicInterfaceCastableSample.exe`
* Non-Windows: `bin/Debug/IDynamicInterfaceCastableSample`

The expected output will show information about the native objects as they are represented in manage as well as results from calls to the native objects:

```
Native Object #0
- does not implement IGreet
- does not implement ICompute

Native Object #1
- implements IGreet
-- Hello World from NativeObject #1
- does not implement ICompute

Native Object #2
- does not implement IGreet
- implements ICompute
-- Returned sum: 5

Native Object #3
- implements IGreet
-- Hello World from NativeObject #3
- implements ICompute
-- Returned sum: 6
```

Note: The way the sample is built is relatively complicated. The goal is that it's possible to build and run the sample with simple `dotnet run` with minimal requirements on pre-installed tools. Typically real-world projects which have both managed and native components will use different build systems for each; for example msbuild/dotnet for managed and CMake for native.

Visual Studio support
---------------------
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved

The `src\IDynamicInterfaceCastableSample.sln` can be used to open the sample in Visual Studio 2019. In order to be able to build from Visual Studio, though, it has to be started from the correct developer environment. From the developer environment console, start it with `devenv src\IDynamicInterfaceCastableSample.sln`. With that, the solution can be built. To run it set the start project to `IDynamicInterfaceCastableSample`.
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved
15 changes: 15 additions & 0 deletions core/interop/IDynamicInterfaceCastable/build.proj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.Build.Traversal">

<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>

<RunCommand>$(BinRoot)\$(Configuration)\IDynamicInterfaceCastableSample</RunCommand>
<RunCommand Condition="$([MSBuild]::IsOsPlatform('Windows'))">$(BinRoot)\$(Configuration)\IDynamicInterfaceCastableSample.exe</RunCommand>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="src/ManagedApp/*.csproj" />
<ProjectReference Include="src/NativeLib/*.csproj" />
</ItemGroup>

</Project>
5 changes: 5 additions & 0 deletions core/interop/IDynamicInterfaceCastable/global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"msbuild-sdks": {
"Microsoft.Build.Traversal": "2.0.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30224.187
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IDynamicInterfaceCastableSample", "ManagedApp\IDynamicInterfaceCastableSample.csproj", "{5990550F-54A5-4518-841D-AE06C045883A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeLib", "NativeLib\NativeLib.csproj", "{5CA1D5C3-2478-4E91-A841-45128CB3F87D}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NativeLib.vs", "NativeLib\NativeLib.vs.vcxproj", "{6FCE3D39-44AA-4234-85A8-950E57E8F038}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{FB75DD88-ABF9-46E5-879D-5D7BBE0323BD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5990550F-54A5-4518-841D-AE06C045883A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5990550F-54A5-4518-841D-AE06C045883A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5990550F-54A5-4518-841D-AE06C045883A}.Debug|x64.ActiveCfg = Debug|Any CPU
{5990550F-54A5-4518-841D-AE06C045883A}.Debug|x64.Build.0 = Debug|Any CPU
{5990550F-54A5-4518-841D-AE06C045883A}.Debug|x86.ActiveCfg = Debug|Any CPU
{5990550F-54A5-4518-841D-AE06C045883A}.Debug|x86.Build.0 = Debug|Any CPU
{5990550F-54A5-4518-841D-AE06C045883A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5990550F-54A5-4518-841D-AE06C045883A}.Release|Any CPU.Build.0 = Release|Any CPU
{5990550F-54A5-4518-841D-AE06C045883A}.Release|x64.ActiveCfg = Release|Any CPU
{5990550F-54A5-4518-841D-AE06C045883A}.Release|x64.Build.0 = Release|Any CPU
{5990550F-54A5-4518-841D-AE06C045883A}.Release|x86.ActiveCfg = Release|Any CPU
{5990550F-54A5-4518-841D-AE06C045883A}.Release|x86.Build.0 = Release|Any CPU
{5CA1D5C3-2478-4E91-A841-45128CB3F87D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5CA1D5C3-2478-4E91-A841-45128CB3F87D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5CA1D5C3-2478-4E91-A841-45128CB3F87D}.Debug|x64.ActiveCfg = Debug|Any CPU
{5CA1D5C3-2478-4E91-A841-45128CB3F87D}.Debug|x64.Build.0 = Debug|Any CPU
{5CA1D5C3-2478-4E91-A841-45128CB3F87D}.Debug|x86.ActiveCfg = Debug|Any CPU
{5CA1D5C3-2478-4E91-A841-45128CB3F87D}.Debug|x86.Build.0 = Debug|Any CPU
{5CA1D5C3-2478-4E91-A841-45128CB3F87D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5CA1D5C3-2478-4E91-A841-45128CB3F87D}.Release|Any CPU.Build.0 = Release|Any CPU
{5CA1D5C3-2478-4E91-A841-45128CB3F87D}.Release|x64.ActiveCfg = Release|Any CPU
{5CA1D5C3-2478-4E91-A841-45128CB3F87D}.Release|x64.Build.0 = Release|Any CPU
{5CA1D5C3-2478-4E91-A841-45128CB3F87D}.Release|x86.ActiveCfg = Release|Any CPU
{5CA1D5C3-2478-4E91-A841-45128CB3F87D}.Release|x86.Build.0 = Release|Any CPU
{6FCE3D39-44AA-4234-85A8-950E57E8F038}.Debug|Any CPU.ActiveCfg = Debug|Win32
{6FCE3D39-44AA-4234-85A8-950E57E8F038}.Debug|x64.ActiveCfg = Debug|x64
{6FCE3D39-44AA-4234-85A8-950E57E8F038}.Debug|x64.Build.0 = Debug|x64
{6FCE3D39-44AA-4234-85A8-950E57E8F038}.Debug|x86.ActiveCfg = Debug|Win32
{6FCE3D39-44AA-4234-85A8-950E57E8F038}.Debug|x86.Build.0 = Debug|Win32
{6FCE3D39-44AA-4234-85A8-950E57E8F038}.Release|Any CPU.ActiveCfg = Release|Win32
{6FCE3D39-44AA-4234-85A8-950E57E8F038}.Release|x64.ActiveCfg = Release|x64
{6FCE3D39-44AA-4234-85A8-950E57E8F038}.Release|x64.Build.0 = Release|x64
{6FCE3D39-44AA-4234-85A8-950E57E8F038}.Release|x86.ActiveCfg = Release|Win32
{6FCE3D39-44AA-4234-85A8-950E57E8F038}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5CA1D5C3-2478-4E91-A841-45128CB3F87D} = {FB75DD88-ABF9-46E5-879D-5D7BBE0323BD}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5569A5E7-D4A8-4E4C-9834-90E589AF337C}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<DefineConstants Condition="'$(OS)' == 'Windows_NT'">$(DefineConstants);WINDOWS</DefineConstants>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="$(SourceRoot)/NativeLib/NativeLib.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>

<Target Name="SetupForDemo" AfterTargets="Build">

<ItemGroup>
<AllOutputs Include="$(OutputPath)IDynamicInterfaceCastableSample.*" />
</ItemGroup>

<!-- Copy the project outputs to the bin directory -->
<Copy SourceFiles="@(AllOutputs)" DestinationFolder="$(BinRoot)/$(Configuration)" />
</Target>

</Project>
100 changes: 100 additions & 0 deletions core/interop/IDynamicInterfaceCastable/src/ManagedApp/NativeObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace IDynamicInterfaceCastableSample
{
class NativeObject : IDynamicInterfaceCastable, IDisposable
{
public string Name { get; }

private delegate int _QueryInterface(IntPtr _this, ref Guid iid, out IntPtr ppv);
IEvangelist marked this conversation as resolved.
Show resolved Hide resolved
private delegate uint _Release(IntPtr _this);
IEvangelist marked this conversation as resolved.
Show resolved Hide resolved

[StructLayout(LayoutKind.Sequential)]
private struct IUnknownVtbl
{
public _QueryInterface QueryInterface;
IEvangelist marked this conversation as resolved.
Show resolved Hide resolved
public IntPtr AddRef;
public _Release Release;
IEvangelist marked this conversation as resolved.
Show resolved Hide resolved
}

[StructLayout(LayoutKind.Sequential)]
private struct VtblPtr
{
public IntPtr Vtbl;
}

private class NativeImpl
{
public IntPtr Ptr;
public object Vtbl;
}

private readonly Dictionary<RuntimeTypeHandle, RuntimeTypeHandle> interfaceTypeToImplType;
private readonly IntPtr objPtr;
private readonly IUnknownVtbl unknownVtbl;
private readonly Dictionary<RuntimeTypeHandle, NativeImpl> typeToNativeImpl = new Dictionary<RuntimeTypeHandle, NativeImpl>();
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved

public NativeObject(string name, IntPtr obj, Dictionary<RuntimeTypeHandle, RuntimeTypeHandle> interfaceMap)
{
Name = name;
objPtr = obj;
interfaceTypeToImplType = interfaceMap;
VtblPtr vtblPtr = Marshal.PtrToStructure<VtblPtr>(obj);
unknownVtbl = Marshal.PtrToStructure<IUnknownVtbl>(vtblPtr.Vtbl);
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved
}

bool IDynamicInterfaceCastable.IsInterfaceImplemented(RuntimeTypeHandle interfaceType, bool throwIfNotImplemented)
{
if (!interfaceTypeToImplType.ContainsKey(interfaceType))
return false;

if (typeToNativeImpl.ContainsKey(interfaceType))
return true;

Type type = Type.GetTypeFromHandle(interfaceType);
Guid guid = type.GUID;
bool success = unknownVtbl.QueryInterface(objPtr, ref guid, out IntPtr ppv) == 0;
if (!success)
return false;

typeToNativeImpl.Add(interfaceType, new NativeImpl { Ptr = ppv });
return true;
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved
}

RuntimeTypeHandle IDynamicInterfaceCastable.GetInterfaceImplementation(RuntimeTypeHandle interfaceType)
{
Type type = Type.GetTypeFromHandle(interfaceType);
if (!typeToNativeImpl.ContainsKey(interfaceType) || !interfaceTypeToImplType.ContainsKey(interfaceType))
return default;

return interfaceTypeToImplType[interfaceType];
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved
}

public T GetVtbl<T>(RuntimeTypeHandle interfaceType, out IntPtr ptr)
{
if (!typeToNativeImpl.TryGetValue(interfaceType, out NativeImpl impl))
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved
throw new InvalidOperationException();

ptr = impl.Ptr;
if (impl.Vtbl != null)
return (T)impl.Vtbl;

VtblPtr vtblPtr = Marshal.PtrToStructure<VtblPtr>(ptr);
T vtbl = Marshal.PtrToStructure<T>(vtblPtr.Vtbl);
impl.Vtbl = vtbl;

return vtbl;
}

public void Dispose()
{
unknownVtbl.Release(objPtr);

// Release for every successful QI
for (var i = 0; i < typeToNativeImpl.Count; i++)
unknownVtbl.Release(objPtr);
elinor-fung marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Loading