Skip to content

Repository files navigation

PCT-FSharp-ComClassImportFix


Fix the COM class with the attribute ComImport problem in F#.

Warning

Snapshot Version

This is a snapshot version. It is probably not stable, like compiler error or no effect to your build.

Usage

[<ComImport>]
[<ComClassImportFix>]  // Add this attribute.
[<Guid("A2A9545D-A0C2-42B4-9708-A0B2BADD77C8")>]
type StartMenuPin() = class end

Install

Download from the release .

Using dotnet CLI to install downloaded packages.

dotnet add package PCT.FSharp.ComClassImportFix --source ".\YouDownloadDir"

Why using it?

In C#, you can write this to declare a COM object. And it works well.

[ComImport]
[Guid("A2A9545D-A0C2-42B4-9708-A0B2BADD77C8")]
public class StartMenuPin { }

If you rewrite this in F# below, you will get an runtime exception TypeLoadException.

[<ComImport>]
[<Guid("A2A9545D-A0C2-42B4-9708-A0B2BADD77C8")>]
type StartMenuPin() = class end

If we decompile it, we will find the different handling method between the C# and F# compiler.

// Decompile from C# implement
[SupportedOSPlatform("windows")]
[Guid("A2A9545D-A0C2-42B4-9708-A0B2BADD77C8")]
[ComImport]
internal class StartMenuPin 
{
    [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    public extern StartMenuPin();
}

// Decompile from F# implement
[ComImport]
[Guid("A2A9545D-A0C2-42B4-9708-A0B2BADD77C8")]
[CompilationMapping(SourceConstructFlags.ObjectType)]
[Serializable]
public class StartMenuPin
{
    public StartMenuPin() 
        : this()
    {
    }
}

If we add [ComImport] attribute, the C# compiler can identify this attribute and generate a extern constructor declaration with [MethodImpl] attribute, to tell CLR that this is a COM invoke and handle it in correct way. But F# compiler cannot handle this feature in a special way and added a default constructor for this class. In other words, F# compiler incorrectly treated this COM class declaration as a regular class. This work will cause an exception TypeLoadException like this output below.

Unhandled exception. System.TypeLoadException: Method with non-zero RVA in an Import.
   at Experiments.FSharp.TaskberComObjectLogic.unpinFromTaskbar(String linkFilePath)
   at <StartupCode$Experiments-FSharp>.$TaskbarComObjectExperiment.Pipe #1 stage #1 at line 164@164.Invoke(String p) in E:\dotNETWork\Experiments\Experiments.FSharp\TaskbarComObjectExperiment.fs:line 164
   at Microsoft.FSharp.Primitives.Basics.List.map[T,TResult](FSharpFunc`2 mapping, FSharpList`1 x) in D:\a\_work\1\s\src\fsharp\src\FSharp.Core\local.fs:line 247
   at Microsoft.FSharp.Collections.ListModule.Map[T,TResult](FSharpFunc`2 mapping, FSharpList`1 list) in D:\a\_work\1\s\src\fsharp\src\FSharp.Core\list.fs:line 97
   at Experiments.FSharp.TaskbarComObjectExperiment.runPlayground(FSharpList`1 args) in E:\dotNETWork\Experiments\Experiments.FSharp\TaskbarComObjectExperiment.fs:line 164
   at Experiments.FSharp.Program.main(String[] _arg1) in E:\dotNETWork\Experiments\Experiments.FSharp\Program.fs:line 12

We use Mono.Cecil to fix the assembly after build in build process. Tools remove the default constructor of the COM class declaration and add the correct one to fix this problem like the decompiled code below.

[ComImport]
[ComClassImportFix]  // Only add this!
[Guid("A2A9545D-A0C2-42B4-9708-A0B2BADD77C8")]
[CompilationMapping(SourceConstructFlags.ObjectType)]
[Serializable]
public class StartMenuPin
{
	[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
	public extern StartMenuPin();  // Correct constructor declaration
}

This tool is easy to use. You only need to add an attribute [ComClassImportFix] and wait the compiler to build it.

Limitation

  1. This is a snapshot version. It is probably not stable, like compiler error or no effect to your build.

  2. This tool only identify the instance class with [ComClassImportFix] and [ComImport] , not static class, abstrct class, interface and so on. If you add [ComClassImportFix] to normal class, this class will be skipped.

    However, there is indeed a known issue about the COM interface declaration. This is due to F#'s type inference not automatically recognising the relationship between the class and its interface.

    // Interface declaration
    [<ComImport>]
    [<Interface>]
    [<Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")>]
    [<InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)>]
    type IShellItem = interface end
    
    [<ComImport>]
    [<Interface>]
    [<Guid("4CD19ADA-25A5-4A32-B3B7-347BEE5BE36B")>]
    [<InterfaceType(ComInterfaceType.InterfaceIsIUnknown)>]
    type IStartMenuPinnedList =
        abstract member RemoveFromList : [<MarshalAs(UnmanagedType.Interface)>] pIShellItem : obj -> int
    
    let pinItem = StartMenuPin()
    match pinItem with   // FS0193: Type constraint mismatch. The type 'IStartMenuPinnedList' is not compatible with type 'StartMenuPin'.
        | :? IStartMenuPinnedList as pinnedList -> (* Do somthing *)
        | _ -> (* Handle other value types. *)

    There is no tools can fix this problem. To solve this error, you should use box function to cast the value type to obj

    let pinItem = StartMenuPin()
    match box pinItem with   // Correct!
        | :? IStartMenuPinnedList as pinnedList -> (* Do somthing *)
        | _ -> (* Handle other value types. *)

Supported .NET Versions

Although this project support .NET Standard 2.0, we suggest you using it on .NET version 5~10. And it only supports on Windows platform.

Report Bugs

This project is developing. It probably contains some bugs. Welcome to submit an issue!

About

Fix the COM class with the attribute ComImport problem in F#

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages