SkillAgentSearch skills...

Jaccall

Ja[va] C call[ing]

Install / Use

/learn @udevbe/Jaccall
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Intro

Jaccall makes C libraries accessible from Java without the need to write any native code. It is project similar to JNA or BridJ.

Status:

  • available on maven central: 1.0.5
  • All major features implemented. In maintenance mode.
  • Feature complete [ Linker API, Pointer API, Struct API, Function Pointer API ]
  • Build Status

Jaccall's does not try to be Java, but instead tries to make C accessible in Java.

  • What you allocate, you must free yourself. watch out for memory leaks!
  • Cast to and from anything to anything. Watch out for cast mismatches!
  • Read and write to and from anything to anything. Watch out for segfaults!

Design goals:

  • Simple usage.
  • Simple runtime API.
  • No config files.
  • C only.
  • Support for Linux: aarch64, armv7hf, armv7sf, armv6hf, x86_64, i686.
  • Support for all common use cases: unions, callbacks, pointer-to-pointer, ...

Comparison with other libraries

Jaccall was born out of a frustration with existing solutions. Existing solutions have the nasty trade-off of having a complete but cumbersome API and slow runtime, or have excellent speed and good API but suffer from scope creep while lacking armhf support.

Jaccall tries to remedy this by strictly adhering to the KISS princicple.

Overview

Linker API

The linker API forms the basis of all native method invocation. Without it, you wouldn't be able to call any native methods.

To call a C method, we must create a Java class where we define what C method we are interested in, what they look like and where they can be found. This is done by mapping Java methods to C methods, and providing additional information through annotations.

A linker example

C

library: libsomething.so

header: some_header.h

struct test {
    char field0;
    unsigned short field1;
    int field2[3];
    int *field3;
};
...
struct test do_something(struct test* tst,
                         char field0,
                         unsigned short field1,
                         int* field2,
                         int* field3);

Java SomeHeader.java

@Lib("something")
public class SomeHeader {
    static {
        Linker.link(SomeHeader.class);
    }
    
    @ByVal(StructTest.class)
    public native long do_something(@Ptr(StructTest.class) long tst,
                                    byte field0,
                                    @Unsigned short field1,
                                    @Ptr(int.class) long field2,
                                    @Ptr(int.class) long field3);
}

This Java class exposes the C header file some_header.h to the Java side and informs the linker where these symbols (methods) can be resolved. This is done by providing the @Lib(...) annotation who's value must match the name part of libsomething.so. This whole flow is triggered by calling Linker.link(...).

Don't worry about the struct, that is handled in the Struct API.

In order to pass data back and forth between Java and C, there are a few mapping rules to keep in mind.

  • Java method name must match C method name
  • Java method must be declared native
  • Java method must be declared public
  • Java method must only consist of a specific set of primitives for both arguments and return type.

Mapping

The Java mapping tries to match it's C counterpart as close as possible. There are however a few non intuitive exceptions. Let's have a look on how C types map to their Java counterpart.

| C | Java | |---|------| | unsigned char or char | byte | | unsigned char | @Unsigned byte | | short | short | | unsigned short | @Unsigned short | | int | int| | unsigned int | @Unsigned int| | long | long | | unsigned long | @Unsigned long | | long long | @Lng long | | unsigned long long | @Unsigned @Lng long | | float | float | | double | double | | struct foo | @ByVal(Foo.class) long | | union bar | @ByVal(Bar.class) long | | foo* | @Ptr(Foo.class) long|

The Java primitive types boolean and char do not have a corresponding C type and are not allowed.

The class argument for @Ptr is optional.

Arrays should be mapped as a pointer of the same type.

By value By reference

Java does not support the notion of passing by reference or by value. By default, all method arguments are passed by value in Java, inlcuding POJOs which are actually pointers internally. This limits the size of a single argument in Java to 64-bit. As such, Jaccall can not pass or return a C struct by value. Jaccall works around this problem by allocating heap memory and copyin/reading struct-by-value data. Jaccall must then only pass a pointer between Java and the native side.

The drawback of this approach is that all returned struct-by-value data must be freed manually!

Internals

Jaccall has a compile time step to perform both fail-fast compile time checks and Java source code generation. To aid the Linker in processing a natively mapped Java class, the LinkerGenerator does an initial pass over any @Lib annotated class to verify it's integrity and mapping rules. If this succeeds, it does a second pass and generates a Foo_Jaccall_LinkSymbols.java soure file for every Foo.java annotated with @Lib. This file should not be used by application code. This file contains linker data to aid the Linker in linking the required native Java methods to it's C counterpart.

For every mapped method, 4 parts of linker data are generated.

  • The method name (the C symbol name).
  • The number of arguments.
  • A LibFFI call interface.
  • A JNI signature.

If we reiterate our first mapping example

C some_header.h

struct test {
    char field0;
    unsigned short field1;
    int field2[3];
    int *field3;
};
...
struct test do_something(struct test* tst,
                         char field0,
                         unsigned short field1,
                         int* field2,
                         int* field3);

Java SomeHeader.java

@ByVal(StructTest.class)
public native long do_something(@Ptr(StructTest.class) long tst,
                                byte field0,
                                short field1,
                                @Ptr(int.class) long field2,
                                @Ptr(int.class) long field3);

The generated linker data for this mapping: SomeHeader_Jaccall_LinkSymbols.java

...
@Generated("org.freedesktop.jaccall.compiletime.LinkerGenerator")
public final class SomeHeader_Jaccall_LinkSymbols extends LinkSymbols {
    public SomeHeader_Jaccall_LinkSymbols() {
        super(new String[]{"do_something"},
              new byte[]{5},
              new long[]{JNI.ffi_callInterface(StructTest.FFI_TYPE,
                                               JNI.FFI_TYPE_POINTER,
                                               JNI.FFI_TYPE_SINT8,
                                               JNI.FFI_TYPE_UINT16,
                                               JNI.FFI_TYPE_POINTER,
                                               JNI.FFI_TYPE_POINTER)},
              new String[]{"(JBSJJ)J"});
    }
}
  • "do_something" The name of the method
  • 5 The number of arguments
  • JNI.ffi_callInterface(...) The libffi call interface.
  • "(JBSJJ)J" The JNI method signature.

Linker data of different methods matches on array index.

Pointer API

A pointer example

C

...
size_t int_size = sizeof(int);
void* void_p = malloc(int_size);
int* int_p = (int*) void_p;
...
free(int_p);

This example is pretty self explenatory. A new block of memory is allocated of size 'int'. This block of memory is then cast to a pointer of type int, and finally the memory is cleaned up.

Using Jaccall this translates to

//(Optional) Define a static import of the Pointer and Size classes 
//to avoid prefixing all static method calls.
import static Pointer.*
import static Size.*
...
//Calculate the size of Java type `Integer` wich corresponds to a C int.
int int_size = sizeof((Integer)null);
//Allocate a new block of memory, using `int_size` as it's size.
Pointer<Void> void_p = malloc(int_size);
//Do a pointer cast of the void pointer `void_p` to a pointer of type `Integer`.
Pointer<Integer> int_p = void_p.castp(Integer.class);
...
//free the memory pointed to by `int_p`
int_p.close();

Stack vs Heap

C has the concept of stack and heap allocated memory. Unfortunately this doesn't translate well in Java. Jaccall tries to alleviate this by utilizing Java's garbage collector mechanism. To understand the idea behind this, it's important to know the difference between memory allocated with Pointer.malloc(..) and Pointer.nref(..).

A pointer that refers to malloc memory is not subject to Java's garbage collector and will never be freed manualy. Just like in C, the program is expected to free the memory when it's done with it.

A pointer that refers to nref memory is subject to Java's garbage collector and as such requires no manual call to free. It is meant to mimic C's stack allocated memory. It is strongly advices to use this memory solely within the scope of the method that called nref.

One can also specifically scope heap allocated memory as Jacall defines a Pointer<...> as an

View on GitHub
GitHub Stars12
CategoryDevelopment
Updated2y ago
Forks0

Languages

Java

Security Score

75/100

Audited on Mar 20, 2024

No findings