JavaC++

JNI: Shared Libraries mit dem Java Native Interface nutzen

Plattform spezifische Schnittstellen lassen sich mit dem Java Native Interface nutzen. So lässt sich Funktionalität implementieren, die vom Betriebssystem oder auch Hardware abhängig sind.

Das Java Native Interface schlägt eine Brücke zu Plattform spezifischen Schnittstellen. Dazu ist es ähnlich wie bei nativen NodeJs Modulen notwendig in C/C++ geschriebene Funktionen und Methoden mit speziell definierten Datentypen für Parameter und Rückgabewerte zu schreiben. Diese können dann in Java Methoden aufgerufen werden. Zudem kann die Java VM im C/C++ Code referenziert werden und es können Java Objekte instanziiert werden und Methoden der Objekte wie auch Methoden aus einem statischen Kontext können aufgerufen werden.

Eine CLI Anwendung zur Maussteuerung

Als Beispiel Anwendung soll eine Kommandozeilenanwendung zur Maussteuerung dienen. Diese wird für Windows entwickelt und greift auf Funktionen der WinAPI zurück. Hierbei habe ich allerdings darauf geachtet, dass ich einen Compiler einsetze, der Plattform unabhängig ist und mit ein wenig Rechereche diese Anwendung auch auf anderen Betriebssysteme portiert werden kann. Zumindest für Linux weiß ich das es eine Tatsache ist.


Eine Schnittstelle zwischen Java und nativem Code

Um eine Klasse auf Funktionen und Methoden nativer Bibiliotheken verweisen zulassen müssen wir drei Dinge wissen:

  1. Die loadLibrary(String name) Methode der System Klasse lädt die Shared Library aus dem Verzeichnis, das in der Option library.path angegeben wurde. Das Argument ist der Dateiname ohne Dateierweiterung. Die Erweiterung ändert sich je nach Betriebssystem ohnehin.
  2. Sie wird im static Block der Klasse aufgerufen. Dieser wird nur beim erstmaligen Laden der Klasse in den Speicher noch vor dem Konstruktor ausgeführt.
  3. Das Schlüsselwort native gibt dem Java Compiler auskunft, das die Methode in der geladenen Bibliothek zu finden ist. Diese Methoden haben daher auch keinen Body.

src/main/java/de/michm/vin/lib/Mouse.java

package de.michm.vin.lib;
// We need to get the screen resolution later.
import java.awt.*;

public class Mouse {    
    /*
        The duration after which the state will switch 
        from down to up.
    */
    final private static int CLICK_DURATION = 300;

    /*
        These constants are flags that are passed when 
        calling the Windows API function SendInput().
    */
    final public static long LEFT_DOWN = 0x0002;
    final public static long LEFT_UP = 0x0004;
    final public static long RIGHT_DOWN = 0x0008;
    final public static long RIGHT_UP = 0x0010;
    final public static long MIDDLE_DOWN = 0x0020;
    final public static long MIDDLE_UP = 0x0040;

    /*
        Loads vinlib.dll
    */
    static {
        System.loadLibrary("vinlib");
    }

    /**
     * Moves the mouse cursor relative to the
     * current position along x and y-axis.
     * @param x long value of x-axis offset
     * @param y long value of y-axis offset
     */
    protected native void move(long x, long y);

    /**
     * Moves the mouse cursor absolute to the
     * screen size along x and y-axis.
     * @param x long value of x-axis position
     * @param y long value of y-axis position
     */
    protected void moveAbs(long x, long y) {
        final long MAX_SIZE = 0xFFFF;
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        double width = screenSize.getWidth();
        double height = screenSize.getHeight();
        double factorX = x / width;
        double factorY = y / height;

        x = Math.round(MAX_SIZE * factorX);
        y = Math.round(MAX_SIZE * factorY);
        nativeMoveAbs(x, y);
    }

    /**
     * Moves the mouse cursor absolute to the
     * screen size along x and y-axis.
     * @param x long value of x-axis position
     * @param y long value of y-axis position
     */
    private native void nativeMoveAbs(long x, long y);

    /**
     * Sends left click event to the operating system.
     */
    protected void click() {
        click(LEFT_DOWN);
    }

    /**
     * Sends click event to the operating system.
     * @param button one of following final long values:
     *               LEFT_DOWN, LEFT_UP,
     *               RIGHT_DOWN, RIGHT_UP,
     *               MIDDLE_DOWN, MIDDLE_UP
     */
    protected void click(long button) {
        try {
            nativeClick(button);
            Thread.sleep(CLICK_DURATION);
            switch ((int) button) {
                case (int) LEFT_DOWN -> nativeClick(LEFT_UP);
                case (int) RIGHT_DOWN -> nativeClick(RIGHT_UP);
                case (int) MIDDLE_DOWN -> nativeClick(MIDDLE_UP);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * Sends click event to the operating system.
     * @param button one of following final long values:
     *               LEFT_DOWN, LEFT_UP,
     *               RIGHT_DOWN, RIGHT_UP,
     *               MIDDLE_DOWN, MIDDLE_UP
     */
    private native void nativeClick(long button);

    /**
     * Calls the winapi function GetCursorPos()
     * and wraps it result into a new Point Object.
     * @return a Point object with cursor coordinates
     */
    protected native Point nativeGetCursorPos();
}

C Header Dateien erzeugen

Man könnte die Header Definitionen auch einfach selbst schreiben, allerdings bietet der Java Compiler die Möglichkeit diese für uns aus einer Klasse zu erzeugen.

Dies funktioniert mit folgendem Parametern, die dem Java Compiler übergeben werden:

javac -h .\src\main\native\win32 -d .\target\classes -classpath .\src\main\java .\src\main\java\de\michm\vin\lib\Mouse.java

Dadurch wird folgende Header Datei im Ordner src\main\native\win32 erzeugt.

src/main/native/win32/de_michm_vin_lib_Mouse.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class de_michm_vin_lib_Mouse */

#ifndef _Included_de_michm_vin_lib_Mouse
#define _Included_de_michm_vin_lib_Mouse
#ifdef __cplusplus
extern "C" {
#endif
#undef de_michm_vin_lib_Mouse_CLICK_DURATION
#define de_michm_vin_lib_Mouse_CLICK_DURATION 300L
#undef de_michm_vin_lib_Mouse_LEFT_DOWN
#define de_michm_vin_lib_Mouse_LEFT_DOWN 2i64
#undef de_michm_vin_lib_Mouse_LEFT_UP
#define de_michm_vin_lib_Mouse_LEFT_UP 4i64
#undef de_michm_vin_lib_Mouse_RIGHT_DOWN
#define de_michm_vin_lib_Mouse_RIGHT_DOWN 8i64
#undef de_michm_vin_lib_Mouse_RIGHT_UP
#define de_michm_vin_lib_Mouse_RIGHT_UP 16i64
#undef de_michm_vin_lib_Mouse_MIDDLE_DOWN
#define de_michm_vin_lib_Mouse_MIDDLE_DOWN 32i64
#undef de_michm_vin_lib_Mouse_MIDDLE_UP
#define de_michm_vin_lib_Mouse_MIDDLE_UP 64i64
/*
 * Class:     de_michm_vin_lib_Mouse
 * Method:    move
 * Signature: (JJ)V
 */
JNIEXPORT void JNICALL Java_de_michm_vin_lib_Mouse_move
  (JNIEnv *, jobject, jlong, jlong);

/*
 * Class:     de_michm_vin_lib_Mouse
 * Method:    nativeMoveAbs
 * Signature: (JJ)V
 */
JNIEXPORT void JNICALL Java_de_michm_vin_lib_Mouse_nativeMoveAbs
  (JNIEnv *, jobject, jlong, jlong);

/*
 * Class:     de_michm_vin_lib_Mouse
 * Method:    nativeClick
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_de_michm_vin_lib_Mouse_nativeClick
  (JNIEnv *, jobject, jlong);

/*
 * Class:     de_michm_vin_lib_Mouse
 * Method:    nativeGetCursorPos
 * Signature: ()Lde/michm/vin/lib/Point;
 */
JNIEXPORT jobject JNICALL Java_de_michm_vin_lib_Mouse_nativeGetCursorPos
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

Implementation der Header Datei

Nun können wir die einzelnen Funktionen aus der Header Datei implementieren. Wir greifen dabei auf die Funktionen SendInput() und GetCursorPos() der Windows API zurück.

src/main/native/win32/de_michm_vin_lib_Mouse.cpp

#include "de_michm_vin_lib_Mouse.h"
#include <Windows.h>
#include <iostream>

/**
 * Moves mouse cursor to the specified coordinates.
 *
 * @param long x-axis offset / position
 * @param long y-axis offset / position
 * @param long flags for DWORD Flags
 */
void mouseMove(long x, long y, long flags) {
    INPUT inputs[1] = {};

    ZeroMemory(inputs, sizeof(inputs));

    inputs[0].type = INPUT_MOUSE;
    inputs[0].mi.dx = x;
    inputs[0].mi.dy = y;
    inputs[0].mi.dwFlags = MOUSEEVENTF_MOVE | flags;

    SendInput(ARRAYSIZE(inputs), inputs, sizeof(INPUT));
}

/**
 * Moves mouse cursor relative to it's current position.
 *
 * @param JNIEnv*
 * @param jobject
 * @param jlong x-axis offset
 * @param jlong y-axis offset
 */
JNIEXPORT void JNICALL Java_de_michm_vin_lib_Mouse_move(JNIEnv *env, jobject jobj, jlong x, jlong y) {
    mouseMove((long) x, (long) y, 0x0000);
}

/**
 * Moves mouse cursor absolute to the screen.
 *
 * @param JNIEnv*
 * @param jobject
 * @param jlong x-axis position
 * @param jlong y-axis position
 */
JNIEXPORT void JNICALL Java_de_michm_vin_lib_Mouse_nativeMoveAbs(JNIEnv *env, jobject jobj, jlong x, jlong y) {
    mouseMove((long) x, (long) y, MOUSEEVENTF_ABSOLUTE);
}

/**
 * Sets the mouse button state.
 *
 * @param JNIEnv*
 * @param jobject
 * @param jlong button
 */
JNIEXPORT void JNICALL Java_de_michm_vin_lib_Mouse_nativeClick(JNIEnv *env, jobject jobj, jlong button) {
    INPUT inputs[1] = {};

    ZeroMemory(inputs, sizeof(inputs));

    inputs[0].type = INPUT_MOUSE;
    inputs[0].mi.dwFlags = (long) button;

    SendInput(ARRAYSIZE(inputs), inputs, sizeof(INPUT));
}

/**
 * Calls GetCursorPos and wraps it's result into a Point object and
 * returns it to the Java context.
 *
 * @param JNIEnv*
 * @param jobject
 * @return Point object
 */
JNIEXPORT jobject JNICALL Java_de_michm_vin_lib_Mouse_nativeGetCursorPos(JNIEnv *env, jobject obj) {
    LPPOINT pt;
    jlong x = -1;
    jlong y = -1;
    jclass pointClass = env->FindClass("de/michm/vin/lib/Point");
    jmethodID jconstructor = env->GetMethodID(pointClass, "<init>", "(JJ)V");

    if (GetCursorPos(pt)) {
        x = (jlong) pt->x;
        y = (jlong) pt->y;
    }

    return env->NewObject(pointClass, jconstructor, x, y);
}

Java's primitive Datentypen können mit ihrem nativen Gegenstücken nach belieben hin und zurück gecasted werden.

Type casting ist eine expliziete Typkonvertierung.

Komplizierter wird es mit allem was nicht auf dem Stack gespeichert wird.

Nach dem alle Funktionen implementiert wurden, kann man mit dem GCC Compiler nun eine Shared Library kompilieren.

g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 .\src\main\native\win32\de_michm_vin_lib_Mouse.cpp -o .\src\main\resources\vinlib.o
g++ -shared -o .\src\main\resources\vinlib.dll .\src\main\resources\vinlib.o

Point Klasse

Die Point Klasse ist simpel strukturiert. Sie hat 2 Eigenschaften: X und Y. Beide sind vom Typ long und als final deklariert. Der Konstruktor wird überladen. Die erste Variante nimmt einen String an, welcher in X und Y geparsed wird. Die zweite nimmt schlicht Werte für X und Y entgegen.

src/main/java/de/michm/vin/lib/Point.java

package de.michm.vin.lib;

public class Point {
    private final long x;
    private final long y;

    Point(String cords) {
        cords = cords.replace(", ", ",");

        int separatorIndex = cords.indexOf(',');
        String pointX = cords.substring(0, separatorIndex);
        String pointY = cords.substring(separatorIndex + 1);

        x = Long.parseLong(pointX);
        y = Long.parseLong(pointY);
    }

    Point(long x, long y) {
        this.x = x;
        this.y = y;
    }

    public long getX() {
        return x;
    }
    public long getY() {
        return y;
    }
}

NbBufferReader Klasse

Ein normaler Scanner würde den Thread blockieren. Das würde den gewünschten Programmfluss behindern. Daher implementieren wir eine Klasse, die in einem extra Thread auf eine Eingabe wartet. Wird eine neue Zeile in die Eingabe geschrieben, so wird diese in eine Queue abgelegt. Dort kann sie dann im Laufe des Main-Loops herausgeholt werden.

src/main/java/de/michm/vin/lib/NbBufferedReader.java

package de.michm.vin.lib;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

public class NbBufferedReader {
    private final BlockingQueue<String> lines = new LinkedBlockingQueue<>();
    private volatile boolean closed = false;
    private volatile boolean ready = false;
    private Thread backgroundThread;

    /**
     * Creates a new NbBufferedReader object.
     *
     * @param in InputStream
     */
    public NbBufferedReader(final InputStream in) {
        final BufferedReader reader = new BufferedReader(new InputStreamReader(in));

        backgroundThread = new Thread(() -> {
            try {
                while (!Thread.interrupted()) {
                    String line = reader.readLine();
                    ready = reader.ready();

                    if (line == null)
                        break;

                    lines.add(line);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            } finally {
                closed = true;
            }
        });

        backgroundThread.setDaemon(true);
        backgroundThread.start();
    }

    /**
     * Retrieves and removes the first line of
     * the internal queue. Returns null if the
     * queue is empty or the reader is closed.
     *
     * @return String first line of the queue
     * @throws IOException
     */
    public String readLine() throws IOException {
        try {
            return closed && lines.isEmpty() ? null : lines.poll(100L, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            throw new IOException("The BackgroundReaderThread was interrupted!", e);
        }
    }

    /**
     * Closes the BufferedReader
     *
     */
    public void close() {
        if (backgroundThread != null) {
            backgroundThread.interrupt();
            backgroundThread = null;
        }
    }

    /**
     * Returns ready state
     *
     * @return boolean ready
     */
    public boolean isReady() {
        return ready;
    }
}

Das Schlüsselwort volatile bedeutet in Java, dass auf die Variable Thread übergreifend zugriffen wird. In C und C++ bedeutet es allerdings, dass der Zustand der Variable flüchtig ist und sich ohne expliziten Zugriff aus dem Source Code ändern kann.


Test Klasse

Abschließend bleibt nur noch die Implementation der einzelnen Klassen in einer Anwendung.

src/main/java/de/michm/vin/lib/Test.java

package de.michm.vin.lib;

import java.io.IOException;

public class Test {
    public static void main(String[] args) throws IOException {
        boolean run = true;
        boolean abs = false;
        boolean pos = false;
        boolean isIndicating = false;
        final NbBufferedReader reader = new NbBufferedReader(System.in);
        Point pt = new Point(-1, -1);

        String input = null;
        Mouse mouse = new Mouse();

        printHelp();

        while (run) {
            if (!isIndicating) {
                isIndicating = true;
                System.out.print("> ");
            }

            input = reader.readLine();

            if (input != null) {
                isIndicating = false;

                if (input.equals("q") || input.equals("quit")) {
                    run = false;
                } else if (!abs && input.matches("^-?\\d+, ?-?\\d+$")) {
                    Point point = new Point(input);
                    mouse.move(point.getX(), point.getY());
                } else if (abs && input.matches("^-?\\d+, ?-?\\d+$")) {
                    Point point = new Point(input);
                    mouse.moveAbs(point.getX(), point.getY());
                } else if (input.matches("^click .+$") || input.equals("click")) {
                    String btnIn = input.substring(input.indexOf(' ') + 1);

                    if (btnIn.equals("left") || btnIn.equals("click")) {
                        mouse.click();
                    } else if (btnIn.equals("right")) {
                        mouse.click(Mouse.RIGHT_DOWN);
                    } else if (btnIn.equals("middle")) {
                        mouse.click(Mouse.MIDDLE_DOWN);
                    }
                } else if (input.startsWith("abs ")) {
                    abs = input.endsWith(" true");
                } else if (input.equals("abs")) {
                    System.out.printf("> abs: %s\n", abs);
                } else if (input.equals("pos") || input.equals("p")) {
                    pos = !pos;
                } else if (input.equals("help")) {
                    printHelp();
                }
            }

            if (input != null && input.isEmpty() && pos) {
                pos = false;
            } else if (pos) {
                Point mousePoint = mouse.nativeGetCursorPos();
                boolean hasChanged = mousePoint.getX() != pt.getX() || mousePoint.getY() != pt.getY();

                if (hasChanged) {
                    System.out.println(String.format("x: %s, y: %s", mousePoint.getX(), mousePoint.getY()));
                    isIndicating = false;
                    pt = mousePoint.clone();
                }

            }
        }

        reader.close();
    }

    private static void printHelp() {
        System.out.println("\nEnter coordinates to which the cursor should move. [x, y]");
        System.out.println("e.g.: 120, 80\n");
        System.out.println("Enter click <left|right|middle> to click with the");
        System.out.println("specified mouse button.\n");
        System.out.println("Enter abs <true|false> to toggle move mode");
        System.out.println("between absolute to screen size and relative to position.\n");
        System.out.println("Enter pos to receive system mouse cursor position.");
        System.out.println("Hit enter to unhook.\n");
        System.out.println("Enter quit or hit \"q\" to quit.\n");
    }
}

Repository

GitHub - jibbex/vinlib
Contribute to jibbex/vinlib development by creating an account on GitHub.
image