Skip to content

Commit 30dae1a

Browse files
committed
Fixes #10
Fixes #9 #7 code refactoring
1 parent c7445a5 commit 30dae1a

File tree

15 files changed

+378
-325
lines changed

15 files changed

+378
-325
lines changed

.github/workflows/gradle.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ jobs:
3535
run: chmod +x ./gradlew
3636
- name: Build Gradle
3737
run: |
38-
./gradlew build -DheaderVersion=$headerVersion
38+
./gradlew build -Dversion=$headerVersion

.github/workflows/maven-publish.yml

Lines changed: 0 additions & 32 deletions
This file was deleted.

README.md

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,65 @@
33
![Maven Central Version](https://img.shields.io/maven-central/v/io.github.digitalsmile.native/annotation?label=annotation)
44
![Maven Central Version](https://img.shields.io/maven-central/v/io.github.digitalsmile.native/annotation-processor?label=annotation-processor)
55
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/digitalsmile/native-memory-processor/gradle.yml)
6+
67
## Introduction
8+
79
With the release of JDK 22 the new Foreign Function & Memory API (FFM API) has been introduced from preview phase.
810

911
This API is intended to substitute Java Native Interface (JNI) and become a standard of interacting with native code from Java.
1012

11-
While FFM API itself is very powerful and can provide access to onheap/offheap memory regions and call a native function, it is lacking of extended features of converting C/C++ structures to Java classes.Of course, we have `jextract` tool, but the code it generates is kind of messy and hard to understand.
13+
While FFM API itself is very powerful and can provide access to onheap/offheap memory segments and call a native function, it is lacking of extended features of converting C/C++ structures to Java classes.
14+
Of course, we have `jextract` tool, but the code it generates is kind of messy and hard to understand.
15+
16+
This project goal is to combine the power of FFM API and Java, like annotation processing, to make you happy while working with native functions and structures.
1217

13-
This project goal is to combine the power of FFM API and some Java patterns, like annotation and annotation processing, to make you happy while working with native functions and structures.
1418
## Features
1519
- two separate libraries - `annotation` and `annotation-processor` working with code generation during compile time
1620
- C/C++ types support in top level: struct, union, enum
17-
- can load third party libraries or use already loaded by classloader
18-
- flexible native-to-java method declarations
21+
- can load third party native libraries or use already loaded by classloader
22+
- flexible native-to-java function declarations
1923
- pass to native functions primitives and objects, by value or just a pointer
2024
- java exceptions with the power of errno/strerr
2125
- safe and clean code generation
2226
- useful helpers to work with structures, like `.createEmpty()`
23-
- ready to write your project unit tests with generated structures
24-
## Getting started
25-
1) Ensure you are working with JDK22+.
26-
2) To enable FFM API access use `--enable-native-access=ALL-UNNAMED` run parameter or add `Enable-Native-Access: ALL-UNNAMED` to jar manifest file.
27-
3) Add dependencies to your `build.gradle`:
27+
- ready to write your project unit tests with generated native structures
28+
29+
## Requirements
30+
31+
- `JDK22+` with FFM API enabled ( use `--enable-native-access=ALL-UNNAMED` run parameter or add `Enable-Native-Access: ALL-UNNAMED` to jar manifest file)
32+
- `glibc 2.31+` (ubuntu 20.04+ or MacOS Sonoma+) on host machine where annotation processor is running (requirement for `libclang`).
33+
Windows hosts are not yet supported.
34+
35+
## Quick start
36+
37+
1) Add dependencies to your `build.gradle`:
2838
```groovy
2939
dependencies {
30-
annotationProcessor 'io.github.digitalsmile.native:annotation-processor:{$version}'
40+
// Annotations to use for code generation
3141
implementation 'io.github.digitalsmile.native:annotation:{$version}'
42+
// Process annotations and generate code at compile time
43+
annotationProcessor 'io.github.digitalsmile.native:annotation-processor:{$version}'
3244
}
3345
```
34-
4) Define interface class and run your build:
46+
47+
2) Define interface class and run your build:
3548
```java
3649
@NativeMemory(header = "gpio.h")
3750
@Structs
3851
@Enums
3952
public interface GPIO {
40-
@Function(name = "ioctl", useErrno = true, returnType = int.class)
53+
@NativeFunction(name = "ioctl", useErrno = true, returnType = int.class)
4154
int nativeCall(int fd, long command, int data) throws NativeMemoryException;
4255
}
4356
```
4457
This code snippet will generate all structures and enums within the header file `gpio.h` (located in `resources` folder), as well as `GPIONative` class with call implementation of `ioctl` native function.
45-
Find more examples in javadoc or in my other project https://github.com/digitalsmile/gpio
58+
Find more examples in [documentation](USAGE.md) or in my other project https://github.com/digitalsmile/gpio
59+
60+
3) Enjoy! :)
4661

47-
5) Enjoy! :)
4862
## Plans
63+
4964
- more support of native C/C++ types and patterns
50-
- adding `libclang` as a separate transient dependency
5165
- validation of structures parameters and custom validation support
5266
- adding more context on different header files, especially connected with each other
53-
- adding javadoc in code generation classes
5467

USAGE.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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+
```

annotation-processor/src/main/java/io/github/digitalsmile/NativeProcessor.java

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import io.github.digitalsmile.annotation.NativeMemory;
66
import io.github.digitalsmile.annotation.NativeMemoryOptions;
77
import io.github.digitalsmile.annotation.function.ByAddress;
8-
import io.github.digitalsmile.annotation.function.Function;
8+
import io.github.digitalsmile.annotation.function.NativeFunction;
99
import io.github.digitalsmile.annotation.function.NativeMemoryException;
1010
import io.github.digitalsmile.annotation.function.Returns;
1111
import io.github.digitalsmile.annotation.structure.Enums;
@@ -15,6 +15,7 @@
1515
import io.github.digitalsmile.composers.FunctionComposer;
1616
import io.github.digitalsmile.composers.StructComposer;
1717
import io.github.digitalsmile.functions.FunctionNode;
18+
import io.github.digitalsmile.functions.Library;
1819
import io.github.digitalsmile.functions.ParameterNode;
1920
import io.github.digitalsmile.headers.mapping.ObjectTypeMapping;
2021
import io.github.digitalsmile.headers.model.NativeMemoryModel;
@@ -46,7 +47,7 @@
4647
import java.util.*;
4748
import java.util.regex.Pattern;
4849

49-
@GeneratePrism(Function.class)
50+
@GeneratePrism(NativeFunction.class)
5051
@SupportedSourceVersion(SourceVersion.RELEASE_22)
5152
public class NativeProcessor extends AbstractProcessor {
5253

@@ -56,21 +57,6 @@ public Set<String> getSupportedAnnotationTypes() {
5657
}
5758

5859

59-
public record Library(String libraryName, boolean isAlreadyLoaded) {
60-
@Override
61-
public boolean equals(Object o) {
62-
if (this == o) return true;
63-
if (o == null || getClass() != o.getClass()) return false;
64-
Library library = (Library) o;
65-
return isAlreadyLoaded == library.isAlreadyLoaded && Objects.equals(libraryName, library.libraryName);
66-
}
67-
68-
@Override
69-
public int hashCode() {
70-
return Objects.hash(libraryName, isAlreadyLoaded);
71-
}
72-
}
73-
7460
@Override
7561
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
7662
if (roundEnv.processingOver()) {
@@ -88,7 +74,7 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
8874

8975
processHeaderFiles(rootElement, headerFiles, packageName, nativeOptions);
9076

91-
List<Element> functionElements = new ArrayList<Element>(roundEnv.getElementsAnnotatedWith(Function.class)).stream()
77+
List<Element> functionElements = new ArrayList<Element>(roundEnv.getElementsAnnotatedWith(NativeFunction.class)).stream()
9278
.filter(f -> f.getEnclosingElement().equals(rootElement)).toList();
9379
processFunctions(rootElement, functionElements, packageName);
9480

@@ -109,7 +95,7 @@ private void processHeaderFiles(Element element, String[] headerFiles, String pa
10995
List<Path> headerPaths = getHeaderPaths(headerFiles);
11096
for (Path path : headerPaths) {
11197
if (!path.toFile().exists()) {
112-
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Cannot find header file '" + path + "'!", element);
98+
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Cannot find header file '" + path + "'! Please, check file location", element);
11399
return;
114100
}
115101
}
@@ -221,7 +207,7 @@ private void processFunctions(Element rootElement, List<Element> functionElement
221207
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Method '" + functionElement + "' must throw NativeMemoryException!", functionElement);
222208
break;
223209
}
224-
var instance = FunctionPrism.getInstanceOn(functionElement);
210+
var instance = NativeFunctionPrism.getInstanceOn(functionElement);
225211
if (instance == null) {
226212
break;
227213
}
@@ -260,9 +246,20 @@ private void processFunctions(Element rootElement, List<Element> functionElement
260246
private List<Path> getHeaderPaths(String[] headerFiles) {
261247
List<Path> paths = new ArrayList<>();
262248
for (String headerFile : headerFiles) {
249+
var beginVariable = headerFile.indexOf("${");
250+
var endVariable = headerFile.indexOf("}");
251+
if (beginVariable != -1 && endVariable != -1) {
252+
var sub = headerFile.substring(beginVariable + 2, endVariable);
253+
var property = System.getProperty(sub);
254+
if (property != null) {
255+
headerFile = headerFile.replace("${" + sub + "}", property);
256+
} else {
257+
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "'" + headerFile + "' contains system property '" + sub + "', which is no defined");
258+
continue;
259+
}
260+
}
263261
Path headerPath;
264262
if (headerFile.startsWith("/")) {
265-
headerFile = headerFile.replace("${version}", System.getProperty("headerVersion"));
266263
headerPath = Path.of(headerFile);
267264
} else {
268265
Path rootPath;

0 commit comments

Comments
 (0)