JNA with JDK 22 java.lang.foreign

35 views
Skip to first unread message

Harri Pesonen

unread,
Apr 10, 2024, 7:31:07 AMApr 10
to Java Native Access
Hello, I have been using JNA for years, but only one function to read Windows registry.
Now that JDK 22 is released, I though of implementing that function with java.lang.foreign.
This is it:

Advapi32Util.java

    public static TreeMap<String, Object> registryGetValues(HKEY root,
                                                            String keyPath, int samDesiredExtra) {
        try (Arena arena = Arena.ofConfined()) {
            SymbolLookup advapi32 = SymbolLookup.libraryLookup("Advapi32", Arena.global());
            Linker linker = Linker.nativeLinker();
            MemorySegment hKeyRef = arena.allocate(JAVA_LONG);
            Object o;
            try {
                o = linker.downcallHandle(
                                advapi32.find("RegOpenKeyExW").orElseThrow(),
                                FunctionDescriptor.of(
                                        JAVA_INT, // return 0 = ERROR_SUCCESS
                                        JAVA_LONG, // HKey
                                        ADDRESS, // sub key
                                        JAVA_INT, // options
                                        JAVA_INT, // samDesired
                                        ADDRESS // address of returned handle
                                ))
                        .invoke(
                                Pointer.nativeValue(root.getPointer()),
                                arena.allocateFrom(keyPath, StandardCharsets.UTF_16LE),
                                0, // options
                                WinNT.KEY_READ | samDesiredExtra,
                                hKeyRef
                        );
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
            int rc = (int)(Integer)o;
            if (rc != W32Errors.ERROR_SUCCESS) {
                throw new RuntimeException("RegOpenKeyExA " + rc);
//                        throw new Win32Exception(rc);
            }
            long hKey = hKeyRef.get(JAVA_LONG, 0);
            try {
                return registryGetValues(advapi32, arena, hKey);
            } finally {
                try {
                    o = linker.downcallHandle(
                                    advapi32.find("RegCloseKey").orElseThrow(),
                                    FunctionDescriptor.of(
                                            JAVA_INT, // return 0 = ERROR_SUCCESS
                                            JAVA_LONG // HKey
                                    ))
                            .invoke(hKey);
                    rc = (int)(Integer)o;
                    if (rc != W32Errors.ERROR_SUCCESS) {
                        log.log(System.Logger.Level.ERROR, "RegCloseKey", rc);
//                            throw new Win32Exception(rc);
                    }
                } catch (Throwable e) {
                    log.log(System.Logger.Level.ERROR, "RegCloseKey", e);
                }
            }
        }
    }

    public static TreeMap<String, Object> registryGetValues(SymbolLookup advapi32, Arena arena,
                                                            long hKey) {
        TreeMap<String, Object> keyValues = new TreeMap<>();
        Linker linker = Linker.nativeLinker();
        MemorySegment lpcValues = arena.allocate(JAVA_INT);
        MemorySegment lpcMaxValueNameLen = arena.allocate(JAVA_INT);
        MemorySegment lpcMaxValueLen = arena.allocate(JAVA_INT);
        Object o;
        try {
            o = linker.downcallHandle(
                            advapi32.find("RegQueryInfoKeyA").orElseThrow(),
                            FunctionDescriptor.of(
                                    JAVA_INT, // return 0 = ERROR_SUCCESS
                                    JAVA_LONG, // HKey
                                    ADDRESS, // lpClass
                                    ADDRESS, // lpClass
                                    ADDRESS, // lpReserved
                                    ADDRESS, // lpcSubkeys
                                    ADDRESS, // lpcMaxSubKeyLen
                                    ADDRESS, // lpcMaxClassLen
                                    ADDRESS, // lpcValues
                                    ADDRESS, // lpcMaxValueNameLen
                                    ADDRESS, // lpcMaxValueLen
                                    ADDRESS, // lpcbSecurityDescriptor
                                    ADDRESS // lpftLastWriteTime
                            ))
                    .invoke(
                            hKey,
                            MemorySegment.NULL, // lpClass
                            MemorySegment.NULL, // lpClass
                            MemorySegment.NULL, // lpReserved
                            MemorySegment.NULL, // lpcSubkeys
                            MemorySegment.NULL, // lpcMaxSubKeyLen
                            MemorySegment.NULL, // lpcMaxClassLen
                            lpcValues,
                            lpcMaxValueNameLen,
                            lpcMaxValueLen,
                            MemorySegment.NULL, // lpcbSecurityDescriptor
                            MemorySegment.NULL // lpftLastWriteTime
                    );
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        int rc = (int) (Integer) o;
        if (rc != W32Errors.ERROR_SUCCESS) {
            throw new RuntimeException("RegQueryInfoKeyW " + rc);
//                        throw new Win32Exception(rc);
        }
        int maxValueNameLen = lpcMaxValueNameLen.get(JAVA_INT, 0);
        MemorySegment name = arena.allocate(JAVA_CHAR, maxValueNameLen + 1);
        // Allocate enough memory to hold largest value and two
        // terminating WCHARs -- the memory is zeroed so after
        // value request we should not overread when reading strings
        int maxValueLen = lpcMaxValueLen.get(JAVA_INT, 0);
        MemorySegment byteData = arena.allocate(JAVA_CHAR, maxValueLen + 2);
        int valueCount = lpcValues.get(JAVA_INT, 0);
        MemorySegment lpcchValueName = arena.allocate(JAVA_INT);
        MemorySegment lpcbData = arena.allocate(JAVA_INT);
        MemorySegment lpType = arena.allocate(JAVA_INT);
        for (int i = 0; i < valueCount; i++) {
            byteData.asByteBuffer().clear(); // TODO: Arrays.fill(b.array(), (byte)0) ???
            lpcchValueName.set(JAVA_INT, 0, maxValueLen + 1);
            lpcbData.set(JAVA_INT, 0, maxValueLen);
            try {
                o = linker.downcallHandle(
                                advapi32.find("RegEnumValueW").orElseThrow(),
                                FunctionDescriptor.of(
                                        JAVA_INT, // return 0 = ERROR_SUCCESS
                                        JAVA_LONG, // HKey
                                        JAVA_INT, // dwIndex
                                        ADDRESS, // lpValueName
                                        ADDRESS, // lpcchValueName
                                        ADDRESS, // reserved
                                        ADDRESS, // lpType
                                        ADDRESS, // lpData
                                        ADDRESS // lpcbData
                                ))
                        .invoke(hKey, i, name, lpcchValueName, MemorySegment.NULL, lpType, byteData, lpcbData);

            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
            rc = (int) (Integer) o;
            if (rc != W32Errors.ERROR_SUCCESS) {
                throw new RuntimeException("RegEnumValueW " + rc);
//                        throw new Win32Exception(rc);
            }
            String nameString = name.getString(0, StandardCharsets.UTF_16LE);
            int dataSize = lpcbData.get(JAVA_INT, 0);
            int type = lpType.get(JAVA_INT, 0);
            if (dataSize == 0) {
                switch (type) {
                    case WinNT.REG_BINARY: {
                        keyValues.put(nameString, new byte[0]);
                        break;
                    }
                    case WinNT.REG_SZ:
                    case WinNT.REG_EXPAND_SZ: {
                        keyValues.put(nameString, new char[0]);
                        break;
                    }
                    case WinNT.REG_MULTI_SZ: {
                        keyValues.put(nameString, new String[0]);
                        break;
                    }
                    case WinNT.REG_NONE: {
                        keyValues.put(nameString, null);
                        break;
                    }
                    default:
                        throw new RuntimeException("Unsupported empty type: " + type);
                }
                continue;
            }
            switch (type) {
                case WinNT.REG_QWORD: {
                    keyValues.put(nameString, byteData.get(JAVA_LONG, 0));
                    break;
                }
                case WinNT.REG_DWORD: {
                    keyValues.put(nameString, byteData.get(JAVA_INT, 0));
                    break;
                }
                case WinNT.REG_SZ:
                case WinNT.REG_EXPAND_SZ: {
                    keyValues.put(nameString, byteData.getString(0, StandardCharsets.UTF_16LE));
                    break;
                }
                case WinNT.REG_BINARY: {
                    byte[] buf = new byte[dataSize];
                    byteData.asByteBuffer().limit(dataSize).get(buf);
                    keyValues.put(nameString, buf);
                    break;
                }
                case WinNT.REG_MULTI_SZ: {
                    ArrayList<String> result = new ArrayList<>();
                    int offset = 0;
                    while (offset < dataSize) {
                        String s = byteData.getString(offset, StandardCharsets.UTF_16LE);
                        offset += s.length() * 2; // TODO: ???
                        offset += 2;
                        if (s.isEmpty()) {
                            // A sequence of null-terminated strings,
                            // terminated by an empty string (\0).
                            // => The first empty string terminates the
                            break;
                        } else {
                            result.add(s);
                        }
                    }
                    keyValues.put(nameString, result.toArray(new String[0]));
                    break;
                }
                default:
                    throw new RuntimeException("Unsupported type: " + type);
            }
        }
        return keyValues;
    }

It works fine, but unfortunately I can't use it yet because we are still targeting Java 11.
Posting this if this is useful for someone.
Converting JNA to java.lang.foreign is quite straightforward, but quite a lot of work.
This took me one day.
Wondering if we get JNA for JDK 22 later?

-Harri

Harri Pesonen

unread,
Apr 10, 2024, 11:14:24 AMApr 10
to Java Native Access
I implemented one more function to get the error message from Windows. Here I use fixed buffer instead of WIndows allocated buffer, because I didn't find a secure way access that returned buffer.
Here you can see my java.lang.foreign code before the original JNA code.

    public static String formatMessage(int code, int primaryLangId, int sublangId) {
        if (!Native.jni) {
            SymbolLookup kernel32 = SymbolLookup.libraryLookup("kernel32", Arena.global());
            Linker linker = Linker.nativeLinker();

            try (Arena arena = Arena.ofConfined()) {
                int bufferSize = 128;
                MemorySegment buffer = arena.allocate(JAVA_CHAR, bufferSize);

                Object o;
                try {
                    o = linker.downcallHandle(
                                    kernel32.find("FormatMessageW").orElseThrow(),
                                    FunctionDescriptor.of(
                                            JAVA_INT, // return nLen
                                            JAVA_INT, // int dwFlags
                                            ADDRESS, // Pointer lpSource
                                            JAVA_INT, // int dwMessageId
                                            JAVA_INT, // int dwLanguageId
                                            ADDRESS, // PointerByReference lpBuffer
                                            JAVA_INT, // int nSize
                                            ADDRESS// Pointer va_list
                                    ))
                            .invoke(
//                                    WinBase.FORMAT_MESSAGE_ALLOCATE_BUFFER |
                                            WinBase.FORMAT_MESSAGE_FROM_SYSTEM
                                            | WinBase.FORMAT_MESSAGE_IGNORE_INSERTS,
                                    MemorySegment.NULL,
                                    code,
                                    WinNT.LocaleMacros.MAKELANGID(primaryLangId, sublangId),
                                    buffer, bufferSize, MemorySegment.NULL

                            );
                } catch (Throwable e) {
                    throw new RuntimeException(e);
                }
                int len = (int)(Integer)o;
                if (len == 0) {
                    throw new LastErrorException("Error " + code);
                }
                return buffer.getString(0, StandardCharsets.UTF_16LE);
            }
        } // end of java.lang.foreign, start of original JNA code
        PointerByReference buffer = new PointerByReference();
        int nLen = Kernel32.INSTANCE.FormatMessage(
                WinBase.FORMAT_MESSAGE_ALLOCATE_BUFFER
                        | WinBase.FORMAT_MESSAGE_FROM_SYSTEM
                        | WinBase.FORMAT_MESSAGE_IGNORE_INSERTS,
                null,
                code,
                WinNT.LocaleMacros.MAKELANGID(primaryLangId, sublangId),
                buffer, 0, null);
        if (nLen == 0) {
            throw new LastErrorException(Native.getLastError());
        }

        Pointer ptr = buffer.getValue();
        try {
            String s = ptr.getWideString(0);
            return s.trim();
        } finally {
            freeLocalMemory(ptr);
        }
    }

Markus Karg

unread,
Apr 18, 2024, 4:36:44 AMApr 18
to Java Native Access
Maybe you like to give jextract a try? It is still a beta, but it should already be able to generate all that code for you, and possibly it spares you some potential bugs. :-)

Harri Pesonen

unread,
Apr 23, 2024, 6:47:08 AMApr 23
to Java Native Access
Yes, certainly I will try jextract at some point.

But now I have been playing with JNA, changing it to use FFM instead of JNI.
I have a fork here:


This is a hobby project, hopefully useful for someone.

Status of JNA unit tests:
* Tests failed: 206, passed: 431, ignored: 24 of 661 tests

Status of Win32 platform unit tests:
* Tests failed: 143, passed: 502, ignored: 13 of 658 tests (COM tests skipped)
Most standard JNA features work.
Does not work:
* By-value structures larger than 8 bytes (8 bytes fit into Java long)
* Java objects (native libraries normally do not use or return Java objects)
* Varargs
* ...

Markus Karg

unread,
Apr 23, 2024, 7:22:49 AMApr 23
to Java Native Access
Cool! Maybe some day JNA will have with customizable backends? :-)

Harri Pesonen

unread,
Apr 23, 2024, 12:52:05 PMApr 23
to jna-...@googlegroups.com
Yes, certainly I will try jextract at some point.

But now I have been playing with JNA, changing it to use FFM instead of JNI.
I have a fork here:


This is a hobby project, hopefully useful for someone.

Status of JNA unit tests:
* Tests failed: 206, passed: 431, ignored: 24 of 661 tests

Status of Win32 platform unit tests:
* Tests failed: 143, passed: 502, ignored: 13 of 658 tests (COM tests skipped)
Most standard JNA features work.
Does not work:
* By-value structures larger than 8 bytes (8 bytes fit into Java long)
* Java objects (native libraries normally do not use or return Java objects)
* Varargs
* ...

--
You received this message because you are subscribed to a topic in the Google Groups "Java Native Access" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/jna-users/xlzcKGIycnE/unsubscribe.
To unsubscribe from this group and all its topics, send an email to jna-users+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/jna-users/62a1acb9-947b-4469-9043-71abcf3aa89dn%40googlegroups.com.
Reply all
Reply to author
Forward
0 new messages