Usando bibliotecas nativas do Windows em Java com a GraalVM Native Image

Olá!
Eu estou usando o novo compilador AOT da GraalVM, que gera imagens nativas, e quero usar a API do Windows na minha aplicação Java, mais especificamente a biblioteca User32.dll.

Para integrar o código Java com a biblioteca nativa, eu estou me baseando na documentação javadoc da Graal: https://www.graalvm.org/sdk/javadoc/index.html

E também neste tutorial oficial no Github: https://github.com/oracle/graal/blob/master/substratevm/src/com.oracle.svm.tutorial/src/com/oracle/svm/tutorial/CInterfaceTutorial.java

Estou tentando criar uma função que lista os teclados USB conectados, o código produzido em Java compila para nativo e executa sem erros, no entanto, o resultado da função GetRawInputDeviceInfo é sempre -1 , de acordo com a documentação da Microsoft isso significa que o parâmetro pData não tem espaço suficiente para os dados, mas estou passando um ponteiro nulo, e de acordo com a documentação, quando pData é nulo, o valor de retorno é zero, tenho dois códigos fazendo exatamente a mesma coisa, mas aquele escrito em C funciona e o escrito em Java não.

OBS: No código C existe uma diretiva #define, definindo a constante RIDI_DEVICENAME como 0x20000007, equivalente a 536870919, como não sei como “importar” as diretivas de pré-processamento no código Java, estou colocando o valor literal temporariamente.

Código em C:

int main() {

    UINT nDevices;
	if (GetRawInputDeviceList(NULL, &nDevices, sizeof(RAWINPUTDEVICELIST)) != 0) {
		return 1;
	}

	PRAWINPUTDEVICELIST pRawInputDeviceList;
	if ((pRawInputDeviceList = malloc(sizeof(RAWINPUTDEVICELIST) * nDevices)) == NULL) {
		return 1;
	}

	if (GetRawInputDeviceList(pRawInputDeviceList, &nDevices, sizeof(RAWINPUTDEVICELIST)) == UINT_MAX) {
		free(pRawInputDeviceList);
		return 1;
	}

	for (size_t nDevice = 0; nDevice < nDevices; nDevice++) {

		if (pRawInputDeviceList[nDevice].dwType != RIM_TYPEKEYBOARD)
			continue;

		char* deviceName = NULL; 
		UINT pcbSize;
		if (GetRawInputDeviceInfo(pRawInputDeviceList[nDevice].hDevice, RIDI_DEVICENAME, NULL, &pcbSize) != UINT_MAX && pcbSize != 0) {
			
            /*
            char pdata[pcbSize];
			if (GetRawInputDeviceInfo(pRawInputDeviceList[nDevice].hDevice, RIDI_DEVICENAME, pData, &pcbSize) != UINT_MAX) {
				deviceName = pData;
				printf("%s", deviceName);
			}*/

		}
	}

	return 0;
}

Código em Java:

public class Main {
​
    public static void main(String[] args) {
		
        var deviceAmountPointer = StackValue.get(CIntPointer.class);
​
        if (getRawInputDeviceList(WordFactory.nullPointer(), deviceAmountPointer,
                SizeOf.get(RawInputDeviceList.class)) != 0)
            return;
​
        var deviceAmount = deviceAmountPointer.read();
        var deviceListPointer = UnmanagedMemory.<RawInputDeviceList>malloc(deviceAmount * SizeOf.get(RawInputDeviceList.class));
​
        if (deviceListPointer.isNull())
            return;
​
        if (getRawInputDeviceList(deviceListPointer, deviceAmountPointer, SizeOf.get(RawInputDeviceList.class)) == -1) {
            UnmanagedMemory.free(deviceListPointer);
            return;
        }
​
        for (int i = 0; i <= deviceAmount; ++i) {
​
            RawInputDeviceList device = deviceListPointer.addressOf(i);
​
            if (device.deviceType() != 1)
                continue;
			
            var pcbSize = StackValue.get(CIntPointer.class);
​
            if (getRawInputDeviceInfo(device.deviceHandle(), 536870919, WordFactory.nullPointer(), pcbSize) != -1 && pcbSize.read() != 0) {
                var pData = UnmanagedMemory.<CCharPointer>malloc((pcbSize.read() + 1) * SizeOf.get(CCharPointer.class));
​
                if (getRawInputDeviceInfo(device.deviceHandle(), 536870919, (VoidPointer) pData, pcbSize) != -1) {
                    System.out.println(CTypeConversion.toJavaString(pData));
                }
            }
        }
    }
}
​
@CContext(WindowsDirectives.class)
class Windows {
​
    @CTypedefOfInfo("HANDLE")
    @CPointerTo(nameOfCType = "void")
    public interface Handle extends PointerBase { }
​
    @CFunction("GetRawInputDeviceList")
    public static native int getRawInputDeviceList(RawInputDeviceList inputDeviceList,
                                                    CIntPointer pointerDevicesAmount, int size);
​
    @CFunction("GetRawInputDeviceInfoW")
    public static native int getRawInputDeviceInfo(Handle deviceHandle, int uiCommand,
                                                    VoidPointer pData, CIntPointer pcbSize);
​
    @CStruct("tagRAWINPUTDEVICELIST")
    public interface RawInputDeviceList extends PointerBase {
​
        @CFieldAddress("hDevice")
        Handle deviceHandle();
​
        @CField("dwType")
        int deviceType();
​
        RawInputDeviceList addressOf(int index);
    }
}
​
class WindowsDirectives implements CContext.Directives {
​
    @Override
    public List<String> getHeaderFiles() {
        return Collections.singletonList("<windows.h>");
    }
​
    @Override
    public List<String> getLibraries() {
        return Collections.singletonList("User32");
    }
}

Documentação da Microsoft: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getrawinputdeviceinfoa

Alguém sabe o que estou fazendo de errado para a função GetRawInputDeviceInfo retornar -1 ao invés de zero?

Não acha mais fácil usar JNA pra “conversar” com as DLLs do Windows?

É que eu quero gerar um executável nativo, sem depender da JVM, e embora que, com uma certa quantidade de configuração, pareça que é possível usar o JNA ou JNI na imagem nativa, a Graal já oferece uma integração natural e elegante, sem sobrecarga de JNA/JNI, o que é mais eficiente na performance final da imagem nativa

1 curtida

Eu consegui resolver o problema!
Eu importei a função getLastError da biblioteca Kernel32.dll, e quando chamada após o método GetRawInputDeviceInfo, ela retornava 6, que de acordo com a documentação da microsoft é o código do erro ERROR_INVALID_HANDLE, eu não entendi o porque do handle do dispositvo ser inválido, já que eu passava ele para o método da mesma maneira que ele veio da struct do RawInputDeviceList, quando eu converti o tipo dele de um ponteiro void, para CIntPointer, usando o método read() ele retornava o que parece ser o número do dispositivo.

Eu decidi passar esse número ao invés de um ponteiro para a função GetRawInputDeviceInfo, alterando o tipo do parâmetro de handle do dispositivo para int, dessa forma:

@CFunction("GetRawInputDeviceInfoW")
public static native int getRawInputDeviceInfo(int deviceHandle, UnsignedWord uiCommand, PointerBase pData, CIntPointer pcbSize);

E por algumo motivo bizarro, isso funciona, mesmo que no código C, o Handle seja definido como um ponteiro e não como um int:

typedef void *HANDLE;

Além disso, na observação da pergunta eu disse que não sabia como importar o RIDI_DEVICENAME para o código Java, mas descobri como fazer isso usando a anotação @CConstant:

@CConstant("RIDI_DEVICENAME")
public static native int DEVICENAME();

O código final ficou assim:
Main.java:

package com.nealsmith.NativeTest;

import org.graalvm.nativeimage.StackValue;
import org.graalvm.nativeimage.UnmanagedMemory;
import org.graalvm.nativeimage.c.struct.SizeOf;
import org.graalvm.nativeimage.c.type.*;
import org.graalvm.word.WordFactory;

import static com.nealsmith.NativeTest.Native.Windows.*;
import static org.graalvm.nativeimage.UnmanagedMemory.malloc;

public class Main {

    public static void main(String[] args) {

        var deviceAmountPointer = StackValue.get(CIntPointer.class);

        if (getRawInputDeviceList(WordFactory.nullPointer(), deviceAmountPointer,
                SizeOf.get(RawInputDeviceList.class)) != 0) {
            return;
        }

        var deviceAmount = deviceAmountPointer.read();
        var deviceListPointer = UnmanagedMemory.<RawInputDeviceList>malloc(deviceAmount * SizeOf.get(RawInputDeviceList.class));

        if (deviceListPointer.isNull()) {
            return;
        }

        if (getRawInputDeviceList(deviceListPointer, deviceAmountPointer, SizeOf.get(RawInputDeviceList.class)) == -1) {
            UnmanagedMemory.free(deviceListPointer);
            return;
        }

        for (int i = 0; i <= deviceAmount; ++i) {

            RawInputDeviceList device = deviceListPointer.addressOf(i);

            if (device.deviceType() != 1) {
                continue;
            }

            var pcbSize = StackValue.get(CIntPointer.class);

            if (getRawInputDeviceInfo(device.deviceHandle().read(), RIDI_DEVICENAME(), WordFactory.nullPointer(), pcbSize) != -1 && pcbSize.read() != 0) {
                var pData = (CCharPointer) malloc((pcbSize.read() + 1) * SizeOf.get(CCharPointer.class));

                if (getRawInputDeviceInfo(device.deviceHandle().read(), RIDI_DEVICENAME(), pData, pcbSize) != -1) {
                    System.out.println("Device Name: " + CTypeConversion.toJavaString(pData));
                }
            }
        }
    }
}

Windows.java:

package com.nealsmtih.NativeTest.Native;

import org.graalvm.nativeimage.c.CContext;

import org.graalvm.nativeimage.c.type.CIntPointer;
import org.graalvm.nativeimage.c.struct.*;
import org.graalvm.nativeimage.c.constant.CConstant;
import org.graalvm.nativeimage.c.function.CFunction;

import org.graalvm.word.PointerBase;

@CContext(WindowsDirectives.class)
public class Windows {

    @CConstant("RIDI_DEVICENAME")
    public static native int RIDI_DEVICENAME();

    @CFunction("GetAsyncKeyState")
    public static native short getAsyncKeyState(int key);

    @CFunction("GetRawInputDeviceList")
    public static native int getRawInputDeviceList(RawInputDeviceList inputDeviceList,
                                                   CIntPointer pointerDevicesAmount, int size);

    @CFunction("GetRawInputDeviceInfoW")
    public static native int getRawInputDeviceInfo(int deviceHandle, int uiCommand, PointerBase pData, CIntPointer pcbSize);

    @CStruct("tagRAWINPUTDEVICELIST")
    public interface RawInputDeviceList extends PointerBase {

        @CFieldAddress("hDevice")
        CIntPointer deviceHandle();

        @CField("dwType")
        int deviceType();

        RawInputDeviceList addressOf(int index);
    }

    @CFunction("GetLastError")
    public static native long getLastError();
}

WindowsDirectives.java:

package com.nealsmtih.NativeTest.Native;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.graalvm.nativeimage.c.CContext;

public class WindowsDirectives implements CContext.Directives {

    @Override
    public List<String> getHeaderFiles() {
        return Collections.singletonList("<windows.h>");
    }

    @Override
    public List<String> getLibraries() {
        return Arrays.asList("User32", "Kernel32");
    }
}

Apenas um detalhe, por algum motivo, o método toJavaString da classe CTypeConversion não está convertendo a char array do C corretamente para uma String Java nesse caso, todos os nomes dos dispositivos começam com “\\?\”, por algum motivo, o retorno é uma string com apenas uma barra “\”, então como gambiarra temporária, estou fazendo um for para iterar por cada caractere no ponteiro da char array:

System.out.print("Device Name: ");

for (int j = 0; j < (pcbSize.read() + 1) * SizeOf.get(CCharPointer.class); ++j) {
    if (!Character.toString((char) pData.read(j)).isBlank())
        System.out.print((char) pData.read(j));
}
System.out.println();

Eu tive muita dificuldade em achar conteúdo na internet sobre essa ferramenta, pois ela é bem nova ainda, então se alguém também quiser usar a ferramenta de imagem nativa da Graal, esses dois tutoriais me ajudaram muito:

Eles ensinam como integrar código nativo com o código Java que será compilado para uma imagem nativa.

Além deles, também tem um exemplo oficial no Github das possíveis integrações de código C: https://github.com/oracle/graal/blob/master/substratevm/src/com.oracle.svm.tutorial/src/com/oracle/svm/tutorial/CInterfaceTutorial.java

E caso o que você queria fazer algo que não esteja incluso em nenhum destes tutorais, você pode consultar o Javadoc da Graal:
https://www.graalvm.org/sdk/javadoc/index.html

E se você quiser falar com alguém para tirar suas dúvidas, existe o Slack do projeto Graal também: https://www.graalvm.org/slack-invitation/

2 curtidas

Nativo é muito melhor, sem oveheads.

1 curtida

GraalVM não gera um simples nativo, tem muita coisa da imagem da JVM que lida com uma serie de recursos, mesmo porque é feito usando a JVM.

Em termos de velocidade e tamanho em memoria do resultado final chega a impressionar em muitos casos, o lado ruim é que demora muito pra converter a imagem para nativo.

De qualquer forma é uma ótima opção a mais, e tambem é multi linguagem.

Um Programinha aqui em JVM modularizado consumia cerca de 150 mega, já com nativo gerado pela GraalVM ficou em cerca de 30 mega, e iniciou mais rapido, mas como dito fica preso a plataforma e hoje com as Stores que obrigam codigo nativo é uma ótima opção.

Tem alguns desenvolvedores trabalhando em Compilador Cruzado que atraves de um unico SO será possivel gerar para varios outros SO’s.

Tem futuro o GraalVM.

1 curtida

Acho que você se refere a SubstrateVM, no site oficial da Graal eles descrevem ela dessa forma:

It does not run on the Java VM, but includes necessary components like memory management and thread scheduling from a different virtual machine, called “Substrate VM”. Substrate VM is the name for the runtime components (like the deoptimizer, garbage collector, thread scheduling etc.).

Então algumas coisas como o coletor de lixo, ainda usam uma espécie de máquina virtual embutida no nativo final. Mas o que eu acho mais interessante, é que diferente da JVM ela não compila bytecodes, no nativo final, os seus bytecodes foram compilados para código nativo no momento de criação da imagem.
Então na há necessidade de “aquecer” o compilador JIT, e o programa final inicia bem mais rápido.

Onde falam sobre a SubstrateVM no site da Graal: https://www.graalvm.org/reference-manual/native-image/

Agora eu entendi porque essa gambiarra estava funcionando!
O Handle no código C é um ponteiro, quando eu representei ele no código Java, ele veio como um ponteiro para o ponteiro Handle, e como eu estava passando o ponteiro do ponteiro Handle, e não o próprio ponteiro Handle, não funcionava, quando eu converti para um CIntPointer, e usei o método read(), eu acho que li o valor desse ponteiro, ou seja, o verdadeiro ponteiro Handle.

Agora estou representando o método e o Handle assim:

@CTypedefOfInfo("HANDLE")
public interface Handle extends WordPointer { }
    
@CFunction("GetRawInputDeviceInfoW")
public static native int getRawInputDeviceInfo(Handle deviceHandle, int uiCommand, PointerBase pData, CIntPointer pcbSize);

Mas como o Handle que estou usando no código Java, não é o próprio ponteiro Handle, mas sim um outro ponteiro para ele, quando chamo o método, ao invés de passar o ponteiro do ponteiro Handle como estava fazendo antes, agora uso o método read(), para ler o valor desse outro ponteiro:

getRawInputDeviceInfo(device.deviceHandle().read(), RIDI_DEVICENAME(), name, nameSizePointer)

Então no final, o problema todo é que eu pensava que tinha o ponteiro Handle, mas o que eu realmente tinha, era um ponteiro do ponteiro Handle. Agora funciona sem o cast desnecessário para CIntPointer.

Mas um ponteiro é só um endereço de memória, por isso funciona com o int :wink:

1 curtida

Faz sentido, acho que toda essa ideia teria sido mais fácil se eu tivesse experiência prévia com C, foi um pouco complicado para um explorador do mundo Java se aventurar com todos esses tipos malucos do C :laughing: