Skip to content

dscom, a toolkit for creating and registering type libraries (tlb) and additional interop helper methods for COM

License

Notifications You must be signed in to change notification settings

dspace-group/dscom

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dSPACE COM tools

Nuget:Cli
Nuget:Lib
Nuget:LibBuildTask

Release License dSPACE

Unit Tests Example Tests Code Style Check

The command line client dscom is a replacement for tlbexp.exe and creates and registers TLBs from .NET assemblies.
The dSPACE.Runtime.InteropServices library contains various classes and methods for COM.
It can be used in net5+ or in net48 projects. With the library you can register assemblies and classes for COM and programmatically generate TLBs at runtime.
The dSPACE.Runtime.InteropServices.BuildTasks library provides build tasks which can be used to automatically generate TLBs at compile time.

Introduction

Fortunately, .NET still supports COM, but there is no support for generating TLBs.
From the Microsoft documentation:

Unlike in .NET Framework, there is no support in .NET Core or .NET 5+ for generating a COM Type Library (TLB) from a .NET assembly.

https://docs.microsoft.com/en-us/dotnet/core/native-interop/expose-components-to-com

One main goal is to make dscom behave like tlbexp.exe.

Also, some classes are missing in .NET 5+ that were available in the full framework. This is where dSPACE.Runtime.InteropServices may be able to help.

Command Line Client

The command-line interface (CLI) tool dscom is a replacement for tlbexp.exe and OleView (View TypeLib).

It supports the following features:

  • Convert an assembly to a type library
    • Optionally embed the generated type library into the converted assembly
  • Convert a type library to YAML file
  • Register a type library
  • Unregister a type library
  • Embeds a type library into an existing assembly

Installation

The installation is quite simple. You can use dotnet tool to install the dscom binary if you want to create a 64Bit TLB.

dotnet tool install --global dscom

Here you can find all available versions:
https://www.nuget.org/packages/dscom/

Alternatively you can download dscom.exe from the relase page.
https://github.com/dspace-group/dscom/releases

Usage

Use dscom --help to get further information.

c:\> dscom --help
Description:
  dSPACE COM tools

Usage:
  dscom [command] [options]

Options:
  --version       Show version information
  -?, -h, --help  Show help and usage information

Commands:
  tlbexport <Assembly>                           Export the assembly to the specified type library
  tlbdump <TypeLibrary>                          Dump a type library
  tlbregister <TypeLibrary>                      Register a type library
  tlbunregister <TypeLibrary>                    Unregister a type library
  tlbembed <SourceTypeLibrary> <TargetAssembly>  Embeds a source type library into a target file

Library

Usage:

dotnet add package dSPACE.Runtime.InteropServices

dSPACE.Runtime.InteropServices supports the following methods and classes:

  • TypeLibConverter
    • ConvertAssemblyToTypeLib
  • TypeLibEmbedder
    • EmbedTypeLib
  • RegistrationServices
    • RegisterTypeForComClients
    • UnregisterTypeForComClients
    • RegisterAssembly
    • UnregisterAssembly

TypeLibConverter.ConvertAssemblyToTypeLib

If you miss the TypeLibConverter class and the ConvertAssemblyToTypeLib method in .NET, then the dSPACE.Runtime.InteropServices might help you. This method should behave compatible to the .NET Framework method.

public object? ConvertAssemblyToTypeLib(
  Assembly assembly,
  string tlbFilePath,
  ITypeLibExporterNotifySink? notifySink)

https://www.nuget.org/packages/dSPACE.Runtime.InteropServices/

Example:

using dSPACE.Runtime.InteropServices;

// The assembly to convert
var assembly = typeof(Program).Assembly;

// Convert to assembly
var typeLibConverter = new TypeLibConverter();
var callback = new TypeLibConverterCallback();
var result = typeLibConverter.ConvertAssemblyToTypeLib(assembly, "MyTypeLib.tlb", callback);

// Get the name of the type library
var typeLib2 = result as System.Runtime.InteropServices.ComTypes.ITypeLib2;
if (typeLib2 != null)
{
    typeLib2.GetDocumentation(-1, out string name, out _, out _, out _);
    Console.WriteLine($"TypeLib name: {name}");
}

// The callback to load additional type libraries, if necessary
public class TypeLibConverterCallback : ITypeLibExporterNotifySink
{
    public void ReportEvent(ExporterEventKind eventKind, int eventCode, string eventMsg)
    {
        Console.WriteLine($"{eventCode}: {eventMsg}");
    }

    public object? ResolveRef(System.Reflection.Assembly assembly)
    {
        // Returns additional type libraries
        return null;
    }
}

TypeLibEmbedder.EmbedTypeLib

.NET +6 introduced ability to embed type library into assemblies with the ComHostTypeLibrary property. However, using this is not fully compatible with the dscom build tools as it requires a type library to be already generated prior to the build. This class provides the implementation for embedding a type library into an assembly via Win32 API p/invoke calls.

The class and method are static, so you only need to create a settings to provide parameter for the source type library and the target assembly for where the type library will be embedded.

It is important to note that type libraries are not bit-agnostic and therefore, it will not make sense to embed them in an AnyCPU assemblies. For .NET 5.0 and greater, that is not an issue since the generated *.comhost.dll are tied to a specific bitness. For .NET 4.8, it is strongly recommended that the assembly be built with either x64 or x86 rather than AnyCPU.

public static bool EmbedTypeLib(
    TypeLibEmbedderSettings settings
)

Example:

using dSPACE.Runtime.InteropServices;

var settings = new TypeLibEmbedderSettings
{
    SourceTlbPath = "C:\\path\\to\\type\\library.tlb",
    TargetAssembly = "C:\\path\\to\\assembly.dll"
};
TypeLibEmbedder.EmbedTypeLib(settings);

IMPORTANT: Embedding the type library will alter the assembly, which may cause issues with signing the assembly. Therefore, the scenario of signing the assembly with a certificate or a strong name is not tested. If it is required that the assembly be signed, it is recommended that a build script be used to ensure proper sequence of steps is executed.

RegistrationServices.RegisterTypeForComClients

The dSPACE.Runtime.InteropServices.RegistrationServices provides a set of services for registering and unregistering managed assemblies for use from COM.

This method is equivalent to calling CoRegisterClassObject in COM.
You can register a .NET class so that other applications can connect to it (For example as INPROC_SERVER or as a LOCAL_SERVER).

A outproc demo application is available here: examples\outproc

Example:

using dSPACE.Runtime.InteropServices;

var registration = new RegistrationServices();
var cookie = registration.RegisterTypeForComClients(typeof(Server.Common.Greeter), 
  RegistrationClassContext.LocalServer, 
  RegistrationConnectionType.MultipleUse);

Console.WriteLine($"Press enter to stop the server");
Console.ReadLine();

registration.UnregisterTypeForComClients(cookie);

RegistrationServices.RegisterAssembly

Registers the classes in a managed assembly to enable creation from COM.

using dSPACE.Runtime.InteropServices;

var registration = new RegistrationServices();

// Register MyAssembly
registration.RegisterAssembly(typeof(MyAssembly), true);

// Unregister MyAssembly
registration.UnregisterAssembly(typeof(MyAssembly), true);

Build Tasks

The dSPACE.Runtime.InteropServices.BuildTasks assembly and NuGet package provide the ability to create type libraries for a certain assembly at compile time.

Build task usage

To create a type library at compile time, simply add a reference to the nuget package, e.g. by using the command line.

dotnet add package dSPACE.Runtime.InteropServices.BuildTasks

The result should be a line as follows in your .csproj file:

    <PackageReference Include="dSPACE.Runtime.InteropServices.BuildTasks" Version="0.17.0" NoWarn="NU1701">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>

Note: The extra attribute NoWarn="NU1701" is only required, if neither .NET 4.8 nor .NET 6.0 are targeted, since dotnet pack will currently not create a .NETStandard 2.0 compliant NuGet Package.

Using the native build task

The native build task is automatically selected, if a .NET 4.8 or .NET 6.0 assembly for Windows is being build using an x64 platform.

Using the CLI based task

The CLI task is automatically selected, if a .NET Standard 2.0 assembly is being build. It is also chosen if the target platform is set to x86.

Enforcing the usage of the CLI

It might be necessary to select the CLI based task. To do so, add the following property to your .csproj file:

<_DsComForceToolUsage>true</_DsComForceToolUsage>

This will enforce the usage of the DsCom as a command-line tool. Please note, that verbose logging will no longer be working.

Enforcing to stop the build, if an error occurs

The build tasks puts a warning to the build log, if the desired type library has not been created, even if the backend has reported a success.

This warning is issued with the warning code DSCOM001, which can be collected in the WarningsAsErrors array:

<WarningsAsErrors>$(WarningsAsErrors);DSCOM001</WarningsAsErrors>

This way the build stops, if the type library is not exported.

Parameters

The build task can be parameterized with the following properties:

Name Description
_DsComTlbExt Extension of the resulting type library.
Default Value: .tlb
_DsComForceToolUsage Use DsCom Exe files to create the TLB
Default value: false
DsComTypeLibraryUniqueId Overwrite the library UUID
Default Value: Empty Guid
DsComOverideLibraryName Overwrite the IDL name of the library.
Default Value: Empty string
DsComRegisterTypeLibrariesAfterBuild Use regasm call after the build to register type library after the build
Default value: false
DsComTlbExportAutoAddReferences Add referenced assemblies automatically to type libraries
Default value: true
DsComTlbExportIncludeReferencesWithoutHintPath If a Reference assembly does not provide a HintPath Metadata, the item spec shall be task.
Default value: false
_DsComExportTypeLibraryTargetFile Path to the resulting file.
Default value: $(TargetDir)\$(TargetName)$(_DsComTlbExt) *
_DsComExportTypeLibraryAssemblyFile Path to the source assembly file.
Default value: $(TargetPath) *
DsComTypeLibraryEmbedAfterBuild Embeds the generated type library into the source assembly file.
Default value: false
*) This value cannot be overridden.

The build task consumes the following items:

Name Description
DsComTlbExportTlbReferences Referenced type library files.
DsComTlbExportReferencePaths Directories containing type libraries to use for export.
DsComTlbExportAssemblyPaths Assemblies to add for the export.
DsComTlbAliasNames Names to use as aliases for types with aliases.

Example

<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
      <DsComTypeLibraryUniqueId>f74a0889-2959-4e7b-9a1b-f41c54f31d74</DsComTypeLibraryUniqueId>
   </PropertyGroup>
   <ItemGroup>
    <DsComTlbExportTlbReferences Include="C:\Path\To\My.tlb" />
    <DsComTlbExportAssemblyPaths Include="C:\Path\To\Assemblies\Dependency.dll" />
    <DsComTlbExportReferencePaths Include="C:\Path\To\Additional\TypeLibraries\" />
   </ItemGroup>
</Project>

Hints

When using the build tasks to create an 32 bit version for your type libraries / projects: The tasks require the .NET runtime in their specific CPU architecture to be installed.

So, for building 32 bit TLBs, dscom32 will be executed, which requires .NET runtime in x86 flavor to be available.

32Bit support

dscom installed by dotnet tool install can only handle AnyCPU or 64Bit assemblies and can only generate a 64bit TLB. Depending on whether you want to process 32bit or 64bit assemblies, you need to download different executables from the release page.

  • dscom.exe to create a 64Bit TLB from a AnyCPU or a 64Bit assembly
  • dscom32.exe to create a 32Bit TLB from a AnyCPU or a 32Bit assembly

Warning!
If your assembly is an AnyCPU assembly, then an yourassemblyname.comhost.dll is created as a 64 bit dll.
Therefore after calling regserv32.exe a 64 bit dll is registred.
To prevent this it is recommended that the assembly is compiled as a 32 bit assembly and not as an AnyCPU assembly.
see: dotnet/runtime#32493

Migration notes (mscorelib vs System.Private.CoreLib)

Both assemblies are ComVisible=false but lot of .NET Framework types are ComVisible=true. But this is not the case for .NET (.NET Core and .NET >= 5).
Unlike mscorelib (the good old .NET Framework), no tlb is shipped for .NET.

As example the System.Exception class:

In case of mscorelib the System.Exception class is ComVisible=true:

[Serializable]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(_Exception))]
[ComVisible(true)]
public class Exception : ISerializable, _Exception

In case of System.Private.CoreLib (.NET Core and .NET >=5), the Exception class is ComVisible=false

[Serializable]
[TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
public class Exception : ISerializable
{

The _Exception class interface (default interface in this case) is not available in .NET (.NET Core and .NET >=5).

Why can I load a .NET Framework library into a .NET application?

The magic is in TypeForwardedFromAttribute.
If you try to load an .NET Framework assembly inside a .NET (.NET Core and .NET >=5) application, the runtime will forward the original type to a type defined in the System.Private.CoreLib assembly.

classextern forwarder System.Exception
{
    .assemblyextern System.Private.CoreLib
}

Therefore you should make sure that you do not use any types from the mscorelib typelib in your .NET Framework project if you plan to migrate to .NET 5+

Limitations

RegisterAssembly

  • InProc Registration only will take place, if a comhost assembly is present.
  • No CustomRegisterFunction Or CustomUnRegisterFunction Attribute support
  • No PrimaryInteropAssembly Support