Win32 / OLE issues

Duncan Gittins duncan.gittins at gmail.com
Thu Oct 28 20:05:37 UTC 2021


Apologies for the long email. I've refactored / inlined my Windows OLE code
to a single class test case showing the bug in jextract.

All this code sample does is load a Window COM object by the string version
of its CLSID GUID, and retrieves a requested interface IID GUID (plus any
other IIDs specified. This ought to work for any COM / IID interface, but
it defaults to CLSID_ShellLink with IID_IShellLinkW / IID_Persist COM
interface lookup using only the IUnknown callbacks - QueryInterface /
AddRef / Release. Perhaps you could refactor into a test case for jextract
in future Windows builds.

The error is:

java.lang.AssertionError: should not reach here
        at
duncan.win.ole.IUnknownVtbl$Release.lambda$ofAddress$0(IUnknownVtbl.java:123)
        at duncan.panama.test.TestGuid$JUnknown.Release(Unknown Source)
        at duncan.panama.test.TestGuid.main(Unknown Source)
        at
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native
Method)
        at
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:76)
        at
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:51)
        at java.base/java.lang.reflect.Method.invoke(Method.java:569)
        at duncan.launch.Launch.main(Unknown Source)
        at duncan.launch.Launch.run(Unknown Source)
        at duncan.launch.Launch.main(Unknown Source)
Caused by: java.lang.invoke.WrongMethodTypeException: expected
(NativeSymbol,Addressable)int but found (NativeSymbol,MemoryAddress)int
        at
java.base/java.lang.invoke.Invokers.newWrongMethodTypeException(Invokers.java:523)
        at
java.base/java.lang.invoke.Invokers.checkExactType(Invokers.java:532)
        at
duncan.win.ole.IUnknownVtbl$Release.lambda$ofAddress$0(IUnknownVtbl.java:121)

Class is below

Kind regards

Duncan

import static duncan.win.ole.Ole32_h.S_OK;
import static jdk.incubator.foreign.ValueLayout.ADDRESS;
import static jdk.incubator.foreign.ValueLayout.JAVA_BYTE;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;

import duncan.win.ole.GUID;
import duncan.win.ole.IUnknown;
import duncan.win.ole.IUnknownVtbl;
import duncan.win.ole.Ole32_h;

import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.ResourceScope;
import jdk.incubator.foreign.SegmentAllocator;

/**
 * Test app to run through Windows COM IUnknown API calls.
 * A JUNIT test runs main() - easily removed
 *
 * Usage:
 java ...  TestGuid GUID_CLSID GUID_IID+
 *
 * GUIDs are defined in Windows format eg.
"{00021401-0000-0000-C000-000000000046}"
 *
 * Note: have kept to Windows naming conventions when calling Windows
definitions.
 *
 * This instantiates the class with GUID_CLSID, and returns the requested
IID interface.
 * <p>If additional IIDs are provided these are requested
 * <p>This requires jextract where files are:
 * Ole32.h:
 *      #include <objbase.h>
 *
 * jextract -source -lole32 -t duncan.win.ole -d duncan.win\src Ole32.h
 */
public class TestGuid
{
    // Junit testcase
    @EnabledOnOs(OS.WINDOWS)
    @Test void coverageTestGuid() throws Exception
    {
        TestGuid.main();
    }

    // This uses slimmed down version of my JUnknown, inlined here
    // along with all the dependent calls on other my other win32 classes
    // Not called IUnknown to avoid name classes with jextract generated
code.
    static class JUnknown
    {
        protected final MemoryAddress comObj;

        private final IUnknownVtbl.QueryInterface QueryInterface;
        private final IUnknownVtbl.AddRef         AddRef;
        private final IUnknownVtbl.Release        Release;

        /**
         * Derived classes can use this after chaining to the protected
constructor
         */
        protected final MemorySegment vtable;
        protected final SegmentAllocator allocator;

        /**
         * Derived classes must not use this constructor as it
         * only reserves only a small memory segment for the VTABLE
         */
        public JUnknown(ResourceScope scope, SegmentAllocator allocator,
MemoryAddress comObj)
        {
            this(scope, allocator, comObj,
IUnknownVtbl.ofAddress(vtable(scope, comObj), scope));
        }

        /**
         * This version ensures a larger vTABLE can be read:
         * A constructor for a derived class - say IShellLink
         * would need to extract its own callbacks from vtable like this:
         * <code>
            public class JShellLink extends JUnknown { ...

                public JShellLink(ResourceScope scope, SegmentAllocator
allocator, MemoryAddress comObj)
                {
                    super(scope, allocator, comObj,
IShellLinkWVtbl.ofAddress(vtable(scope, comObj), scope));
                    GetPath = IShellLinkWVtbl.GetPath(vtable, scope);
                    SetPath = IShellLinkWVtbl.SetPath(vtable, scope);
                    SetArguments = IShellLinkWVtbl.SetArguments(vtable,
scope);
                    GetArguments = IShellLinkWVtbl.GetArguments(vtable,
scope);
                    GetDescription = IShellLinkWVtbl.GetDescription(vtable,
scope);
                    GetIDList      = IShellLinkWVtbl.GetIDList(vtable,
scope);
                }
            ...
            }
         * </code>
         * @param scope
         * @param allocator
         * @param comObj
         * @param vtable as provided by a call to
Ole32_h.IxxxxxxVtbl.ofAddressRestricted(vtableA(comObj))
         */
        protected JUnknown(ResourceScope scope, SegmentAllocator allocator,
MemoryAddress comObj, MemorySegment vtable)
        {
            this.allocator = allocator;
            this.comObj = comObj;
            this.vtable = vtable;

            QueryInterface = IUnknownVtbl.QueryInterface(vtable, scope);
            AddRef = IUnknownVtbl.AddRef(vtable, scope);
            Release = IUnknownVtbl.Release(vtable, scope);
        }

        public String toString()
        {
            return
getClass().getSimpleName()+"["+toHex(comObj.toRawLongValue())+"]";
        }

        /**
         * Reads the vTABLE for this COM object, returning the address
         * which must be dereferenced by the outermost derived class with
the jextract
         * call specific for the derived classes VTABLE:
         *      IClassName.ofAddressRestricted(vtable(comObj));
         */
        public static MemoryAddress vtable(ResourceScope scope,
MemoryAddress comObj)
        {
            // Only need to retrieve a pointer at index 0 so size of one
pointer is OK:
            MemorySegment memseg = IUnknown.ofAddress(comObj, scope);

            // The address of QueryInterface is the first entry in the
vtable:
            MemoryAddress pVT = IUnknown.lpVtbl$get(memseg);

            // This is only the address of the vTable, see
IClassName.ofAddressRestricted(pVT);
            return pVT;
        }

        public int AddRef()
        {
            int refcount = AddRef.apply(comObj);
            System.out.println(this+".AddRef => "+refcount);

            return refcount;
        }

        /**
         * IUnknown::Release method (unknwn.h)
         *
https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-release
         * ULONG Release()
         */
        public int Release()
        {
            int refcount = Release.apply(comObj);
            System.out.println(this+".Release => "+refcount);

            return refcount;
        }

        /**
         *
https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(refiid_void)
         * IUnknown::QueryInterface(REFIID,void) method (unknwn.h)
         * HRESULT IUnknown::QueryInterface(REFIID iid, void** ppv);
         * <P>
         * https://osm.hpi.de/LV/Components04/VL5/MSDN/DrGUI-on-COM
         */
        public MemoryAddress QueryInterface(MemorySegment riid)
        {
            System.out.println(this+".QueryInterface
riid="+toHex(riid.toArray(JAVA_BYTE)));

            // This is allocated each time in case more than one call made
to QueryInterface
            MemorySegment queryRes = allocator.allocate(ADDRESS,
MemoryAddress.NULL);

            int hRes = QueryInterface.apply(comObj, riid.address(),
queryRes.address());

            MemoryAddress address = queryRes.get(ADDRESS, 0);
            check0("QueryInterface", hRes);
            System.out.println(" => IID="+toHex(address.toRawLongValue()));

            return address;
        }
    }

    public static MemorySegment toWideString(String s, SegmentAllocator
allocator)
    {
        return allocator.allocateArray(JAVA_BYTE,
(s+"\0").getBytes(StandardCharsets.UTF_16LE));
    }
    public static void check0(String func, int hRes)
    {
        checkThat(hRes == S_OK(), () -> "Error: "+func+" NOT OK, result was
"+hRes+" / 0x"+Integer.toHexString(hRes));
        System.out.println(func+" OK result => "+hRes);
    }

    public static void checkThat(boolean condition, Supplier<String> error)
    {
        if (!condition)
        {
            String message = error.get();
            System.err.println(message);
            throw new RuntimeException(message);
        }
    }

    public static MemorySegment CLSIDFromString(SegmentAllocator allocator,
String guid)
    {
        System.out.println("CLSIDFromString clsid="+guid);
        // Falls through to do the actual conversion:
        MemorySegment pGUID = GUID.allocate(allocator);

        MemorySegment wide = toWideString(guid, allocator);

        int hRes = Ole32_h.CLSIDFromString(wide, pGUID);
        check0("CLSIDFromString", hRes);
        System.out.println(" => GUID="+toHex(pGUID.toArray(JAVA_BYTE)));

        return pGUID;
    }
    public static String toHex(long value)
    {
        return "0x"+Long.toHexString(value).toUpperCase();
    }
    private static String toHex(byte[] arr) {
        HexFormat hex = HexFormat.ofDelimiter(",
").withPrefix("0x").withUpperCase();
        return "new byte["+arr.length+"] {"+hex.formatHex(arr)+"}";
    }
    public static MemoryAddress CoCreateInstance(SegmentAllocator
allocator, MemorySegment rclsid, int dwClsContext, MemorySegment riid)
    {
        MemoryAddress pUnkOuter = MemoryAddress.NULL;
        MemorySegment ptrComObj = allocator.allocate(ADDRESS,
MemoryAddress.NULL);
        // https://www.purebasic.fr/english/viewtopic.php?f=13&t=45583

        System.out.println("CoCreateInstance
rclsid="+toHex(rclsid.toArray(JAVA_BYTE)));
        System.out.println("CoCreateInstance
pUnkOuter="+pUnkOuter.address());
        System.out.println("CoCreateInstance dwClsContext="+dwClsContext);
        System.out.println("CoCreateInstance
riid="+toHex(riid.toArray(JAVA_BYTE)));

        int hRes = Ole32_h.CoCreateInstance(rclsid, pUnkOuter,
dwClsContext, riid, ptrComObj);
        check0("CoCreateInstance", hRes);
        MemoryAddress comObj = ptrComObj.get(ADDRESS, 0);
        System.out.println("CoCreateInstance =>
address="+toHex(comObj.toRawLongValue()));

        return comObj;
    }
    public static void checkResult(String func, int hRes, int ... okcodes)
    {
        for (int ok : okcodes)
        {
            if (ok == hRes)
            {
                System.out.println(func+" OK result => "+hRes+"
ok="+Arrays.toString(okcodes));
                return;
            }
        }
        checkThat(false, () -> "Error: "+func+" NOT OK, result code
"+hRes+" / 0x"+Integer.toHexString(hRes)+" ok="+Arrays.toString(okcodes));
    }
    public static AutoCloseable CoInitialize()
    {
        int hRes = Ole32_h.CoInitialize(MemoryAddress.NULL);
        checkResult("CoInitialize", hRes, Ole32_h.S_OK(),
Ole32_h.S_FALSE());

        return Ole32_h::CoUninitialize;
    }

    public static void main(String ... args) throws Exception
    {
        final long t0 = System.nanoTime();

        // Default run instantiates COM object CLSID_ShellLink and asks for
its IShellLinkW and IID_Persist interfaces
        String[] defaults = {
                /*GUID_CLSID_ShellLink_str*/
"{00021401-0000-0000-C000-000000000046}",
                /*GUID_IID_IShellLinkW_str*/
"{000214F9-0000-0000-C000-000000000046}",
                /*GUID_IID_Persist_str*/
 "{0000010B-0000-0000-C000-000000000046}",
        };

        if (args.length < 2) args = defaults;

        String clsidStr = args[0];
        String iidStr   = args[1];
        System.out.println("lookup GUID CLSID "+clsidStr+" IID "+iidStr);

        try(ResourceScope scope = ResourceScope.newConfinedScope())
        {
            SegmentAllocator allocator =
SegmentAllocator.newNativeArena(scope);

            // Lookup GUIDs for class and interfaces
            MemorySegment clsid  = CLSIDFromString(allocator, clsidStr);
            MemorySegment iid    = CLSIDFromString(allocator, iidStr);

            // Setup COM:
            try(var autoC = CoInitialize())
            {
                // Get a pointer to the COM instance.
                MemoryAddress comObj = CoCreateInstance(allocator, clsid,
Ole32_h.CLSCTX_INPROC_SERVER(), iid);

                // JUnknown ignores the complete vtable, only initialises
callbacks for IUnknown interface
                JUnknown iUnknown = new JUnknown(scope, allocator, comObj);
                try
                {
                    // test add/release on this iUnknown
                    int ar = iUnknown.AddRef();
                    int re = iUnknown.Release();
                    checkResult("AddRef/Release()", ar, re+1);

                    // Access other interfaces via the first one:
                    for (int ii = 2; ii < args.length; ii++)
                    {
                        // Use Query interface on another interface from
the CLSID
                        MemorySegment iidextra = CLSIDFromString(allocator,
args[ii]);

                        // Query CLS for this other interface definition
                        MemoryAddress iOtherUnknown =
iUnknown.QueryInterface(iidextra);
                        JUnknown iOther = new JUnknown(scope, allocator,
iOtherUnknown);
                        try
                        {
                            // test add/release on this other iUnknown (NB
same instance but with different vtable)
                            int add = iOther.AddRef();
                            checkResult("AddRef()", add, 3);
                            int rel = iOther.Release();
                            checkResult("Release()", rel, 2);
                        }
                        finally
                        {
                            // Signal end of use of iOther
                            // Note that the ref count includes the value
on iUnknown
                            int refC = iOther.Release();
                            checkResult("Release()", refC, 1);
                        }
                    }
                }
                finally
                {
                    // Signal end of use of iUnknown
                    int refC = iUnknown.Release();
                    // Note that the ref count should now be zero
                    checkResult("Release()", refC, 0);
                }
            }
            final long now = System.nanoTime();
            System.out.println("Ended
ms="+TimeUnit.NANOSECONDS.toMillis(now - t0)+" "+clsidStr);
        }
    }
}

On Mon, 27 Sept 2021 at 16:45, Duncan Gittins <duncan.gittins at gmail.com>
wrote:

> The jextract parameters which generate the broken Windows OLE API code I
> outlined below is:
>
>     jextract -source -lole32 -t duncan.win.ole -d source\duncan.win\java
> headers\Ole32.h
>
> where headers\Ole32.h contains:
>
>     #include <objbase.h>
>
> A temporary workaround for this jextract issue is to edit
> FunctionalInterfaceBuilder.java / emitFunctionalFactoryForPointer() to
> insert the (Addressable) casts for MemoryAddress parameters:
>
> ---
> a/src/jdk.incubator.jextract/share/classes/jdk/internal/jextract/impl/FunctionalInterfaceBuilder.java
> +++
> b/src/jdk.incubator.jextract/share/classes/jdk/internal/jextract/impl/FunctionalInterfaceBuilder.java
> @@ -123,7 +123,7 @@ public class FunctionalInterfaceBuilder extends
> ClassSourceBuilder {
>              append(mhConstant.accessExpression() +
> ".invokeExact((Addressable)addr");
>              if (fiType.parameterCount() > 0) {
>                  String params = IntStream.range(0,
> fiType.parameterCount())
> -                        .mapToObj(i -> "x" + i)
> +                        .mapToObj(i ->
> (fiType.parameterType(i).getName().endsWith(".MemoryAddress") ?
> "(Addressable)":"")+"x" + i)
>                          .collect(Collectors.joining(", "));
>                  append(", " + params);
>              }
>
> this fixes the ofAddress callbacks, for example see IPersistFileVtbl.java:
>
>    public interface Load {
>         ....
>         static Load ofAddress(MemoryAddress addr) {
>             return (jdk.incubator.foreign.MemoryAddress x0,
> jdk.incubator.foreign.MemoryAddress x1, int x2) -> {
>                 try {
> // WAS:          return
> (int)IPersistFileVtbl.Load$MH.invokeExact((Addressable)addr, x0, x1, x2);
>                       return
> (int)IPersistFileVtbl.Load$MH.invokeExact((Addressable)addr,
> (Addressable)x0, (Addressable)x1, x2);
>                 } catch (Throwable ex$) {
>                     throw new AssertionError("should not reach here", ex$);
>                 }
>             };
>         }
>
> Kind regards
>
> Duncan
>
>
> On Fri, 24 Sept 2021 at 16:36, Duncan Gittins <duncan.gittins at gmail.com>
> wrote:
>
>> I've pulled latest panama-foreign which has the changes outlined in
>>
>>     https://inside.java/2021/09/16/finalizing-the-foreign-apis/
>>
>> I've a few problems to resolve to match up, one is related to jextract
>> which generates interfaces for Windows OLE APIs.  The invokeExact params
>> are missing some (Addressable) casts (I think?) eg this is IUnknown Release:
>>
>>     public interface Release {
>>
>>         int apply(jdk.incubator.foreign.MemoryAddress x0);
>>         static CLinker.UpcallStub allocate(Release fi) {
>>             return RuntimeHelper.upcallStub(Release.class, fi,
>> IUnknownVtbl.Release$FUNC, "(Ljdk/incubator/foreign/MemoryAddress;)I");
>>         }
>>         static CLinker.UpcallStub allocate(Release fi, ResourceScope
>> scope) {
>>             return RuntimeHelper.upcallStub(Release.class, fi,
>> IUnknownVtbl.Release$FUNC, "(Ljdk/incubator/foreign/MemoryAddress;)I",
>> scope);
>>         }
>>         static Release ofAddress(MemoryAddress addr) {
>>             return (jdk.incubator.foreign.MemoryAddress x0) -> {
>>                 try {
>>                     return
>> (int)IUnknownVtbl.Release$MH.invokeExact((Addressable)addr, x0);
>>                 } catch (Throwable ex$) {
>>                     throw new AssertionError("should not reach here",
>> ex$);
>>                 }
>>             };
>>         }
>>     }
>>
>> Stack traces show
>>
>>      Caused by: java.lang.invoke.WrongMethodTypeException: expected
>> (Addressable,Addressable)int but found (Addressable,MemoryAddress)int
>>
>> Kind regards
>>
>> Duncan
>>
>>
>>


More information about the panama-dev mailing list