Skip to content

Commit

Permalink
docs: add example for accessing .NET components via COM interop (#919)
Browse files Browse the repository at this point in the history
  • Loading branch information
halildurmus authored Oct 22, 2024
1 parent 69a32f5 commit 173e655
Show file tree
Hide file tree
Showing 9 changed files with 953 additions and 0 deletions.
400 changes: 400 additions & 0 deletions examples/com_interop/MySolution/.gitignore

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions examples/com_interop/MySolution/MyLibrary/Class1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Runtime.InteropServices;

namespace MyLibrary
{
[ComVisible(true)]
[Guid("4C2DDA7F-9DC9-46FD-A107-832254B2EEBE")]
public interface IExampleCom
{
string GetMessage();
int GetSum(int a, int b);
}

[ComVisible(true)]
[Guid("36B142F2-97DC-4594-96A4-8160EEB7184C")]
public class ExampleCom : IExampleCom
{
public string GetMessage() => "Hello from .NET!";
public int GetSum(int a, int b) => a + b;
}
}

49 changes: 49 additions & 0 deletions examples/com_interop/MySolution/MyLibrary/MyLibrary.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{BE1A9D69-94D6-4B3D-8844-3BE2769AF167}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MyLibrary</RootNamespace>
<AssemblyName>MyLibrary</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<RegisterForComInterop>true</RegisterForComInterop>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Class1.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MyLibrary")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("MyLibrary")]
[assembly: AssemblyCopyright("Copyright © Halil Durmus 2024")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("be1a9d69-94d6-4b3d-8844-3be2769af167")]

// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
25 changes: 25 additions & 0 deletions examples/com_interop/MySolution/MySolution.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.11.35327.3
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyLibrary", "MyLibrary\MyLibrary.csproj", "{BE1A9D69-94D6-4B3D-8844-3BE2769AF167}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BE1A9D69-94D6-4B3D-8844-3BE2769AF167}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE1A9D69-94D6-4B3D-8844-3BE2769AF167}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE1A9D69-94D6-4B3D-8844-3BE2769AF167}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE1A9D69-94D6-4B3D-8844-3BE2769AF167}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {38B18BDE-9196-4EC1-A63E-9D255F61D064}
EndGlobalSection
EndGlobal
226 changes: 226 additions & 0 deletions examples/com_interop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# Accessing a .NET Component through COM Interop

This example demonstrates how to access a **.NET component** from Dart using
COM (Component Object Model) interop. It covers two different ways of
interacting with a .NET component exposed to COM:

- **Early Binding** through direct method access using the COM object's VTable
(Virtual function table).
- **Late Binding** via the [IDispatch] interface.

## Folder Structure

- **MySolution/**
- **MyLibrary/**: Contains the C# class library that exposes the .NET
component to COM.
- **main.dart**: Demonstrates calling methods from the .NET component using both
_early_ and _late_ binding.

## .NET Component: `MyLibrary`

`MyLibrary` is a C# class library designed to be used through COM. It defines an
interface and a class that implement two methods: one that returns a string and
another that returns the sum of two integers:

```csharp
[ComVisible(true)]
[Guid("4C2DDA7F-9DC9-46FD-A107-832254B2EEBE")]
public interface IExampleCom
{
string GetMessage();
int GetSum(int a, int b);
}

[ComVisible(true)]
[Guid("36B142F2-97DC-4594-96A4-8160EEB7184C")]
public class ExampleCom : IExampleCom
{
public string GetMessage() => "Hello from .NET!";
public int GetSum(int a, int b) => a + b;
}
```

### Annotations Explained

- **`[ComVisible(true)]`**: Marks the class and interface as visible to COM
clients, including Dart.
- **`[Guid("...")]`**: Assigns a unique identifier (GUID) to both the interface
and class, required for COM registration. You can generate GUIDs with tools
like `guidgen` or online GUID generators.

## Building and Registering the .NET Component

### Steps to Build

1. Open the solution file `MySolution.sln` in Visual Studio.
2. In **Solution Explorer**, right-click on the `MyLibrary` project and select
**Build**.

This will compile the library, generate DLL and TLB files, and register it for
COM interop. Ensure that the **Register for COM Interop** option is enabled in
the project settings so that the component is automatically registered after it
is built.

### Manual Registration

If you have a third-party library and compiled DLL and TLB files, you can
register the component manually using the [regasm] tool:

1. Open a command prompt with administrator privileges.
2. Navigate to the directory containing the DLL and TLB files.
3. Run the following command:

```cmd
regasm MyLibrary.dll /tlb:MyLibrary.tlb
```

After these steps, the .NET component will be accessible from Dart.

## Accessing the .NET Component from Dart

The `main.dart` file shows how to call methods from the .NET component using
both _early_ and _late_ binding.

### Early Binding

Early binding, also known as VTable binding is used whenever a COM object's
`IUnknown` interface is called. To use early binding on an object, you need to
know the structure of the COM object's VTable, which includes both standard
methods (from `IUnknown`) and any custom methods unique to the object. If the
method signatures and their positions in the VTable are known ahead of time, you
can call these custom methods in the same manner as the `IUnknown` methods.

### Late Binding

In late binding, the specific method or property being called is determined at
runtime. This process involves using the `IDispatch` methods to locate the
desired function, akin to looking up a page number in a table of contents rather
than having it printed directly in the text.

The two key functions that facilitate late binding are `GetIDsOfNames` and
`Invoke`. The `GetIDsOfNames` maps method names (as strings) to a unique
identifier known as a **dispid**. Once the dispid for the desired function is
obtained, you can invoke it using the `Invoke` function.

### Choosing Between Early and Late Binding

The decision between early binding and late binding largely depends on your
project's design and requirements. However, **early binding** is generally
recommended in most scenarios.

**Early Binding** is faster because your application binds directly to the
memory address of the function being called. This eliminates the overhead of
runtime lookups, making it at least twice as fast as late binding in terms of
overall execution speed.

On the other hand, **Late Binding** can be beneficial in specific situations:

- It is useful when the exact interface of a COM object is not known at
compile-time.

- Late binding can also help address compatibility issues between different
versions of a component that may have modified or adapted its interface.

The benefits of **early binding** make it the optimal choice whenever possible.

## Understanding the VTable in COM

The VTable is a structure used in COM to manage method calls. Each COM object
has a pointer that points to its VTable, which contains pointers to the object's
methods.

```plaintext
+----------------------------------------------------------------+
| COM Object Memory Layout |
+----------------------------------------------------------------+
| +-------------------+ +----------------------+ |
| p --> | v-table pointer | --> | Function 1 pointer | |
| +-------------------+ +----------------------+ |
| | Function 2 pointer | |
| +----------------------+ |
| | Function 3 pointer | |
| +----------------------+ |
| | ... | |
| +----------------------+ |
+----------------------------------------------------------------+
```

When a client (like Dart) wants to invoke a method on a COM object, it uses the
object's pointer to access the VTable. The VTable contains pointers to each
method, allowing the client to call methods using their respective offsets. The
first method is accessed at offset 1, the second at offset 2, and so on.

The `IUnknown` interface is the base for all COM interfaces, providing methods
for managing COM object lifecycles (like reference counting).

```plaintext
+----------------------------------------------------------------+
| IUnknown Interface Memory Layout |
+----------------------------------------------------------------+
| +-------------------+ +----------------------+ |
| p --> | v-table pointer | --> | QueryInterface | |
| +-------------------+ +----------------------+ |
| | AddRef | |
| +----------------------+ |
| | Release | |
| +----------------------+ |
+----------------------------------------------------------------+
```

COM interfaces can inherit from other interfaces. For instance, `IDispatch`
inherits from `IUnknown`. It includes methods for invoking methods by name and
retrieving type information.

```plaintext
+----------------------------------------------------------------+
| IDispatch Interface Memory Layout |
+----------------------------------------------------------------+
| +-------------------+ +----------------------+ |
| p --> | v-table pointer | --> | QueryInterface | |
| +-------------------+ +----------------------+ |
| | AddRef | |
| +----------------------+ |
| | Release | |
| +----------------------+ |
| | GetTypeInfoCount | |
| +----------------------+ |
| | GetTypeInfo | |
| +----------------------+ |
| | GetIDsOfNames | |
| +----------------------+ |
| | Invoke | |
| +----------------------+ |
+----------------------------------------------------------------+
```

The `IExampleCom` interface extends `IDispatch`, inheriting all its methods and
adding its own:

```plaintext
+----------------------------------------------------------------+
| IExampleCom Interface Memory Layout |
+----------------------------------------------------------------+
| +-------------------+ +----------------------+ |
| p --> | v-table pointer | --> | QueryInterface | |
| +-------------------+ +----------------------+ |
| | AddRef | |
| +----------------------+ |
| | Release | |
| +----------------------+ |
| | GetTypeInfoCount | |
| +----------------------+ |
| | GetTypeInfo | |
| +----------------------+ |
| | GetIDsOfNames | |
| +----------------------+ |
| | Invoke | |
| +----------------------+ |
| | GetMessage | |
| +----------------------+ |
| | GetSum | |
| +----------------------+ |
+----------------------------------------------------------------+
```

[IDispatch]: https://learn.microsoft.com/windows/win32/api/oaidl/nn-oaidl-idispatch
[regasm]: https://learn.microsoft.com/dotnet/framework/tools/regasm-exe-assembly-registration-tool
Loading

0 comments on commit 173e655

Please sign in to comment.