The aim is to have Java bindings to be able to use the new libgpiod mechanism to control GPIO pins on the Raspberry Pi and/or Libre Computer.
To achieve the goal quite a few Java classes will have to be made, each of them representing one of the structures used by libgpiod.
Below is a list of structures currently used in the newest version of libpgiod (2.0-develop as of this writing):
struct gpiod_chip_info
struct gpiod_chip
struct gpiod_line_info
struct gpiod_info_event
struct gpiod_line_request
struct gpiod_line_settings
struct gpiod_line_config
struct gpiod_request_config
struct gpiod_edge_event
struct gpiod_edge_event_buffer
As a starter (or better: try-out) I started with a quite humble implementation that retrieves two things from the Java world:
Chip info, that is: file descriptor and path of the GPIO chip selected.
Chip info structure, that is: number of lines, label and name of the GPIO chip selected.
Below is a step-by-step approach on how I tackled that first hurdle.
I will start by creating the Makefile. This is the heart of the compilation process.
This is the content of the Makefile:
export JAVA_HOME=/home/pi/mystuff/jdk11
export OUT_DIR=${PWD}/out
export LIBGPIOD_DIR=/home/pi/mystuff/libgpiod/gvcresult/lib
export LIBGPIOD_HEADER_DIR=/home/pi/mystuff/libgpiod/gvcresult/include
export MY_JNI_HEADERS=${OUT_DIR}/jniHeaders
export PROJECT_PACKAGE=${PWD}/java/com/jni/gpiod
export CPP_FOLDER=${PWD}/cpp
export LD_LIBRARY_PATH=${LIBGPIOD_DIR}
CC=gcc
CFLAGS=-Wall
all: clean
mkdir -p ${OUT_DIR} ${MY_JNI_HEADERS}
javac -h ${MY_JNI_HEADERS} -d ${OUT_DIR} ${PROJECT_PACKAGE}/*.java
$(CC) -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux -I${MY_JNI_HEADERS} -I${LIBGPIOD_HEADER_DIR} ${CPP_FOLDER}/Gpiod.cpp -o ${OUT_DIR}/gpiodLib.o
$(CC) -shared -fPIC -o ${OUT_DIR}/libgpiodgvc.so ${OUT_DIR}/gpiodLib.o -lc -L${LIBGPIOD_DIR} -lgpiod
run: all go
go:
java -cp ${OUT_DIR} -Djava.library.path=${OUT_DIR}:${LIBGPIOD_DIR} com.jni.gpiod.Gpiod
clean:
rm -rf ${OUT_DIR} ${MY_JNI_HEADERS}
We first start with defining som environment variables that must be exported.
JAVA_HOME
To start with, JAVA_HOME must be defined so that the system knows which JDK compiler is to be used.
OUT_DIR
OUT_DIR is the directory where I want the generated files to be created. It takes the current directory (PWD: Present Working Directory, a default Linux environment variable) and adds a subdirectory /out to it. So, all generated files (.h, .so, .o,...) will be directly under /out or under a subdirectory of it (depending on how some other environment variables are defined)
LIBGPIOD_DIR
This environment variable must point to the directory location where the library libgpiod can be found. Normally, when you run sudo apt install libgpiod libgpiod-dev the location will be /usr/lib/arm-linux-gnueabihf but since I built the libgpiod library myself (see here) based upon the bleeding-edge source (currently, 2.0-develop) I'm referring to the location where I've put my library.
This will be used during the linking process later on.
LIBGPIOD_HEADER_DIR
LIBGPIOD_HEADER_DIR points to the location of the gpiod.h header file that contains all prototypes of libgpiod. This will be used during the C compilation process later on.
MY_JNI_HEADERS
This environment variable points to the location where the JDK must put the headers generated during the JNI compilation step. This location will also be used during the C compilation process later on.
PROJECT_PACKAGE
This environment variable points to the location of the Java files and has the directory structure that corresponds to the package name used: com.jni.gpiod. It starts from the current working directory ${PWD}.
The java path in front is just another separator to keep the directory structure nice and proper.
CPP_FOLDER
This is the location where the C implementation of the generated header file will be stored. This path will be used during the C compilation phase.
LD_LIBRARY_PATH
This is a very important environment variable. This variable will be used to override the normal libgpiod library location.
During the installation process through sudo apt install libgpiod libgpiod-dev the following two library paths will be added to the file /etc/ld.so.conf.d/arm-linux-gnueabihf:
/lib/arm-linux-gnueabihf
/usr/lib/arm-linux-gnueabihf
Executing the command sudo ld-config will make sure that the libgpiod library will be found in one of those directories. However, since I built libgpiod myself that won't work anymore. During the C linking process the linker will search for libgpiod stuff in the above two mentioned directories. If found, my libgpiod library won't be taken into account anymore.
One solution could be to add the line /home/pi/mystuff/libgpiod/gvcresults/lib to that conf file and well so that it's put before the two other library paths.
This is, however, a bad and very clumsy way of working. One can't ask other people to change the content of that configuration file. All should go correctly through the compilation process.
Therefore, you can "force" the linker to look into a path other than the default paths by adding a linker switch -L (mind the upper case!) to the linking process, pointing to the location where my libgpiod library is located. The command -lgpiod will then take "my" libgpiod.so file (that is, the one I compiled from the libgpiod source files) to link against the resulting libgpiodgvc.so library.
Hence, the importance of this environment variable.
See this SO link for useful information.
CC
Next to the exported environment variables, there are two more local environment variables of which CC is one of them. This just simply defines the compiler that will be used.
Note that I'm deliberately using the C compiler and not the C++ compiler. Although I started the implementation in C++ the author of libgpiod asked me not to use C++ for the Java bindings to avoid another conversion layer in the JNI chain: Java -> C++ -> C. He prefers directly Java -> C.
Hence, the choice for the C compiler and not the C++ compiler.
CFLAGS
This is yet another local environment variable defining some C compiler flags. Here, there's only one: -Wall. This will make the C compiler more strict in giving warnings and errors. For example, unused variables will be tagged by this compiler switch where otherwise the compiler would be silent.
Targets
Next section is the target section. We will have 4 targets:
all
This target calls another target clean and will start from a clean slate and remove everything first (all generated stuff) before restarting the compilation from scratch. The following actions are taken:
call target clean to remove all generated files
invoke the Java compiler to:
compile all Java files
generate the JNI header file
invoke the C compiler to compile the C implementation of the JNI generated header file and generate a .o output file
invoke the C compiler to generate the final libgpiodgvc.so library and take the necessary libraries needed:
-lc: means link against libc.a or libc.so. Since there's the linker option -shared it will take the shared object library, so libc.so.
-L${LIBGPIOD_DIR} means: don't search in the paths given in /etc/ld.so.conf.d/arm-linux-gnueabihf but search in the path defined by ${LIBGPIOD_DIR}
-lgpiod: link against libgpiod.so and look for it in the path given by -L${LIBGPIOD_DIR}
Once the above is finished, we should have our Java file compiled and be able to run our Java code and access the libgpiod library through JNI. See further.
run
This target is quite simple: it calls the target all and then the target go. In other words, this target will compile from scratch and then run the Java application (see further)
go
This target invokes the Java application and runs the Gpiod class file taking into account the classpath and the paths to the needed libraries.
It needs to know the location of the class to run. That's done through the switch -cp (classpath) which in this case points to ${OUT_DIR} . From there it will run the class com.jni.gpiod.Gpiod which is located in ${OUT_DIR}/com/jni/gpiod.
Next, it needs to know the library paths.
One is located in the ${OUT_DIR} directory: it needs the location of libgpiodgvc.so since that will be loaded in the main Java file (see further).
Another one is located in the ${LIBGPIOD_DIR} library (again, see above for the content of it) where it will find the libgpiod.so library so that the C implementations can be called.
clean
As already mentioned before, the target clean will remove all generated files. It can be called directly through make clean or it can be invoked by another target (here: target all).
GpiodChip.java
This class is the Java "counterpart" for the C structure struct gpio_chip.
The class contains the following properties:
public int fileDescriptor: the file descriptor number given to the GPIO chip
public String gpioChipPath: the path in the Linux RFS where the GPIO chip is located
The class also contains an overriden version of the toString() method to feedback the found result.
Below is the implementation. Note that I've chosen com.jni.gpiod as package name:
package com.jni.gpiod;
public class GpiodChip {
public int fileDescriptor;
public String gpioChipPath;
@Override
public String toString() {
return String.format(
"\n GpioChip content:\n" +
" - File descriptor = [%d]\n" +
" - Path = [%s]\n", fileDescriptor, gpioChipPath
);
}
}
GpiodChipInfo.java
This class is the Java "counterpart" for the C structure struct gpio_chip_info.
The class contains the following properties:
public int numLines: the number of lines assigned to the GPIO chip. Each line can correspond to a physical GPIO pin on the GPIO header
public String name: the name of the GPIO chip.
public String label: the label of the GPIO chip. Can be seen as a kind of alias, although not exactly correct.
The class also contains an overriden version of the toString() method to feedback the found result.
Below is the implementation:
package com.jni.gpiod;
public class GpiodChipInfo {
public int numLines;
public String name;
public String label;
@Override
public String toString() {
return String.format(
"\n GpiodChipInfo content:\n" +
" - Number of lines = [%d]\n" +
" - Name = [%s]\n" +
" - Label = [%s]\n"
, numLines
, name
, label
);
}
}
Gpiod.java
Now that we have the two Java classes that are "counterparts" of the libgpiod C structures struct gpio_chip and struct gpio_chip_info we can create our main class Gpiod.java that will define two native methods and call them from the Java world.
We will start with a minimalistic content because we first have to let Java generate our native method's header file.
package com.jni.gpiod;
public class Gpiod {
/**
* Method to retrieve the following info from the GPIO chip:
* - File descriptor
* - GPIO path
*
* @parm - chipName: the GPIO chip for which to retrieve the GPIO chip information
* @return - GpioChip object containing both the file descriptor as well as the GPIO path
* - null if retrieving the information failed
*/
public native GpiodChip getGpiodChip(String chipName);
/**
* Method to retrieve the following info from the GPIO chip:
* - Number of lines
* - GPIO chip name
* - GPIO chip label
*
* @parm - chipName: the GPIO chip for which to retrieve the GPIO chip information
* @return - GpiodChipInfo object containing both the file descriptor as well as the GPIO path
* - null if retrieving the information failed
*/
public native GpiodChipInfo getGpiodChipInfo(String chipName);
}
The first thing that happens, is loading the libgpiodgvc.so library that was created before.