With JDK22+ you can use pure Java implementation that uses the Foreign Function and Memory API to call directly to the built in Windows libraries for shell link. There should be no errors with binary incompatibility (unless Microsoft adds one), which would be a risk for all other approaches.
This example program looks up the Windows CLSID for ShellLink and retrieves the IShellLinkW and IPersistFile COM interfaces to load a Windows shortcut ".lnk" file. Then you just need to retrieve all properties eg GetPath. This sample is quite long so for sake of brevity I've left out reading other properties such as arguments, and other operations such as Save.
public record WindowsShortcut(Path lnk, Path target) { }
/** Load a shortcut .lnk file using COM CLSID_ShellLink object */
public static WindowsShortcut load(Path file) throws IOException {
// Performs the various COM operations using same native memory arena
try(Arena arena = Arena.ofConfined()) {
// Global GUIDs definitions are part of uuid.lib not bound in DLL
var CLSID_ShellLink = arena.allocateFrom(JAVA_BYTE, // "{00021401-0000-0000-C000-000000000046}"
new byte[] {0x1,0x14,0x2,0x0, 0x0,0x0, 0x0,0x0, (byte) 0xC0,0x0,0x0,0x0,0x0,0x0,0x0,0x46});
var IID_IShellLink = arena.allocateFrom(JAVA_BYTE, // "{000214F9-0000-0000-C000-000000000046}"
new byte[] {(byte)0xF9,0x14,0x2,0x0,0x0,0x0,0x0,0x0,(byte)0xC0,0x0,0x0,0x0,0x0,0x0,0x0,0x46});
// Interface IID_Persist
var IID_IPersistFile = arena.allocateFrom(JAVA_BYTE, // "{0000010B-0000-0000-C000-000000000046}"
new byte[] {0xB,0x1,0x0,0x0,0x0,0x0,0x0,0x0,(byte)0xC0,0x0,0x0,0x0,0x0,0x0,0x0,0x46});
// Allocate memory to receive pointer to ADDRESS, re-used
MemorySegment pAddress = arena.allocateFrom(Shortcut_h.C_POINTER, MemorySegment.NULL);
// String for the pathname of lnk, resolved to absolute path
MemorySegment pszFileName = arena.allocateFrom(file.toRealPath().toString(), StandardCharsets.UTF_16LE);
// Setup COM:
int hRes = Shortcut_h.CoInitialize(MemorySegment.NULL);
Objects.checkIndex(hRes, 2); // checks Shortcut_h.S_OK(), Shortcut_h.S_FALSE()
try {
// Get a pointer to the IShellLink interface.
int cciRes = Shortcut_h.CoCreateInstance(CLSID_ShellLink, /*pUnkOuter*/MemorySegment.NULL, /*dwClsContext*/Shortcut_h.CLSCTX_INPROC_SERVER(), /*riid*/IID_IShellLink, pAddress);
Objects.checkIndex(cciRes, 1);
// Wrap as Java ShellLink
try (var iShellLink = new JShellLink(pAddress.get(ADDRESS, 0))) {
// Query IShellLink for the IPersistFile interface for load/save
try (var iPersist = new JPersistFile(/*IPersistFile*/ iShellLink.QueryInterface(IID_IPersistFile, pAddress))) {
// Load the shortcut lnk file
iPersist.Load(pszFileName);
// Load properties of LNK: TODO: Setup other fields here such as Arguments IDPath etc
String target = iShellLink.GetPath(arena, arena.allocate(JAVA_BYTE, Shortcut_h.MAX_PATH()));
return new WindowsShortcut(file, target != null ? Path.of(target) : null);
}
}
} finally {
Shortcut_h.CoUninitialize();
}
}
}
public static void main(String ... args) throws IOException {
var shortcut = WindowsShortcut.load(Path.of(args[0]));
System.out.println("shortcut => "+shortcut);
}
In order to get the simple method above, I refactored various actions into Java helper classes so can re-use this style of callbacks elsewhere in my own code:
/** COM definition holds pointers to OBJ and VTABLE (matching the size for IXYZ table) */
record COM(MemorySegment obj, MemorySegment vtab) {}
/** JUnknown handles all operations on IUnknown */
class JUnknown implements AutoCloseable {
protected final COM com;
/** Derived classes must use this constructor with COM object definition with appropriate memory vtable size. */
protected JUnknown(final MemorySegment comObj, long vTableSize) {
// retrieve IUnknown structure from the provided comObj segment
// retrieve vtable of size appropriate to the caller eg I{xyz}Vtbl.sizeof()
this.com = new COM(comObj, IUnknown.lpVtbl(comObj.reinterpret(IUnknown.sizeof())).reinterpret(vTableSize));
}
/** IUnknown Release */
public int Release() {
return IUnknownVtbl.Release.invoke(IUnknownVtbl.Release(com.vtab()), com.obj());
}
/** IUnknown QueryInterface -> returns requested pInterface */
public MemorySegment QueryInterface(MemorySegment riid, MemorySegment ppv) {
int qiRes = IUnknownVtbl.QueryInterface.invoke(IUnknownVtbl.QueryInterface(com.vtab()), com.obj(), riid, ppv);
Objects.checkIndex(qiRes, 1); // check 0
return ppv.get(Shortcut_h.C_POINTER, 0);
}
/** Autoclose - calls Release to avoid leaking refs to COM objects. */
public void close() {
Release();
}
}
/** JShellLink handles all operations on IShellLink */
class JShellLink extends JUnknown {
/** Setup IShellLink for the COM object given */
public JShellLink(final MemorySegment comObj) {
super(comObj, IShellLinkWVtbl.sizeof());
}
/**IShellLinkW::GetPath method (shobjidl_core.h) */
public String GetPath(Arena arena, MemorySegment path83) {
// Get the path to the link target which is converted from 8.3 format
// An error here might be target file not found
// Ignores WIN32_FIND_DATAA *pfd for now
int hRes = IShellLinkWVtbl.GetPath.invoke(IShellLinkWVtbl.GetPath(com.vtab()), com.obj(), path83, (int)path83.byteSize(), /*pfd*/ MemorySegment.NULL, /*fFlags*/Shortcut_h.SLGP_SHORTPATH());
Objects.checkIndex(hRes, 2); // check 0/1
String target = path83.getString(0, StandardCharsets.UTF_16LE);
if (hRes == Shortcut_h.S_OK()) {
// LNK Target appears to be 8.3 so convert it to long format
int nChars = Shortcut_h.MAX_PATH();
final MemorySegment lpszLongPath = arena.allocate(Shortcut_h.WCHAR, nChars); // Max: Kernel32_h.UNICODE_STRING_MAX_CHARS()
int len = Shortcut_h.GetLongPathNameW(path83, lpszLongPath, nChars);
// len returned is = number of chars copied OR size of buffer required to hold "number of chars + NULL
Objects.checkIndex(len, nChars); // check in bounds
target = lpszLongPath.getString(0, StandardCharsets.UTF_16LE);
}
return target;
}
}
/** JPersistFile handles all operations on IPersistFile */
class JPersistFile extends JUnknown {
/** Setup IPersistFile for the COM object given */
public JPersistFile(final MemorySegment comObj) {
super(comObj, IPersistFileVtbl.sizeof());
}
/** IPersistFile::Load method (objidl.h) */
public void Load(MemorySegment pszFileName) {
int hRes = IPersistFileVtbl.Load.invoke(IPersistFileVtbl.Load(com.vtab()), com.obj(), pszFileName, Shortcut_h.STGM_READ());
Objects.checkIndex(hRes, 1); // check 0
}
}
Finally, all of the above requires more code which is generated by jextract to access the binary layouts of each Windows API data structure used. You of course need Windows API header files, which is available if you download Visual Studio. The command I used was:
jextract -lshell32 -lkernel32 -lole32 -t gen.code.shortcut --output junit --dump-includes Shortcut.sym
Then edited Shortcut.sym to be the symbols used above keeping only required symbol of each Shortcut_h.xxxx call referenced, then re-run with:
jextract -lshell32 -lkernel32 -lole32 -t gen.code.shortcut --output junit --header-class-name Shortcut_h @Shortcut.sym "<shlobj_core.h>"