|
| 1 | +# Getting started |
| 2 | + |
| 3 | +## Concepts |
| 4 | + |
| 5 | +The main concept behind the library is to provide easy access to native structures and functions of dynamic libraries written in different languages. |
| 6 | +Java FFM API provides solid core objects and patterns to interact with native code, but is lacking of DevEx when working with real native code. |
| 7 | +That said, the developer needs to write a lot of boilerplate code to make s simple native call and understand the whole native-java engine under the hood. |
| 8 | + |
| 9 | +To solve this issue `native-memory-processor` consists of two major parts: |
| 10 | +1) `annotation` project, which declares core annotations and some helpers to work with native code in runtime. |
| 11 | + - `@NativeMemory` - main annotation to interface. Provide header files for parsing or leave empty if you need just native function generation. |
| 12 | + - `@NativeMemoryOptions` - annotation to interface, can define optional advanced options to code generation. |
| 13 | + - `@Structs` / `@Struct` - annotations to interface, defines the intent to parse structures. |
| 14 | + - `@Enums` / `@Enum` - annotations to interface, defines the intent to parse enums. |
| 15 | + - `@Unions` / `@Union` - annotations to interface, defines the intent to parse unions. |
| 16 | + - `@NativeFunction` - annotation to method in interface, defines the native function to be generated. |
| 17 | + - `@ByAddress` / `@Returns` - annotation to parameter of method in interface, defines parameter option to send as a pointer / value and native function return object. |
| 18 | + |
| 19 | +2) `annotation-processor` project, which provides processing of annotation described above at compile time in few steps: |
| 20 | + - indicate the main `@NativeMemory` annotation on interface |
| 21 | + - get provided header files (if any) and generate structures/enums/unions as defined by corresponding annotations |
| 22 | + - get annotated by `@NativeFunction` methods in interface and generate java-to-native code |
| 23 | + |
| 24 | +In general, library tries to encapsulate the hardcore part of native interacting by code generation and provide user-specific classes with variety of options. |
| 25 | + |
| 26 | +## Difference from jextract |
| 27 | + |
| 28 | +The OpenJDK team had already created a similar tool, called `jextract` to keep developer hands clean from native code. |
| 29 | +This is a standalone CLI tool, which works with `libclang` to parse provided header files and generate the Java FFM-ready code. |
| 30 | +While it is great in concept, I found it 'not-so-friendly' for a person, who never worked with native code before. |
| 31 | +Mainly, the interacting and generated code is hard to understand in different ways: |
| 32 | +- structure/function names usually uses `_` and `$` signs or its combinations in generated methods |
| 33 | +- it is HUGE! Simple `gpiochip_info` structure from `gpio.h` kernel UAPI of three fields transforms to 284 lines of code (sic!) |
| 34 | +- jextract holds no context of what it is generating. Meaning you can get a header file for defining primitive types by kernel (e.g. `__kernel_sighandler_t` for above structure), which is usually do not needed in your code |
| 35 | +- there is no simple way to understand the connections between generated classes, because of it's size and naming |
| 36 | +- it is standalone binary, which is needed to be connected somehow to your gradle/maven build toolchain |
| 37 | + |
| 38 | +I tried to get all advantages of `jextract` and create more user-friendly version of it. |
| 39 | +Still both `native-memory-processor` and `jextract` uses the same parsing library `libclang` and the quality of parsers are the same, but the process is very different: |
| 40 | +- annotation processing by gradle/maven instead of standalone binary |
| 41 | +- very specific control over the objects you are parsing with context based approach |
| 42 | +- more helpers and code validation |
| 43 | + |
| 44 | +## Example usages |
| 45 | +### Example 1 |
| 46 | +Generate all structures, enums and unions from `gpio.h` and generate file `GPIONative.java` with call of native function `ioctl` with errno support. |
| 47 | +```java |
| 48 | +@NativeMemory(header = "gpio.h") |
| 49 | +@Structs |
| 50 | +@Enums |
| 51 | +@Unions |
| 52 | +public interface GPIO { |
| 53 | + @NativeFunction(name = "ioctl", useErrno = true, returnType = int.class) |
| 54 | + int nativeCall(int fd, long command, int data) throws NativeMemoryException; |
| 55 | +} |
| 56 | +``` |
| 57 | +### Example 2 |
| 58 | +Generate only structures with specified name mapping from kernel header and generate file `GPIONative.java` with call of native function `ioctl` without errno support. |
| 59 | +Method declaration will forward the output of `ioctl` to user code, since `returnType` option and return type of method are the same |
| 60 | +```java |
| 61 | +@NativeMemory(header = "/usr/src/linux-headers-6.2.0-39/include/uapi/linux/gpio.h") |
| 62 | +@Structs({ |
| 63 | + @Struct(name = "gpiochip_info", javaName = "ChipInfo"), |
| 64 | + @Struct(name = "gpio_v2_line_info", javaName = "LineInfo") |
| 65 | +}) |
| 66 | +public interface GPIO { |
| 67 | + @NativeFunction(name = "ioctl", returnType = int.class) |
| 68 | + int nativeCall(int fd, long command, int data) throws NativeMemoryException; |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +### Example 3 |
| 73 | + |
| 74 | +Generate only structures with specified name mapping from kernel header and generate file `GPIONative.java` with call of two native functions `ioctl`. |
| 75 | +You can declare a freshly generated structure to use within the native functions. The difference between methods are in native function declaration: |
| 76 | +- first one declare a native function `int ioctl(int, long, gpiochip_info*)`, which passes `data` as pointer, writes the data to structure and returns it to user code as generated object |
| 77 | +- the second one returns the `data` variable with type long as a pointer and returns it to user code |
| 78 | + |
| 79 | +```java |
| 80 | +@NativeMemory(header = "/usr/src/linux-headers-6.2.0-39/include/uapi/linux/gpio.h") |
| 81 | +@Structs({ |
| 82 | + @Struct(name = "gpiochip_info", javaName = "ChipInfo"), |
| 83 | + @Struct(name = "gpio_v2_line_info", javaName = "LineInfo") |
| 84 | +}) |
| 85 | +public interface GPIO { |
| 86 | + @NativeFunction(name = "ioctl", returnType = int.class) |
| 87 | + ChipInfo nativeCall(int fd, long command, @Returns ChipInfo data) throws NativeMemoryException; |
| 88 | + |
| 89 | + @NativeFunction(name = "ioctl", useErrno = true, returnType = int.class) |
| 90 | + long nativeCall(int fd, long command, @Returns @ByAddress long data) throws NativeMemoryException; |
| 91 | +} |
| 92 | +``` |
| 93 | +### Example 4 |
| 94 | + |
| 95 | +You can use system properties `-Dversion=6.2.0-39` to pass variables into header path definition and define options: |
| 96 | +- add paths for `libclang` include lookup. |
| 97 | +- specify generated code location with `packageName` |
| 98 | +- combine all top level `#define` in header file to `GPIOConstant` enum (if all source variables are of the same type) or static class (if source variable are of different types) |
| 99 | + |
| 100 | +```java |
| 101 | +@NativeMemory(header = "/usr/src/linux-headers-${version}/include/uapi/linux/gpio.h") |
| 102 | +@NativeMemoryOptions( |
| 103 | + includes = "/usr/lib/llvm-15/lib/clang/15.0.7/include/", |
| 104 | + packageName = "org.my.project", |
| 105 | + processRootConstants = true |
| 106 | +) |
| 107 | +@Structs |
| 108 | +public interface GPIO { |
| 109 | + @NativeFunction(name = "ioctl", returnType = int.class) |
| 110 | + GpiochipInfo nativeCall(int fd, long command, @Returns GpiochipInfo data) throws NativeMemoryException; |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +### Example 5 |
| 115 | +Generate `some_struct` structure from `library2_header.h` and implement two native functions: |
| 116 | +- `some_func1` from library `library1` with provided absolute path |
| 117 | +- `some_func2` from library `library2` with just name. Java will try to load the library using standard POSIX pattern (e.g. on Linux will look over `LD_LIBRARY_PATH`) |
| 118 | +```java |
| 119 | +@NativeMemory(header = "library2_header.h") |
| 120 | +@Structs |
| 121 | +public interface GPIO { |
| 122 | + @NativeFunction(name = "some_func1", library = "/some/path/to/library1.so") |
| 123 | + void call(int data) throws NativeMemoryException; |
| 124 | + |
| 125 | + @NativeFunction(name = "some_func2", library = "library2") |
| 126 | + SomeStruct nativeCall(int param, @Returns SomeStruct data) throws NativeMemoryException; |
| 127 | +} |
| 128 | +``` |
0 commit comments