5

I have the following C# method:

private static bool IsLink(string shortcutFilename)
{
    var pathOnly = Path.GetDirectoryName(shortcutFilename);
    var filenameOnly = Path.GetFileName(shortcutFilename);

    var shell = new Shell32.Shell();
    var folder = shell.NameSpace(pathOnly);
    var folderItem = folder.ParseName(filenameOnly);
    return folderItem != null && folderItem.IsLink;
}

I have tried converting this to F# as:

let private isLink filename =
    let pathOnly = Path.GetDirectoryName(filename)
    let filenameOnly = Path.GetFileName(filename)
    let shell = new Shell32.Shell()
    let folder = shell.NameSpace(pathOnly)
    let folderItem = folder.ParseName(filenameOnly)
    folderItem <> null && folderItem.IsLink

It however reports an error for the let shell = new Shell32.Shell() line, saying that new cannot be used on interface types.

Have I just made a silly syntactic mistake, or is there extra work needed to access COM from F#?

Guy Coder
  • 24,501
  • 8
  • 71
  • 136
David Arno
  • 42,717
  • 16
  • 86
  • 131
  • 1
    I just thought I'd double check a popular search engine for an answer. Searched "f# com shell32" and it came up with this question as the top result!?!! Guess this isn't a common F# use-case :) – David Arno May 10 '16 at 17:38
  • 4
    The F# compiler is probably missing the [CoClass] attribute lookup feature. Won't support embedding the interop types either. The workaround is to use `new Shell32.Shell32Class()` Or just write it in a C# class library and add a reference to it, I suspect everybody does that. – Hans Passant May 10 '16 at 17:42
  • @HansPassant, thanks, that pointed me in the right direction. Changing the line to `let shell = new Shell32.ShellClass()` fixed the problem and it all works as expected. Now to work out *why* that worked, as I don't like to take the "oh, that fixed it, now to move on" approach; I need to know! :) – David Arno May 10 '16 at 18:03
  • 1
    https://github.com/dotnet/coreclr/blob/master/src/vm/interoputil.cpp#L2879 – Hans Passant May 10 '16 at 18:16
  • Brilliant, thanks @HansPassant, you are a star. If you want to put that all into an answer, I'll mark it as accepted. – David Arno May 10 '16 at 19:17

1 Answers1

9

I don't know enough about the F# compiler but your comments makes it obvious enough. The C# and VB.NET compilers have a fair amount of explicit support for COM built-in. Note that your statement uses the new operator on an interface type, Shell32.Shell in the interop library looks like this:

[ComImport]
[Guid("286E6F1B-7113-4355-9562-96B7E9D64C54")]
[CoClass(typeof(ShellClass))]
public interface Shell : IShellDispatch6 {}

IShellDispatch6 is the real interface type, you can also see the IShellDispatch through IShellDispatch5 interfaces. That's versioning across the past 20 years at work, COM interface definitions are immutable since changing them almost always causes an undiagnosable hard crash at runtime.

The [CoClass] attribute is the important one for this story, that's what the C# compiler goes looking for you use new on a [ComImport] interface type. Tells it to create the object by creating an instance of Shell32.ShellClass instance and obtain the Shell interface. What the F# compiler doesn't do.

ShellClass is a fake class, it is auto-generated by the type library importer. COM never exposes concrete classes, it uses a hyper-pure interface-based programming paradigm. Objects are always created by an object factory, CoCreateInstance() is the workhorse for that. Itself a convenience function, the real work is done by the universal IClassFactory interface, hyper-pure style. Every COM coclass implements its CreateInstance() method.

The type library importer makes ShellClass look like this:

[ComImport]
[TypeLibType(TypeLibTypeFlags.FCanCreate)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("13709620-C279-11CE-A49E-444553540000")]
public class ShellClass : IShellDispatch6, Shell {
    // Methods
    [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType=MethodCodeType.Runtime), DispId(0x60040000)]
    public virtual extern void AddToRecent([In, MarshalAs(UnmanagedType.Struct)] object varFile, [In, Optional, MarshalAs(UnmanagedType.BStr)] string bstrCategory);
    // Etc, many more methods...
}

Lots of fire and movement, none of it should ever be used. The only thing that really matters is the [Guid] attribute, that provides the CLSID that CoCreateInstance() needs. It also needs the IID, the [Guid] of the interface, provided by the interface declaration.

So the workaround in F# is to create the Shell32.ShellClass object, just like the C# compiler does implicitly. While technically you can keep the reference in a ShellClass variable, you should strongly favor the interface type instead. The COM way, the pure way, it avoids this kind of problem. Ultimately it is the CLR that gets the job done, it recognizes the [ClassInterface] attribute on the ShellClass class declaration in its new operator implementation. The more explicit way in .NET is to use Type.GetTypeFromCLSID() and Activator.CreateInstance(), handy when you only have the Guid of the coclass.

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • 3
    For those interested in the machinery behind this, Jon Skeet's blog post, [Faking COM to fool the C# compiler](https://codeblog.jonskeet.uk/2009/07/07/faking-com-to-fool-the-c-compiler/) discusses this topic from a different perspective. – Brian May 12 '16 at 13:12