javawinapijavafxtaskbarjava-ffm

Control taskbar in Windows from java using FFM and winapi


I'm trying to control a taskbar so I can show a progress of some long running task in the JavaFX application. For communicating with winapi I want to use the new Java FFM API, which should replace the JNI one day.

So far I was able successfully create instance of ITaskbarList3 instance, but I'm not able to call any method on it.

I'm using jextract to extract functions from winapi to make sure they are correctly mapped to API:

jextract --output target/generated-sources/jextract -t "taskbar_test.gen" -l :shell32 -l :Explorerframe -l :ole32 -I "C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\shared" -I "C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\um" -I "C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\km" -I "C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\km\crt" "C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\um\ShObjIdl_core.h"

In the code below, you can find complete application with my attempt to in the end call function SetProgressValue. My issue is that I'm not able to successfully call function HrInit which should be called to initialize the ITaskbarList.

package taskbar_test;
import com.sun.glass.ui.Window;
import javafx.application.Application;
import javafx.stage.Stage;
import taskbar_test.gen.CLSID;
import taskbar_test.gen.IID;
import taskbar_test.gen.ITaskbarList;
import taskbar_test.gen.ITaskbarList3;
import taskbar_test.gen.ITaskbarList3Vtbl;
import taskbar_test.gen.ITaskbarListVtbl;
import taskbar_test.gen.ShObjIdl_core_h;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
public class FxWinTaskbar extends Application {
     public static final String GUID_FORMAT = "{%s}";
     // CLSID of ITaskbarList3
     public static final String CLSID_CONST = "56FDF344-FD6D-11d0-958A-006097C9A090";
     // IID of ITaskbarList3
     public static final String IID_ITASKBAR_LIST = "56FDF342-FD6D-11d0-958A-006097C9A090";
     public static final String IID_ITASKBAR_LIST_3 = "EA1AFB91-9E28-4B86-90E9-9E9F8A5EEFAF";
     @Override
     public void start(Stage stage) throws Exception {
         var button = new javafx.scene.control.Button("Click Me");
         button.setOnAction(e -> handleClick());
         var root = new javafx.scene.layout.StackPane(button);
         var scene = new javafx.scene.Scene(root, 300, 200);
         stage.setTitle("JavaFX Stage with Button");
         stage.setScene(scene);
         stage.show();
     }
    void handleClick() {
        long rawHandle = Window.getWindows().getFirst().getRawHandle();
        Executors.newSingleThreadExecutor().submit(() -> {
            try (var arena = Arena.ofConfined()) {
                // 1. Initialize variables

                // https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-clsidfromstring#remarks
                // The CLSID format is {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}.
                var clsidString = arena.allocateFrom(GUID_FORMAT.formatted(CLSID_CONST), StandardCharsets.UTF_16LE);
                var iidITaskbarList = arena.allocateFrom(GUID_FORMAT.formatted(IID_ITASKBAR_LIST), StandardCharsets.UTF_16LE);
                var iidITaskbarList3 = arena.allocateFrom(GUID_FORMAT.formatted(IID_ITASKBAR_LIST_3), StandardCharsets.UTF_16LE);
                var clsid = CLSID.allocate(arena);
                var iidTaskbarList = IID.allocate(arena);
                var iidTaskbarList3 = IID.allocate(arena);
                var taskbarPtrToPtr = arena.allocate(ShObjIdl_core_h.C_POINTER);
                var taskbar3PtrToPtr = arena.allocate(ShObjIdl_core_h.C_POINTER);
                MemorySegment windowHandle = arena.allocate(ValueLayout.ADDRESS, rawHandle);

                // 2. Initialize COM
                int hr = ShObjIdl_core_h.CoInitializeEx(MemorySegment.NULL, ShObjIdl_core_h.COINIT_MULTITHREADED());
                if (hr != ShObjIdl_core_h.S_OK()) {
                    throw new RuntimeException("CoInitialize failed with error code: " + hr);
                }

                // 3. Create CLSID and IIDs
                hr = ShObjIdl_core_h.CLSIDFromString(clsidString, clsid);
                if (hr != ShObjIdl_core_h.S_OK()) {
                    throw new RuntimeException("CLSIDFromString failed with error code: " + hr);
                }

                hr = ShObjIdl_core_h.IIDFromString(iidITaskbarList, iidTaskbarList);
                if (hr != ShObjIdl_core_h.S_OK()) {
                    throw new RuntimeException("IIDFromString failed with error code: " + hr);
                }

                hr = ShObjIdl_core_h.IIDFromString(iidITaskbarList3, iidTaskbarList3);
                if (hr != ShObjIdl_core_h.S_OK()) {
                    throw new RuntimeException("IIDFromString failed with error code: " + hr);
                }

                // 4. Create instance of ITaskbarList
                hr = ShObjIdl_core_h.CoCreateInstance(clsid, MemorySegment.NULL, ShObjIdl_core_h.CLSCTX_ALL(), iidTaskbarList, taskbarPtrToPtr);
                if (hr != ShObjIdl_core_h.S_OK()) {
                    if (hr == ShObjIdl_core_h.REGDB_E_CLASSNOTREG()) {
                        System.out.println("COM class is not registered!");
                    }
                    throw new RuntimeException("CoCreateInstance failed with error code: " + hr);
                }
                // CoCreateInstance returns pointer to pointer to ITaskbarList so here we obtain the "inner" pointer
                var taskbarPtr = taskbarPtrToPtr.get(ValueLayout.ADDRESS, 0);
                // Use reinterpret method to have access to the actual ITaskbarList instance
                var taskbarListInstance = ITaskbarList.reinterpret(taskbarPtr, arena, _ -> {
                    System.out.println("Some cleanup...");
                });

                // 5. Obtain lpVtbl pointer from ITaskbarList
                MemorySegment taskbarListVtblPtr = ITaskbarList.lpVtbl(taskbarListInstance);
                // Use reinterpret method to have access to the actual ITaskbarListVtbl instance
                MemorySegment taskbarListVtbl = ITaskbarListVtbl.reinterpret(taskbarListVtblPtr, arena, _ -> {
                    System.out.println("Some cleanup...");
                });

                // 6. Get pointer to function HrInit to initialize ITaskbarList
                // https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist-hrinit
                // Initializes the taskbar list object. This method must be called before any other ITaskbarList methods can be called.
                MemorySegment functionHrInitPtr = ITaskbarListVtbl.HrInit(taskbarListVtbl);
                hr = ITaskbarListVtbl.HrInit.invoke(functionHrInitPtr, taskbarListVtbl);
                if (hr != ShObjIdl_core_h.S_OK()) {
                    throw new RuntimeException("HrInit failed with error code: " + hr);
                }

                // 7. Create instance of ITaskbarList3
                hr = ShObjIdl_core_h.CoCreateInstance(clsid, MemorySegment.NULL, ShObjIdl_core_h.CLSCTX_ALL(), iidTaskbarList3, taskbar3PtrToPtr);
                if (hr != ShObjIdl_core_h.S_OK()) {
                    if (hr == ShObjIdl_core_h.REGDB_E_CLASSNOTREG()) {
                        System.out.println("COM class is not registered!");
                    }
                    throw new RuntimeException("CoCreateInstance failed with error code: " + hr);
                }
                // 8. Obtain a pointer to the instance
                var taskbar3Ptr = taskbar3PtrToPtr.get(ValueLayout.ADDRESS, 0);
                // Use reinterpret method to have access to the actual ITaskbarList3 instance
                var taskbarList3Instance = ITaskbarList3.reinterpret(taskbar3Ptr, arena, _ -> {
                    System.out.println("Some cleanup...");
                });

                // 9. Obtain lpVtbl pointer from ITaskbarList3
                MemorySegment taskbarList3VtblPtr = ITaskbarList3.lpVtbl(taskbarList3Instance);
                // Use reinterpret method to have access to the actual ITaskbarList3Vtbl instance
                MemorySegment taskbarList3Vtbl = ITaskbarList3Vtbl.reinterpret(taskbarList3VtblPtr, arena, _ -> {
                    System.out.println("Some cleanup...");
                });

                // 10. Set progress state to indeterminate
                MemorySegment functionSetProgressStatePtr = ITaskbarList3Vtbl.SetProgressState(taskbarList3Vtbl);
                hr = ITaskbarList3Vtbl.SetProgressState.invoke(functionSetProgressStatePtr, taskbarList3Vtbl, windowHandle, ShObjIdl_core_h.TBPF_INDETERMINATE());
                if (hr != ShObjIdl_core_h.S_OK()) {
                    throw new RuntimeException("SetProgressState failed with error code: " + hr);
                }

            } catch (Throwable ex) {
                ex.printStackTrace();

            } finally {
                ShObjIdl_core_h.CoUninitialize();
            }
        });
    }

    public static void main(String[] args) {
         launch(args);
     }
 }

I'm not able to call the function SetProgressState directly on interface ITaskbarList3 because generated sources does not have the ability to do so. Instead I have to manually obtain vtbl structure and call the function on this structure.

As you can see on the picture below, the address of vtblPtr and function for HrInit are completely off. Calling function HrInit will fail, because it is accesssing wrong memory.

Does anyone have idea what am I doing wrong?

Thank you. Petr

Memory example

Edit: I have applied suggestions from comments. Now, there is only one instance ITaskbarList3 created and all functions are called on it. I have also extended the code to simulate some progress to see if it can set the progress. The code seems to be running, but unfortunately the taskbar is still without any changes.

package taskbar_test;

import com.sun.glass.ui.Window;
import javafx.application.Application;
import javafx.stage.Stage;
import taskbar_test.gen.CLSID;
import taskbar_test.gen.IID;
import taskbar_test.gen.ITaskbarList3;
import taskbar_test.gen.ITaskbarList3Vtbl;
import taskbar_test.gen.ShObjIdl_core_h;

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;

public class FxWinTaskbar extends Application {

    public static final String GUID_FORMAT = "{%s}";

    // CLSID of ITaskbarList3
    public static final String CLSID_CONST = "56FDF344-FD6D-11d0-958A-006097C9A090";
    // IID of ITaskbarList3
    public static final String IID_ITASKBAR_LIST_3 = "EA1AFB91-9E28-4B86-90E9-9E9F8A5EEFAF";

    @Override
    public void start(Stage stage) throws Exception {
        var button = new javafx.scene.control.Button("Click Me");
        button.setOnAction(e -> handleClick());

        var root = new javafx.scene.layout.StackPane(button);
        var scene = new javafx.scene.Scene(root, 300, 200);

        stage.setTitle("JavaFX Stage with Button");
        stage.setScene(scene);
        stage.show();
    }

    void handleClick() {
        long rawHandle = Window.getWindows().getFirst().getRawHandle();
        Executors.newSingleThreadExecutor().submit(() -> {
            try (var arena = Arena.ofConfined()) {
                // 1. Initialize variables

                // https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-clsidfromstring#remarks
                // The CLSID format is {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}.
                var clsidString = arena.allocateFrom(GUID_FORMAT.formatted(CLSID_CONST), StandardCharsets.UTF_16LE);
                var iidITaskbarList3 = arena.allocateFrom(GUID_FORMAT.formatted(IID_ITASKBAR_LIST_3), StandardCharsets.UTF_16LE);
                var clsid = CLSID.allocate(arena);
                var iidTaskbarList3 = IID.allocate(arena);
                var taskbar3PtrToPtr = arena.allocate(ShObjIdl_core_h.C_POINTER);
                MemorySegment windowHandle = arena.allocate(ValueLayout.ADDRESS, rawHandle);

                // 2. Initialize COM
                int hr = ShObjIdl_core_h.CoInitializeEx(MemorySegment.NULL, ShObjIdl_core_h.COINIT_MULTITHREADED());
                if (hr != ShObjIdl_core_h.S_OK()) {
                    throw new RuntimeException("CoInitialize failed with error code: " + hr);
                }

                // 3. Create CLSID and IIDs
                hr = ShObjIdl_core_h.CLSIDFromString(clsidString, clsid);
                if (hr != ShObjIdl_core_h.S_OK()) {
                    throw new RuntimeException("CLSIDFromString failed with error code: " + hr);
                }

                hr = ShObjIdl_core_h.IIDFromString(iidITaskbarList3, iidTaskbarList3);
                if (hr != ShObjIdl_core_h.S_OK()) {
                    throw new RuntimeException("IIDFromString failed with error code: " + hr);
                }

                // 4. Create instance of ITaskbarList3
                hr = ShObjIdl_core_h.CoCreateInstance(clsid, MemorySegment.NULL, ShObjIdl_core_h.CLSCTX_ALL(), iidTaskbarList3, taskbar3PtrToPtr);
                if (hr != ShObjIdl_core_h.S_OK()) {
                    if (hr == ShObjIdl_core_h.REGDB_E_CLASSNOTREG()) {
                        System.out.println("COM class is not registered!");
                    }
                    throw new RuntimeException("CoCreateInstance failed with error code: " + hr);
                }
                // 5. Obtain a pointer to the instance
                var taskbar3Ptr = taskbar3PtrToPtr.get(ValueLayout.ADDRESS, 0);
                // Use reinterpret method to have access to the actual ITaskbarList3 instance
                var taskbarList3Instance = taskbar3Ptr.reinterpret(ITaskbarList3.sizeof());

                // 6. Obtain lpVtbl pointer from ITaskbarList3
                MemorySegment taskbarList3VtblPtr = ITaskbarList3.lpVtbl(taskbarList3Instance);
                // Use reinterpret method to have access to the actual ITaskbarList3Vtbl instance
                MemorySegment taskbarList3Vtbl = taskbarList3VtblPtr.reinterpret(ITaskbarList3Vtbl.sizeof());

                // https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist-hrinit
                // Initializes the taskbar list object. This method must be called before any other ITaskbarList methods can be called.
                MemorySegment functionHrInitPtr = ITaskbarList3Vtbl.HrInit(taskbarList3Vtbl);
                hr = ITaskbarList3Vtbl.HrInit.invoke(functionHrInitPtr, taskbarList3Instance);
                if (hr != ShObjIdl_core_h.S_OK()) {
                    throw new RuntimeException("HrInit failed with error code: " + hr);
                }

                // 7. Set progress state to indeterminate
                MemorySegment functionSetProgressStatePtr = ITaskbarList3Vtbl.SetProgressState(taskbarList3Vtbl);
                hr = ITaskbarList3Vtbl.SetProgressState.invoke(functionSetProgressStatePtr, taskbarList3Instance, windowHandle, ShObjIdl_core_h.TBPF_INDETERMINATE());
                if (hr != ShObjIdl_core_h.S_OK()) {
                    throw new RuntimeException("SetProgressState failed with error code: " + hr);
                }

                // 8. Simulate some progress
                for (int i = 0; i < 100; i+=20) {
                    System.out.println("Progress is: " + i);
                    MemorySegment functionSetProgressValuePtr = ITaskbarList3Vtbl.SetProgressValue(taskbarList3Vtbl);
                    hr = ITaskbarList3Vtbl.SetProgressValue.invoke(functionSetProgressValuePtr, taskbarList3Instance, windowHandle, i, 100);
                    if (hr != ShObjIdl_core_h.S_OK()) {
                        throw new RuntimeException("SetProgressValue failed with error code: " + hr);
                    }
                    Thread.sleep(500);

                }

                // 9. Reset progress state
                hr = ITaskbarList3Vtbl.SetProgressState.invoke(functionSetProgressStatePtr, taskbarList3Instance, windowHandle, ShObjIdl_core_h.TBPF_INDETERMINATE());
                if (hr != ShObjIdl_core_h.S_OK()) {
                    throw new RuntimeException("SetProgressState failed with error code: " + hr);
                }

            } catch (Throwable ex) {
                ex.printStackTrace();

            } finally {
                ShObjIdl_core_h.CoUninitialize();
            }
        });
    }

    public static void main(String[] args) {
        launch(args);
    }
}


Solution

  • The code changes you have edited get you much closer, firstly by calling each callback with vtable+instance. The jextract generated code gives calls to lookup each Method in the vtable of each OLE interface IXXX. The MemorySegment must be reinterpreted to match the exact memory sizes or the bounds checking of MemorySegment will fail:

    // instance is CoCreateInstance return value
    MemorySegment instance = pointer.get(ValueLayout.ADDRESS, 0); 
    
    // order of Method invoke() is vTable, instance, ... params
    // IXXXVtbl.Method.invoke(IXXXVtbl.Method(IXXX.lpVtbl(instance)), instance, ...)
    // Interpret correct memory bounds:
    MemorySegment iUnknown = instance.reinterpret(IUnknown.sizeof());
    MemorySegment vtabXXX = IUnknown.lpVtbl(iUnknown ).reinterpret(IXXXVtbl.sizeof());
    IXXXVtbl.Method.invoke(vtabXXX, instance, ...)
    

    Remy Lebeau's comment eliminates the unnecessary CoCreateInstance.

    The last issue is that you have not assigned hWnd correctly, it can be resolved as an address using:

    // Wrong: allocates new address containing hWnd
    // MemorySegment windowHandle = arena.allocate(ValueLayout.ADDRESS, rawHandle);
    // hWnd is address:
    MemorySegment windowHandle = MemorySegment.ofAddress(rawHandle);
    

    I could not run your JavaFX example, but packaged the foreign memory calls to a new method and tested with a hWnd of a JFrame - deliberately not tidying your code here:

    static void updateTaskBar(long rawHandle) throws InterruptedException {
        try (var arena = Arena.ofConfined()) {
            // 1. Initialize variables
    
            // https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-clsidfromstring#remarks
            // The CLSID format is {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}.
            var clsidString = arena.allocateFrom(GUID_FORMAT.formatted(CLSID_CONST), StandardCharsets.UTF_16LE);
            var iidITaskbarList3 = arena.allocateFrom(GUID_FORMAT.formatted(IID_ITASKBAR_LIST_3), StandardCharsets.UTF_16LE);
            var clsid = CLSID.allocate(arena);
            var iidTaskbarList3 = IID.allocate(arena);
            var taskbar3PtrToPtr = arena.allocate(Win_h.C_POINTER);
            // FiXED:
            // MemorySegment windowHandle = arena.allocate(ValueLayout.ADDRESS, rawHandle);
            MemorySegment windowHandle = MemorySegment.ofAddress(rawHandle);
    
            // 2. Initialize COM
            int hr = Win_h.CoInitializeEx(MemorySegment.NULL, Win_h.COINIT_MULTITHREADED());
            if (hr != Win_h.S_OK()) {
                throw new RuntimeException("CoInitialize failed with error code: " + hr);
            }
    
            // 3. Create CLSID and IIDs
            hr = Win_h.CLSIDFromString(clsidString, clsid);
            if (hr != Win_h.S_OK()) {
                throw new RuntimeException("CLSIDFromString failed with error code: " + hr);
            }
    
            hr = Win_h.IIDFromString(iidITaskbarList3, iidTaskbarList3);
            if (hr != Win_h.S_OK()) {
                throw new RuntimeException("IIDFromString failed with error code: " + hr);
            }
    
            // 4. Create instance of ITaskbarList3
            hr = Win_h.CoCreateInstance(clsid, MemorySegment.NULL, Win_h.CLSCTX_ALL(), iidTaskbarList3, taskbar3PtrToPtr);
            if (hr != Win_h.S_OK()) {
                if (hr == Win_h.REGDB_E_CLASSNOTREG()) {
                    System.out.println("COM class is not registered!");
                }
                throw new RuntimeException("CoCreateInstance failed with error code: " + hr);
            }
            // 5. Obtain a pointer to the instance
            var taskbar3Ptr = taskbar3PtrToPtr.get(ValueLayout.ADDRESS, 0);
            // Use reinterpret method to have access to the actual ITaskbarList3 instance
            var taskbarList3Instance = taskbar3Ptr.reinterpret(ITaskbarList3.sizeof());
    
            // 6. Obtain lpVtbl pointer from ITaskbarList3
            MemorySegment taskbarList3VtblPtr = ITaskbarList3.lpVtbl(taskbarList3Instance);
            // Use reinterpret method to have access to the actual ITaskbarList3Vtbl instance
            MemorySegment taskbarList3Vtbl = taskbarList3VtblPtr.reinterpret(ITaskbarList3Vtbl.sizeof());
    
            // https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist-hrinit
            // Initializes the taskbar list object. This method must be called before any other ITaskbarList methods can be called.
            MemorySegment functionHrInitPtr = ITaskbarList3Vtbl.HrInit(taskbarList3Vtbl);
            hr = ITaskbarList3Vtbl.HrInit.invoke(functionHrInitPtr, taskbarList3Instance);
            if (hr != Win_h.S_OK()) {
                throw new RuntimeException("HrInit failed with error code: " + hr);
            }
    
            // 7. Set progress state to indeterminate
            MemorySegment functionSetProgressStatePtr = ITaskbarList3Vtbl.SetProgressState(taskbarList3Vtbl);
            hr = ITaskbarList3Vtbl.SetProgressState.invoke(functionSetProgressStatePtr, taskbarList3Instance, windowHandle, Win_h.TBPF_INDETERMINATE());
            if (hr != Win_h.S_OK()) {
                throw new RuntimeException("SetProgressState failed with error code: " + hr);
            }
    
            // 8. Simulate some progress
            int max = 100;
            for (int i = 0; i < max; i++) {
                System.out.println(windowHandle+ " SetProgressValue "+i);
                MemorySegment functionSetProgressValuePtr = ITaskbarList3Vtbl.SetProgressValue(taskbarList3Vtbl);
                hr = ITaskbarList3Vtbl.SetProgressValue.invoke(functionSetProgressValuePtr, taskbarList3Instance, windowHandle, i, max);
                if (hr != Win_h.S_OK()) {
                    throw new RuntimeException("SetProgressValue failed with error code: " + hr);
                }
                Thread.sleep(500);
            }
            // 9. Reset progress state
            hr = ITaskbarList3Vtbl.SetProgressState.invoke(functionSetProgressStatePtr, taskbarList3Instance, windowHandle, Win_h.TBPF_INDETERMINATE());
            if (hr != Win_h.S_OK()) {
                throw new RuntimeException("SetProgressState failed with error code: " + hr);
            }
        } finally {
            Win_h.CoUninitialize();
        }
    }