Boogadi
Boogadi

Reputation: 43

Call SHGetImageList in Go

I'm trying to call SHGetImageList in Go in order to extract icons from .EXE files. The problem is that I don't know how to create/pass an "IImageList2 interface" in Go, which is something SHGetImageList requires.

I've tried miscellaneous things for a few hours, but it all results in the same E_NOINTERFACE error. Basically they're all "shots in the dark" ([]byte array to see if I could receive any "data" at all, an actual interface{} in Go containing the same functions as the IImagelist2 interface defined by MSDN, etc). If it is of any relevance, I do have a working version of this in C# using something along the lines of http://www.pinvoke.net/default.aspx/shell32.shgetimagelist, but I simply have no real clue on how to "translate" that to Go. Any help would be much appreciated.

Example Go code below, with some info and links to MSDN in the comments.

package main

import (
    "fmt"
    "syscall"
    "unsafe"
)

var (
    shell32 = syscall.MustLoadDLL("shell32.dll")

    // https://msdn.microsoft.com/en-us/library/windows/desktop/bb762179(v=vs.85).aspx
    procSHGetFileInfo = shell32.MustFindProc("SHGetFileInfoW")

    //https://msdn.microsoft.com/en-us/library/windows/desktop/bb762185(v=vs.85).aspx
    procSHGetImageList = shell32.MustFindProc("SHGetImageList")
)

func main() {
    someExeFile := `c:\windows\explorer.exe`

    iconIndex := GetIconIndex(someExeFile)

    // The problem:
    HRESULT, _, _ := procSHGetImageList.Call(
        uintptr(SHIL_JUMBO),
        uintptr(unsafe.Pointer(&IID_IImageList2)),

        // I don't know how pass/create an "IImageList interface" in Go,
        // or if it's even possible without relying on CGO.
        // IImageList interface:
        // https://msdn.microsoft.com/en-us/library/windows/desktop/bb761419(v=vs.85).aspx

        // Currently there's just a pointer to an empty []byte so that the code will compile.
        // HRESULT naturally contains the error code E_NOINTERFACE (2147500034),
        // which makes sense seeing as I'm not passing a valid interface.
        uintptr(unsafe.Pointer(&[]byte{})),
    )

    fmt.Println(iconIndex, HRESULT)
}


const SHIL_JUMBO = 0x4

const shGetFileInfoLen = 3
const shGetFileInfoFlags = 16400 //(SysIconIndex|LargeIcon|UseFileAttributes)
// use SHGetFileInfo to get the icon index (only value we care about)
func GetIconIndex(fileName string) int {
    buf := make([]uint16, shGetFileInfoLen)
    ret, _, _ := procSHGetFileInfo.Call(
        uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(fileName))),
        0,
        uintptr(unsafe.Pointer(&buf[0])),
        shGetFileInfoLen,
        shGetFileInfoFlags,
    )

    if ret != 0 && buf[2] > 0 {
        return int(buf[2])
    }

    return 0
}

// From: "192B9D83-50FC-457B-90A0-2B82A8B5DAE1"
var IID_IImageList2 = &GUID{0x192b9d83, 0x50fc, 0x457b, [8]byte{0x90, 0xa0, 0x2b, 0x82, 0xa8, 0xb5, 0xda, 0xe1}}

// http://msdn.microsoft.com/en-us/library/windows/desktop/aa373931.aspx
type GUID struct {
    Data1 uint32
    Data2 uint16
    Data3 uint16
    Data4 [8]byte
}

UPDATE

The problem now is that it's exiting with an error(0xC0000005) when calling the Syscall to get the actual icon pointer.

package main

import (
    "fmt"
    "syscall"
    "unsafe"
)

var (
    shell32 = syscall.MustLoadDLL("shell32.dll")

    // https://msdn.microsoft.com/en-us/library/windows/desktop/bb762179(v=vs.85).aspx
    procSHGetFileInfo = shell32.MustFindProc("SHGetFileInfoW")

    //https://msdn.microsoft.com/en-us/library/windows/desktop/bb762185(v=vs.85).aspx
    procSHGetImageList = shell32.MustFindProc("SHGetImageList")

    ole32 = syscall.MustLoadDLL("ole32.dll")
    procCoInitialize = ole32.MustFindProc("CoInitialize")
)

func main() {
    someExeFile := `c:\windows\explorer.exe`

    procCoInitialize.Call()

    iconIndex := GetIconIndex(someExeFile)

    var imglist *IImageList
    hr, _, _ := procSHGetImageList.Call(
        uintptr(SHIL_JUMBO),
        uintptr(unsafe.Pointer(&IID_IImageList)),
        uintptr(unsafe.Pointer(&imglist)),
    )

    // These look OK
    fmt.Println(iconIndex, hr, imglist.Vtbl.GetIcon)

    var hIcon uintptr
    // GetIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb761463(v=vs.85).aspx
    hr, _, _ = syscall.Syscall(imglist.Vtbl.GetIcon,
        uintptr(unsafe.Pointer(imglist)),
        uintptr(iconIndex),
        getIconFlags,
        uintptr(unsafe.Pointer(&hIcon)),
    )

    // Errors: "Process finished with exit code -1073741819 (0xC0000005)"

    fmt.Println("hIcon:", hIcon) // Never reaches this
}

// ILD_TRANSPARENT | ILD_IMAGE
const getIconFlags = 0x00000001 | 0x00000020

const SHIL_JUMBO = 0x4

const shGetFileInfoLen = 3
const shGetFileInfoFlags = 16400 //(SysIconIndex|LargeIcon|UseFileAttributes)
// use SHGetFileInfo to get the icon index (only value we care about)
func GetIconIndex(fileName string) int {
    buf := make([]uint16, shGetFileInfoLen)
    ret, _, _ := procSHGetFileInfo.Call(
        uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(fileName))),
        0,
        uintptr(unsafe.Pointer(&buf[0])),
        shGetFileInfoLen,
        shGetFileInfoFlags,
    )

    if ret != 0 && buf[2] > 0 {
        return int(buf[2])
    }

    return 0
}


// From: "46EB5926-582E-4017-9FDF-E8998DAA0950"
var IID_IImageList = GUID{0x46eb5926, 0x582e, 0x4017, [8]byte{0x9f, 0xdf, 0xe8, 0x99, 0x8d, 0xaa, 0x09, 0x50}}

// http://msdn.microsoft.com/en-us/library/windows/desktop/aa373931.aspx
type GUID struct {
    Data1 uint32
    Data2 uint16
    Data3 uint16
    Data4 [8]byte
}

type IImageList struct {
    Vtbl *IImageListVtbl
}

type IImageListVtbl struct {
    Add                uintptr
    ReplaceIcon        uintptr
    SetOverlayImage    uintptr
    Replace            uintptr
    AddMasked          uintptr
    Draw               uintptr
    Remove             uintptr
    GetIcon            uintptr
    GetImageInfo       uintptr
    Copy               uintptr
    Merge              uintptr
    Clone              uintptr
    GetImageRect       uintptr
    GetIconSize        uintptr
    SetIconSize        uintptr
    GetImageCount      uintptr
    SetImageCount      uintptr
    SetBkColor         uintptr
    GetBkColor         uintptr
    BeginDrag          uintptr
    EndDrag            uintptr
    DragEnter          uintptr
    DragLeave          uintptr
    DragMove           uintptr
    SetDragCursorImage uintptr
    DragShowNolock     uintptr
    GetDragImage       uintptr
    GetItemFlags       uintptr
    GetOverlayImage    uintptr
}

Upvotes: 4

Views: 544

Answers (2)

andlabs
andlabs

Reputation: 11588

Oh, I see the actual problem now.

        uintptr(unsafe.Pointer(&IID_IImageList2)),
...
var IID_IImageList2 = &GUID{0x192b9d83, 0x50fc, 0x457b, [8]byte{0x90, 0xa0, 0x2b, 0x82, 0xa8, 0xb5, 0xda, 0xe1}}

Your IID_IImageList2 is already a pointer. In your call, you're taking a pointer to that pointer, which means the address is used as the GUID. You should either do

        uintptr(unsafe.Pointer(&IID_IImageList2)),
...
var IID_IImageList2 = GUID{0x192b9d83, 0x50fc, 0x457b, [8]byte{0x90, 0xa0, 0x2b, 0x82, 0xa8, 0xb5, 0xda, 0xe1}}

or

        uintptr(unsafe.Pointer(IID_IImageList2)),
...
var IID_IImageList2 = &GUID{0x192b9d83, 0x50fc, 0x457b, [8]byte{0x90, 0xa0, 0x2b, 0x82, 0xa8, 0xb5, 0xda, 0xe1}}

That way, the GUID itself is used as the GUID, not its location in memory.

Upvotes: 2

andlabs
andlabs

Reputation: 11588

Interfacing COM with Go is going to be painful.

A COM interface like IImageList2 is a list of function pointers, and you can't use those C function pointers from Go directly; you have to use syscall.Syscall() and its siblings (depending on the number of arguments the individual function takes) to call them.

At its core, a COM interface instance is a structure whose first and only field is a pointer to this list of methods. So it'd be something along the lines of

type IImageList2Vtbl struct {
    QueryInterface uintptr
    AddRef         uintptr
    Release        uintptr
|

type IImageList2 struct {
    Vtbl *IImageList2Vtbl
}

When you pass a pointer to a variable of type IImageList2 to the Windows API function you call to create that object, be it SHGetImageList(), CoCreateInstance(), D3D11CreateDeviceAndSwapChain(), or what have you, the system will fill in the Vtbl entry with a pointer in read-only system memory that contains the list of functions.

So the first thing you have to do is make sure the methods are in the right order, so the Go structure IImageList2Vtbl and the list that Windows will give you match. Unfortunately, MSDN isn't good at this; you'll have to spelunk through the header files. Here's what I get:

type IImageList2Vtbl struct {
    QueryInterface       uintptr
    AddRef               uintptr
    Release              uintptr
    Add                  uintptr
    ReplaceIcon          uintptr
    SetOverlayImage      uintptr
    Replace              uintptr
    AddMasked            uintptr
    Draw                 uintptr
    Remove               uintptr
    GetIcon              uintptr
    GetImageInfo         uintptr
    Copy                 uintptr
    Merge                uintptr
    Clone                uintptr
    GetImageRect         uintptr
    GetIconSize          uintptr
    SetIconSize          uintptr
    GetImageCount        uintptr
    SetImageCount        uintptr
    SetBkColor           uintptr
    GetBkColor           uintptr
    BeginDrag            uintptr
    EndDrag              uintptr
    DragEnter            uintptr
    DragLeave            uintptr
    DragMove             uintptr
    SetDragCursorImage   uintptr
    DragShowNolock       uintptr
    GetDragImage         uintptr
    GetItemFlags         uintptr
    GetOverlayImage      uintptr
    Resize               uintptr
    GetOriginalSize      uintptr
    SetOriginalSize      uintptr
    SetCallback          uintptr
    GetCallback          uintptr
    ForceImagePresent    uintptr
    DiscardImages        uintptr
    PreloadImages        uintptr
    GetStatistics        uintptr
    Initialize           uintptr
    Replace2             uintptr
    ReplaceFromImageList uintptr
}

I'm sure I got this wrong; please report any errors. :)

This comes from commoncontrols.h. Go with the C-style, not C++-style, interface definitions (if there are more than one), as that will have all the methods, including those of interfaces that IImageList2 derives from (IUnknown, which all interfaces derive from, and IImageList).

Er wait, I lied: Windows doesn't expect you to give it the memory for the IImageList2. That's because COM interfaces are just like Go interfaces: they're a set of methods that any implementation can implement. So in reality you have to let Windows give you a pointer to an IImageList2, not an IImageList2Vtbl.

So what do we do? We store each instance as a pointer to the interface, and then pass a pointer to that to the creation functions.

So from that, we have

var imglist *IImageList2
hr, _, _ := procSHGetImageList.Call(
    uintptr(SHIL_JUMBO),
    uintptr(unsafe.Pointer(&IID_IImageList2)),
    uintptr(unsafe.Pointer(&imglist)),
)

Notice that we pass a pointer to the IImageList2. I'll call this the instance, and the Vtbl member the vtable.

Now when you want to call a method, you have to add an extra first parameter: the instance itself. You can look on MSDN for the return types and other parameters:

// HRESULT EndDrag(void);
hr, _, _ = syscall.Syscall(instance.Vtbl.EndDrag,
    uintptr(unsafe.Pointer(imglist)))
// HRESULT SetOverlayImage(int, int);
hr, _, _ = syscall.Syscall(instance.Vtbl.SetOverlayImage,
    uintptr(unsafe.Pointer(imglist)),
    4,
    2)

Notice that we pass imglist this time, not &imglist.

Now because C and C++ don't support multiple returns like Go does, functions typically return a HRESULT (the COM equivalent of Go's error) and have you pass in pointers to the rest of the return values at the end of the argument list. For other COM interfaces, we follow the rules above. For other type, we refer to the Windows Data Types page to see what each named type represents, and remember that C int is always Go int32 on Windows, even on 64-bit systems.

// HRESULT AddMasked(HBITMAP, COLORREF, (OUT) int *);
// we'll use uintptr for HBITMAP and int32 for COLORREF
var index int32
hr, _, _ = syscall.Syscall(instance.Vtbl.AddMasked,
    uintptr(unsafe.Pointer(instance)),
    hbitmap,
    mask,
    uintptr(unsafe.Pointer(&index)))

Finally, COM requires us to call the Release() method when we're done with an object. This takes no extra parameters and its return value is irrelevant. You can stuff this in a defer if you want.

syscall.Syscall(instance.Vtbl.Release,
    uintptr(unsafe.Pointer(instance)))

Note: I don't know if SHGetImageList() requires COM to be initialized. If it doesn't, ignore this part until you need some other COM interface.

Oh but that's not enough, because you also have to initialize COM. And COM can operate in a number of threading models. Only two are important: single-threaded apartment, which is used for anything related to GUI, and multithreaded apartment, which is used for special purposes.

The CoInitialize(), CoInitializeEx(), and CoUninitialize() functions all handle COM initialization and uninitialization. You already know how to call them; they're just normal DLL functions.

But beware! If you need a single-threaded apartment, you have to use runtime.LockOSThread() to ensure Go doesn't move the current goroutine to another OS thread from under you. If you don't do this, things will break in bizarre ways.


This is all a load of work, and you have to do it for every interface you're going to use. You might as well use a package that someone already made that does the heavy lifting for you. There are several packages, such as go-ole, that do the basic stuff. I don't see one that provides IImageList2, but you might be able to piggyback off an existing one. There seems to be only one reference for IImageList.

Good luck!

Upvotes: 2

Related Questions