起因

最近做的 Java 项目有在 Windows 下创建快捷方式的需求,需要调用 COM 接口去实现,刚好项目里也有使用 JNA,因此记录一下通过 JNA 调用 IShellLinkW 这个 COM 接口的方法。

IShellLinkW 这个接口是直接从 IUnknown 继承的,所以 JNA 不能帮我们自动去寻找接口的方法,因此需要手动去做。

开始

首先创建一个继承 Unknown 的新类。

1
2
3
4
5
6
7
8
import com.sun.jna.platform.win32.COM.Unknown;
import com.sun.jna.platform.win32.Guid;

public class IShellLink extends Unknown {
    /* ShellLink 的 CLSID 以及 IShellLinkW 的 IID */
    public static final Guid.GUID CLSID_ShellLink = new Guid.GUID("{00021401-0000-0000-c000-000000000046}");
    public static final Guid.GUID IID_IShellLinkW = new Guid.GUID("{000214F9-0000-0000-c000-000000000046}");
}

编写 create 方法,用于创建 IShellLink 的实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private IShellLink(Pointer ptr) {
    super(ptr); // 通过刚才获取的 IShellLink 实例的指针初始化 Unknown 类
}

public static IShellLink create() {
    PointerByReference p = new PointerByReference();
    WinNT.HRESULT hr = Ole32.INSTANCE.CoCreateInstance(CLSID_ShellLink, Pointer.NULL, WTypes.CLSCTX_INPROC_SERVER, IID_IShellLinkW, p); // 创建 IShellLink 实例
    COMUtils.checkRC(hr); // 检查是否成功,若失败则会抛出 COMException 并给出原因
    return new IShellLink(p.getValue());
}

查找 vtable 索引

这里还需要准备下 Windows SDK,我们需要其中的 ShObjIdl_core.h 头文件,它将用于查找我们需要用到的 vtable 索引。

可以通过 Visual Studio Installer 获取这些头文件,也可以通过搜索引擎去寻找这些头文件。

在头文件中查找 IShellLinkW,并在 C style interface 附近查找,它以正确的顺序包含接口所有继承的方法。

以下为 ShObjIdl_core.h 的片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#else 	/* C style interface */

    typedef struct IShellLinkWVtbl
    {
        BEGIN_INTERFACE
        
        HRESULT ( STDMETHODCALLTYPE *QueryInterface )( 
            __RPC__in IShellLinkW * This,
            /* [in] */ __RPC__in REFIID riid,
            /* [annotation][iid_is][out] */ 
            _COM_Outptr_  void **ppvObject);
        
        ULONG ( STDMETHODCALLTYPE *AddRef )( 
            __RPC__in IShellLinkW * This);
        
        ULONG ( STDMETHODCALLTYPE *Release )( 
            __RPC__in IShellLinkW * This);
        
        HRESULT ( STDMETHODCALLTYPE *GetPath )( 
            __RPC__in IShellLinkW * This,
            /* [size_is][string][out] */ __RPC__out_ecount_full_string(cch) LPWSTR pszFile,
            /* [in] */ int cch,
            /* [unique][out][in] */ __RPC__inout_opt WIN32_FIND_DATAW *pfd,
            /* [in] */ DWORD fFlags);
        
        HRESULT ( STDMETHODCALLTYPE *GetIDList )( 
            __RPC__in IShellLinkW * This,
            /* [out] */ __RPC__deref_out_opt PIDLIST_ABSOLUTE *ppidl);
        
        HRESULT ( STDMETHODCALLTYPE *SetIDList )( 
            __RPC__in IShellLinkW * This,
            /* [unique][in] */ __RPC__in_opt PCIDLIST_ABSOLUTE pidl);
        
        HRESULT ( STDMETHODCALLTYPE *GetDescription )( 
            __RPC__in IShellLinkW * This,
            /* [size_is][string][out] */ __RPC__out_ecount_full_string(cch) LPWSTR pszName,
            int cch);
        
        HRESULT ( STDMETHODCALLTYPE *SetDescription )( 
            __RPC__in IShellLinkW * This,
            /* [string][in] */ __RPC__in_string LPCWSTR pszName);
        
        HRESULT ( STDMETHODCALLTYPE *GetWorkingDirectory )( 
            __RPC__in IShellLinkW * This,
            /* [size_is][string][out] */ __RPC__out_ecount_full_string(cch) LPWSTR pszDir,
            int cch);
        
        HRESULT ( STDMETHODCALLTYPE *SetWorkingDirectory )( 
            __RPC__in IShellLinkW * This,
            /* [string][in] */ __RPC__in_string LPCWSTR pszDir);

SetWorkingDirectory 方法为例,从 0 开始数,它的 vtable 索引为 9。

关联起来

知道了我们要调用的方法的 vtable 索引后,就可以去写方法去调用了。

LPCWSTR 实际上就是 const wchar_t*,对应的 JNA 类型为 WString

1
2
3
4
public void SetWorkingDirectory(WString path) {
    int res = this._invokeNativeInt(9, new Object[]{this.getPointer(), path}); // 参数数组必须传入 IShellLinkW 的指针作为第一个参数
    COMUtils.checkRC(new WinNT.HRESULT(res)); // 检查是否成功
}

获取其他接口

也可以通过 QueryInterface 去获取其他接口,以 IPersistFile 为例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* IPersistFile 的 IID */
public static final Guid.GUID IID_IPersistFile = new Guid.GUID("{0000010B-0000-0000-c000-000000000046}");

public IPersistFile getPF() {
    PointerByReference p = new PointerByReference();
    WinNT.HRESULT hr = this.QueryInterface(new Guid.REFIID(new Guid.IID(IID_IPersistFile)), p);
    COMUtils.checkRC(hr);
    return new IPersistFile(p.getValue());
}

public static class IPersistFile extends Unknown {
    private IPersistFile(Pointer ptr) {
        super(ptr);
    }

    public void Save(String path) {
        int res = this._invokeNativeInt(6, new Object[]{this.getPointer(), new WString(path), true});
        COMUtils.checkRC(new WinNT.HRESULT(res));
    }

    // 其他方法...
}

测试

全部准备好后就可以去调用了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static void main(String[] args) {
    Ole32.INSTANCE.CoInitializeEx(Pointer.NULL, Ole32.COINIT_MULTITHREADED);
    try {
        IShellLink lnk = IShellLink.create(); // 创建 ShellLink
        IPersistFile pf = lnk.getPF(); // 获取 IPersistFile
        String dir = System.getProperty("user.home").replaceAll("\"", "\"\""); // 分隔符的转换
        lnk.SetPath(new WString("C:\\Windows\\System32\\cmd.exe"));
        lnk.SetWorkingDirectory(new WString(dir));
        pf.Save(new WString(dir + "\\Desktop\\Opencmd.lnk")); // 保存快捷方式
        pf.Release();
        lnk.Release(); // 注意资源释放
    } finally {
        Ole32.INSTANCE.CoUninitialize();
    }
}

注意有些时候(如使用 JavaFX 的情况下) CoInitializeEx 已经被调用过了,所以不需要再调用一次,也不要调用 CoUninitialize ,可能会产生未知的问题。

参考

Accessing COM Interface with JNA