Friends of OpenJDK Today

Project Panama for Newbies (Part 3)

September 13, 2021

Author(s)

  • Avatar photo
    Brice Dutheil

    Brice Dutheil is a software engineer, aficionado of open source software. Previously a Mockito core contributor, now a Dad of 2.

  • Carl Dea

    Carl Dea is a Senior Developer Advocate at Azul. He has authored Java books and has been developing software for 20+ years with many clients, from Fortune 500 companies to ... Learn more

Updated! (June 26, 2022) The article has been updated to use JEP 424 build 19-ea+25 (2022-09-20).

Panama for newbies Part 3
Panama for newbies Part 3

Introduction

Welcome to the third part of the series on Java's Project Panama for newbies. Before jumping into this article, I want to congratulate you for getting this far, you deserve a virtual pat on the back!

If you are new please check out Part 1 and Part 2. In Part 3, we are going to dig a little deeper in our exploration of Project Panama and how to talk to third party libraries such as SDL & OpenGL. With the skills you've learned from Part 1 and Part 2, you should be able to call most of the common function signatures in many libraries out in the wild.

Listed below is a recap of C concept fundamentals from Part 1 and Part 2:

  • Primitive data types
  • C Strings
  • C Arrays
  • C Structs
  • C Pointers to primitives and structs
  • Array of C structs

To see the source code for all the examples in Part 1 - 3 go to GitHub at: https://github.com/carldea/panama4newbies

Note: To see the prior code examples of this article using JEP 412 go to the branch here.

Do we know enough to call a C function?

Remember in the field of computer science, it depends. You could say it's never enough and on the other hand, you could say it's good enough (at the moment). Patience is key, you can't learn everything right now and what you need to learn will often take time (patience) to explore with any given C library.

Rest assured I believe you know enough to call many 3rd party libraries (maybe?). Assuming the C libraries are mature, the APIs are likely straightforward and decently documented.

Oh, wait there's more to know!

Before diving deeper into calling 3rd party libraries, I want to talk about MethodHandles. In Part 2 we talked about VarHandles to access data stored as ValueLayouts and MemoryLayouts in a low-level way, in the next section we will explore MethodHandles to also access C functions in a low-level way. This is yet another tool to add to your arsenal that will upgrade your abilities to talk to native libraries.

Note: Throughout this article when referencing the word function I'm referring to C functions and when referencing the word method it's a Java class or object's function (member).

What is a MethodHandle?

By now you should be familiar with jextract but have you ever thought about the generated source code it creates? When examining the generated source code you will notice low-level Java (Panama) code using the object MethodHandle to make native invocations.

According to the Javadoc documentation it states the following:

A method handle is a typed, directly executable reference to an underlying method, constructor, field, or similar low-level operation, with optional transformations of arguments or return values. These transformations are quite general, and include such patterns as conversion, insertion, deletion, and substitution.

Javadoc documentation Since JDK 7

A method handle is essentially a Java object that references (java.lang.foreign.Addressable) a native C function, variable, etc. in memory and the ability to invoke or access symbols from Java code. In the C/C++ world symbols are a unique lookup of C functions and variables from the C linker (library in memory).

In C the compiler has an object linker step where symbols are organized in a modular (object files) way before finally creating an executable (or linked library). This concept is very similar to Java's jar files where classes are available on the classpath. In Panama you can access C symbols in two places:

  • Default symbols area - Symbols loaded from the platform OS and Ansi-C functions.
    > Linker.nativeLinker().lookup("some symbol name")
  • Symbol Lookup area - 3rd party libraries added to the java.library.path or dynamically loaded into the SymbolLookup.
    > SymbolLookup.loaderLookup().lookup("some symbol name")

Let's create a MethodHandle to access the system C function getpid() commonly used to obtain a running application's process ID.

Get Pid from a C perspective

Getting the Pid via getpid() is a system level C function to obtain a process id number on Unix/Linux based operating systems.

C Function getpid()

Let's examine the getpid() function's definition as shown below.

#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);

Notice the header sys/types.h and unistd.h which contains the system's data type pid_t and a function getpid(void) respectively. Let's look at what exactly is a pid_t data type and a void parameter.

C's typedef and void parameter

When looking at the function signiture above you're probably wondering what is a pid_t return type? In C (the language) there is a keyword called typedef (Type definition) that lets you create custom data types. For example, you could create a type called human_age_t of primitive data type byte (0-255). In this scenario the user of the API would use the custom type never needing to know the underlying type (byte).

In regards to the type pid_t you can examine closer by looking inside of the C header file sys/types.h. Inside you'll see the typedef pid_t defined as an unsigned C int type. While it's safe to say it is a C int it can vary depending on the platform such as a 16 or 32 bit integer. On Mac/Windows/Linux it is a type C int(32 bit).

Next lets look at what is a void parameter. In the definition you'll notice the getpid(void) function parameter signiture is a type void. This means there are no parameters to pass into the function.

Let's create a MethodHandle

To invoke the function getpid(void) we first need to create a MethodHandle instance via Linker's downcallHandle() method as shown below:

import java.lang.foreign.Linker;

MethodHandle downcallHandle(Addressable symbol, FunctionDescriptor function);

The following are descriptions of each parameter for the downcallHandle() method. Some of the descriptions used are derived from the Javadoc documentation:

  • Addressable symbol - Linker.nativeLinker().defaultLookup() contains all symbols of system library such as functions, variables, structs, etc. Dynamically loaded libraries or 3rd party libraries can be looked up by using the following: SymbolLookup.loaderLookup().lookup("some symbol name")

Note: When initiallizing a C pointer to NULL you can use the MemoryAddress.NULL value.

  • FunctionDescriptor - A function descriptor is made up of zero or more argument layouts and zero or one return layout. A function descriptor is used to model the signature of foreign functions. Unless otherwise specified, passing a null argument, or an array argument containing one or more null elements to a method in this class causes a NullPointerException to be thrown.

    Like we've seen in Part 1 all C datatypes can be of type ValueLayout.OfXxx or other types of MemoryLayout objects (ValueLayout inherits from MemoryLayout). Static instances of C primitive type are already defined as ValueLayouts in the Linker class such as JAVA_INT, JAVA_LONG, ADDRESS etc.

    And when using jextract it will create the following: C_INT, C_LONG, C_POINTER etc. to represent layouts that map specific to the native C primitives. If you aren't using jextract you can use the value layouts mention above. To create a FunctionDescriptor you will call the static of() method as shown below:
import java.lang.foreign.FunctionDescriptor;

public static FunctionDescriptor of(MemoryLayout returnLayout, MemoryLayout... argLayouts) 
public static FunctionDescriptor ofVoid(MemoryLayout... argLayouts) // void return function signature

Now that you know how to lookup symbols and describe method signitures let's invoke the getpid() function using a MethodHandle.

Java calling the C function getpid()

The following code will invoke the native C function getpid() via a MethodHandle and a jextract generated

var cLinker = Linker.nativeLinker();

// Using a MethodHandle
MethodHandle getpidMH = cLinker.downcallHandle(cLinker.defaultLookup().lookup("getpid").get(),
                    FunctionDescriptor.of(JAVA_INT));

int pid	= (int) getpidMH.invokeExact();
System.out.printf("MethodHandle calling getpid() (%d)\n", pid);

// Using Jextract's getpid method.
int jextractPid = foo_h.getpid();
System.out.printf("Jextract's calling getpid()   (%d)\n", jextractPid);

In the above code listing it does the following:

  1. Creates a scope
  2. Creates a MethodHandle
    • Find the symbol getpid (function)
    • Allocates memory reference
    • Describe the function's return type for Java interaction
    • Describe the function's return type (MemoryLayout) for C interaction
  3. Invoke method handle getpidMH, cast to an int.
  4. Output Pid

Outputs the following:

MethodHandle calling getpid() (16514)

Of course if you use the jextract tool the getpid() method would only be a one liner like the following code snippet:

// Using Jextract's getpid method.
int jextractPid = foo_h.getpid();
System.out.printf("Jextract's calling getpid()   (%d)\n", jextractPid);

Now that you know how to call functions in a low-level way using MethodHandles we will be switching gears a bit, by going back to using the jextract tool to generate C functions. This will be neccesary as we approach more complex C functions ahead.

Let's kick it up a notch with little more advanced C function localtime_r() defined in the header file Time.h.

Time.h

Similar to Java's java.util.Date or System.currentTimeMillis(), the local time or some refer to epoch time in milliseconds since 1970. The C function localtime_r(), will be expecting seconds instead of milliseconds.

Shown below is the C function signiture localtime_r() from Time.h.

struct tm *localtime_r( const time_t * epochSeconds, struct tm * tmStruct );

As you can see, the function signature takes a pointer to a time_t and a pointer to a struct tm. It looks very complex to mimic by using a MethodHandle, so let's use jextract!

Using jextract on multiple header files

Before using jextract on the Time.h header file, you might be wondering how to use multiple header files. Since jextract can only work with one header file at a time we encountered compilation issues when generating one header file at a time having the same package namespace.

The architects and engineers on the Panama mailing list suggested a nice workaround. They suggested we create a header file with the multiple header includes. That way jextract can point to a single file as shown below in a file named foo.h:

An header file foo.h that contains multiple header includes as shown below:

#include <stdio.h>
#include <time.h>

Next, you'll run jextract against foo.h to generate source code and/or classes.

Jextract foo.h

The following jextract command line statement will generate source code in the directory generated/src with a package namespace of org.unix with system includes on your preferred platform.

$ jextract --source \
   --output src \
   -t org.unix \
   -I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include \
   foo.h

The next step is the same as before but instead of generating source code jextract will generate class files. You can compile the generated/src directory but the statement below generates files automatically. Notice the absence of the source (--source) option and output destination option set to classes (-d classes).

$ jextract \
   --output classes \
   -t org.unix \
   -I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include \
   foo.h
Generated class files from jextract

Now that you have generated the classes you can use the available convenience methods.

Calling the generated methods

After jextract the classes foo_h, tm and others contain all of the generated objects and methods that represent its C counter parts we can begin allocating objects and invoking native functions. Below is an excerpt of main a few objects and methods we are going to be using to invoke the C localtime_r() function:

/* foo_h.class */

// time_t data type
public static OfLong time_t = Constants$root.C_LONG_LONG$LAYOUT;

// Access to C's localtime_r() function
public static MemoryAddress localtime_r ( Addressable x0,  
                                          Addressable x1 )

/* tm.class */

// tm is a C struct with an allocate method to create space
// by instantiating a MemorySegment (pointer to tm struct)
// struct tm 
public static MemorySegment allocate(SegmentAllocator allocator)

// tm struct (MemoryLayout)
public class tm {
    static final  GroupLayout $struct$LAYOUT = MemoryLayout.structLayout(
        Constants$root.C_INT$LAYOUT.withName("tm_sec"),
        Constants$root.C_INT$LAYOUT.withName("tm_min"),
        Constants$root.C_INT$LAYOUT.withName("tm_hour"),
    // ... the rest

// struct getters
public static int tm_sec$get(MemorySegment seg)
public static int tm_min$get(MemorySegment seg)
public static int tm_hour$get(MemorySegment seg)

// asctime function
public static MemoryAddress asctime ( Addressable x0)

Invoking localtime_r() function

The code listing (PanamaTime.java) below will demonstrate how to call the localtime_r() function by allocating variables (MemorySegment), invoking functions and populate objects(structs). To see the full source code listing visit here.

/* PanamaTime.java */


// The variable now of type MemorySegment holds a C long type. The allocate method will create space.
// time_t * (clong seconds since epoch) probable type is from a typedef long time_t; (64 bit)

// Populate now variable with a Java epoch seconds. C long
var now = memorySession.allocate(C_LONG, System.currentTimeMillis() / 1000);

// Equivalent because time_t is a C_LONG
var now2 = memorySession.allocate(time_t);

// Get contents of now (java epoch seconds)
long secondsSinceEpoch1 = now.get(C_LONG, 0);

// Populate now2 with (C's epoch seconds) and returns as a Java long.
long secondsSinceEpoch2 = time(now2);

// Get contents of now2 (C's epoch seconds) return as a Java long.
long secondsSinceEpoch3 = now2.get(C_LONG, 0);

assert secondsSinceEpoch1 == secondsSinceEpoch2 && secondsSinceEpoch2 == secondsSinceEpoch3;

// Grab epoch time in seconds, then convert to milliseconds.
System.out.printf("1. Java DateTime from C time function: %s\n", new Date(secondsSinceEpoch2 * 1000));

// tm is a C struct with an allocate method to create space
// by instantiating a MemorySegment (pointer to tm struct)
// struct tm *
MemorySegment pTmStruct = tm.allocate(memorySession);

// Calling the C function localtime_r(now, time); now contains seconds, and time is a blank struct.
// [ptr to struct]        [ptr to time_t           ]   [ptr to tm struct     ]
// struct tm *localtime_r( const time_t * epochSeconds, struct tm * tmStruct );
localtime_r(now2, pTmStruct);

// obtaining values based the offset into the tm struct.
var seconds = pTmStruct.get(C_INT, 0);
var minutes = pTmStruct.get(C_INT, 4);
var hours = pTmStruct.get(C_INT, 8);

var cString = memorySession.allocateUtf8String("2. C's printf & tm Struct of local time. %02d:%02d:%02d\n");
printf(cString, hours, minutes, seconds);
fflush(NULL());

// Obtaining values based on the tm struct from jextract
System.out.printf("3. C's tm struct getters tm_hour, tm_min, tm_sec. %02d:%02d:%02d\n",
        tm.tm_hour$get(pTmStruct), tm.tm_min$get(pTmStruct), tm.tm_sec$get(pTmStruct));

// Call time.h asctime() function to display date time.
printf(memorySession.allocateUtf8String("4. C's asctime() function to display date time: %s\n"), asctime(pTmStruct));

How does it work?

The code listing above performs the following steps:

1.) Allocate space for a C_LONG_LONG 64 Bits (time_t),

static final  OfLong C_LONG_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64); // OfLong extends ValueLayout


2.) Invoke time() to populate the MemorySegment object now that will contain the epoch time in seconds
3.) Allocate space and a pointer for a struct tm.
4.) Invoke localtime_r(now, pTmStruct) to populate struct
5.) Use pointer offsets from struct to obtain seconds, minutes, and hours values as MemorySegments.
6.) Output values as references from values
7.) Output using Java's printf() and obtaining seconds, minutes, and hours via method handles
8.) Invoke asctime() function to return a Cstring to be outputted via C's printf()

Running PanamaTime.java

On the command line enter the following to run the PanamaTime.java program.

$ java -cp .:classes \
    --enable-native-access=ALL-UNNAMED \
    --enable-preview --source 19 \
    src/PanamaTime.java

Running PanamaTime.java outputs the following:

1. C's printf & tm Struct of local time. 03:35:36
2. C's tm struct getters tm_hour, tm_min, tm_sec. 03:35:36
3. C's asctime() function to display date time: Mon Aug 30 03:35:36 2021

Now that you have some experience calling system level C functions it's time to look at a third party library. Popular among game developers are SDL and OpenGL API.

A complete program using the SDL (Simple DirectMedia Layer) API

Until now, most of the example in this series have been about single utility function, but we have now all building block to to make use of a complete C API. This section will feature a program written with the SDL2 library. SDL is a cross-platform development library designed to provide low level access to audio, keyboard, mouse, joystick, and graphics hardware via OpenGL.

In this section I'll be relying on the C++ code of this SDL and OpenGL tutorial, this tutorial covers the basics of these APIs, and links to other materials for people thatwould like to explore SDL and OpenGL further.

The first thing needed is to install the libraries, on Linux freeglut3-dev and libdl2-dev, on macOs XCode and SDL2 library. On macOs let's assume SDL is installed with homebrew.

brew install sdl2

This will install the latest version of the library and create the necessary symbolic links in strategic locations. Brew install these in brew --prefix, which on macOs usually resolves to /usr/local (unless it's Apple Silicon, LinuxBrew uses a different folder).

  • /usr/local/lib, symlinks to the actual compiled library objects (/usr/local/lib/libSDL2*)
  • /usr/local/include, symplinks to library header files (/usr/local/include/SDL2)

Once this is done, one should be able to compile a program that uses this library. The CPP example of this tutorial has been placed on the panama4newbies GitHub repository.

Since this program will use a multiple API of the SDL library lets use jextract to generate the Panama mappings. This example will need two headers. At this time, jextract can only accept a single header file, so we'll use a trick by passing our own file containing these two includes.

/* sdlfoo.h */

#include <SDL.h>
#include <SDL_opengl.h>

Then let's run the jextract to generate bindings

jextract --source -source src \
    -t sdl2 \
    -I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include \
    -I /usr/local/include/SDL2 \
    -l SDL2 \
    --header-class-name LibSDL2 \
    sdlfoo.h

Notice the include locations (materialized by the -I option) with the homebrew path mentioned above. Also since this code will load a library, it is necessary to tell what is the name of the compiled library, on Linux (libSDL2.so), on macOs (libSDL2.dylib), In Jaa this library can be looked up by it's name SDL2 (via System.load call), this is the value passed to the -l option. Finally --header-class-name simply tells the name of the generated Java class, otherwise the class will be named after the passed header file.

Note that you'll see some warnings emitted for declarations with types not supported on the JVM, in this case the following methods won't be available because the long double type (12 bytes instead of double's 8 bytes) does not exists on the JVM. This can happen for other special types without equivalents on the JVM. Fortunately this example don't require those special types.

...
WARNING: skipping acosl because of unsupported type usage: long double
WARNING: skipping asinl because of unsupported type usage: long double
WARNING: skipping atanl because of unsupported type usage: long double
WARNING: skipping atan2l because of unsupported type usage: long double
...

Now we can start with a familiar stub the ResourceScope. Then iterate to implement the relevant parts.

import jdk.incubator.foreign.*;

import static sdl2.LibSDL2.*;

public class SDLFoo {
  public static void main(String[] args) {
    try (var memorySession = MemorySession.newConfined()) {
      var sdlFoo = new SDLFoo();

      //// Initialize SDL
      //// Register cleanup actions
      //// Render some OpenGL
    }
  }
}

Let's initialize SDL in an init method.

  private boolean init(MemorySession memorySession) {
    if (SDL_Init(SDL_INIT_VIDEO()) < 0) {
      String errMsg = SDL_GetError().getUtf8String(0);
      System.out.printf("SDL could not initialize! SDL Error: %s\n", errMsg);
      return false;
    }
    else {
      // Starts the intialization sequence of the window

Notice the first 2 lines

  • SDL_Init is a method that accepts a constant, in particular SDL_INIT_VIDEO, jextract expose this constant as a method.
  • If an error happen, the code can check the error via SDL_GetError, which returns a pointer to a regular char array (terminated by a null char), jextract model this pointer as a MemoryAddress (explained in the Part 2 of this series). The pointer can be read as a Java string to be used in the Java world. New in JEP 419 has a getUtf8String() method on a MemoryAddress object to convert a null terminated char array as a Java String.

Now following the original tutorial, this code needs to check if it can open a window then it will try to enable OpenGL on this window and ask for specific parameters. The original tutorial makes the program use OpenGL 2.1, but later version can be used if they are available on the platform.

      SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION(), 2);
      SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION(), 1);

      gWindow = SDL_CreateWindow(memorySession.allocateUtf8String("SDL for Panama"),
                           SDL_WINDOWPOS_UNDEFINED(),
                           SDL_WINDOWPOS_UNDEFINED(),
                           SCREEN_WIDTH,
                           SCREEN_HEIGHT,
                           SDL_WINDOW_OPENGL() | SDL_WINDOW_SHOWN());

      if (Objects.equals(NULL, gWindow)) {
        System.out.printf("Window could not be created! SDL Error: %s\n", SDL_GetError().getUtf8String(0));
        return false;
      } else {
        // Initialize opengl
        gContext = SDL_GL_CreateContext(gWindow);
        if (Objects.equals(NULL, gContext)) {
          System.out.printf("OpenGL context could not be created! SDL Error: %s\n", SDL_GetError().getUtf8String(0));
          return false;
        } else {
          //Use Vsync
          if (SDL_GL_SetSwapInterval(1) < 0) {
            System.out.printf("Warning: Unable to set VSync! SDL Error: %s\n", SDL_GetError().getUtf8String(0));
          }

          //Initialize OpenGL
          if (!initGL()) {
            System.out.println("Unable to initialize OpenGL!\n");
            return false;
          }
        }
      }

      return true;
    }
  }

The above snippet reuses what we learned until now:

  • access to constants SDL_GL_CONTEXT_MAJOR_VERSION(), SDL_WINDOW_OPENGL(), etc.
  • read a native String in Java implicitAllocator().allocateUtf8String()
  • allocate a Java String in a native memory area.
  • receive and pass pointers (MemoryAddress)

Now that the window is ready let's initialize OpenGL itself.

  private boolean initGL() {
    boolean success = true;
    int error = GL_NO_ERROR();

    // Initialize Projection Matrix
    glMatrixMode(GL_PROJECTION());
    glLoadIdentity();

    // Check for error
    error = glGetError();
    if (error != GL_NO_ERROR()) {
      success = false;
    }

    // Initialize Modelview Matrix
    glMatrixMode(GL_MODELVIEW());
    glLoadIdentity();

    // Check for error
    error = glGetError();
    if (error != GL_NO_ERROR()) {
      success = false;
    }

    // Initialize clear color
    glClearColor(0.f, 0.f, 0.f, 1.f);

    // Check for error
    error = glGetError();
    if (error != GL_NO_ERROR()) {
      success = false;
    }

    return success;
  }

Again no surprises, however we no notice in this snippet that the library drives the coding pattern to handle errors. Each native API may use different approach, I advise to follow the regular way to use such API as much as possible.

Then invoke the init method.

    try (var memorySession = MemorySession.newConfined()) {
      var sdlFoo = new SDLFoo();

      //// Initialize SDL
      // Start up SDL and create window
      if (!sdlFoo.init(memorySession)) {
        System.out.println("Failed to initialize!");
        System.exit(1);
      }

      //// Register cleanup actions
      //// Render some OpenGL
    }

Then we need to make sure the code correctly cleans up upon exit. Since ResourceScope is destined to be used in a try-with-resources, it's close method will be invoked to clean allocated resources. Also ResourceScope has a nifty ResourceScope::addCloseAction that can be used to register actions to be performed when this scope closed.

  private void close() {
    SDL_DestroyWindow(gWindow);
    SDL_Quit();
  }
    try (var memorySession = MemorySession.newConfined()) {
      var sdlFoo = new SDLFoo();

      //// Initialize SDL
      // Start up SDL and create window
      if (!sdlFoo.init(memorySession)) {
        System.out.println("Failed to initialize!");
        System.exit(1);
      }

      //// Register cleanup actions
      scope.addCloseAction(sdlFoo::close);

      //// Render some OpenGL
    }

Then we can try to render something on the window like what's on the original tutorial : a quadrilateral surface.

The program can run already but it will close almost immediately, even if the code renders some OpenGL. It's usual to have a loop that wait for some events to happen. With SDL the idea is to wait for events via SDL_PollEvent, also it is needed to tell SDL that keyboard event are allowed.

The SDL_PollEvent accepts a pointer to an SDL_Event, which is an interesting data structure a union. In C a union is a special data type that allows to encode several types in the same memory location. While different members are defined in a union only one member can contain a value at any time.

The following code adds two nested loops, the outer one that will continue as long as the quit boolean is false, the inner one that will handle actual events. In order to receive events and read events the code needs to allocate the necessary space. The maximum size of this union datatype is available via the generated SDL_Event.sizeof(), then we allocate this memory via MemorySegment.allocateNative(SDL_Event.sizeof(), scope). In part 2 you might remember that a MemorySegment implements MemoryAddress, consequently this variable can be used as a parameter of SDL_PollEvent. This is roughly equivalent to

SDL_Event event;
SDL_PollEvent(&event)

Here's the modified code, this code awaits for the user to click the close button (on macOs the red button in the top bar).

    try (var memorySession = MemorySession.newConfined()) {
      var sdlFoo = new SDLFoo();
      if (!sdlFoo.init(memorySession)) {
        System.out.println("Failed to initialize!");
        System.exit(1);
      }
      scope.addCloseAction(sdlFoo::close);

      //// Render some OpenGL
      // Event handling
      // Allocate SDL_Event sdlEvent; which is a union type
      var sdlEvent = memorySession.allocate(SDL_Event.sizeof());

      // Enable text input
      SDL_StartTextInput();

      // While application is running
      boolean quit = false;
      while (!quit) {

        // Handle events on queue
        while (SDL_PollEvent(sdlEvent) != 0) {
          // User clicked the quit button
          if (SDL_Event.type$get(sdlEvent) == SDL_QUIT()) {
            quit = true;
          }
        }

        //// Invoke rendering code
      }

    }

The above code defines a memory zone, the sdlEvent, that is reused for each loop iteration. In C++, one just have to declare SDL_Event e, but with panama it is necessary to reserve the memory for the whole data type. Which is done by this statement allocateNative(SDL_Event.sizeof(), scope), it can be even simplified to SDL_Event.allocate(scope) or a an overload of this method using a SegmentAllocator.

The SDL_Event is a union data type, it is defined in a way such as the field member type is always present and can be used to identify the kind of event (and the actual data structure of this even). The code checks the type via the generated method SDL_Event.type$get(MemorySegment event). However there's some differences in how union types are accessed in C and Panama. In this tutorial, the code needs to read the SDL_TextInputEvent, in C this would be written like this

if(event.type == SDL_TEXTINPUT) {
  char c = event.text.text[0];
}

The code generated by jextract has the name slice: Typically this method SDL_Event.text$slice(sdlEvent) is somewhat equivalent to event.text, and simply restrict the range of the segment to the size of a SDL_TextInputEvent. Then from this reduced slice, it's possible to access SDL_TextInputEvent's members, in particular the text member (which happens to be a C string).

if (SDL_Event.type$get(sdlEvent) == SDL_TEXTINPUT()) {
  var textSeg = SDL_TextInputEvent.text$slice(SDL_Event.text$slice(sdlEvent));
  char c = textSeg.getUtf8String(0).charAt(0);
  if (c == 'q') {
    quit = true;
  }
}

This is not quite as readable as the C code, but since it is a union datatype it's possible to directly use SDL_TextInputEvent.text$slice to access the C string from the SDL_Event segment.

if (SDL_Event.type$get(sdlEvent) == SDL_TEXTINPUT()) {
  var textSeg = SDL_TextInputEvent.text$slice(sdlEvent);
  char c = textSeg.getUtf8String(0).charAt(0);
  if (c == 'q') {
    quit = true;
  }
}

Then the main method can finally perform the OpenGL rendering:

    try (var memorySession = MemorySession.newConfined()) {
      var sdlFoo = new SDLFoo();

      // Start up SDL and create window
      if (!sdlFoo.init(memorySession)) {
        System.out.println("Failed to initialize!");
        System.exit(1);
      }
      scope.addCloseAction(sdlFoo::close);

      // Event handling
      // Allocate SDL_Event sdlEvent; which is a union type
      var sdlEvent = memorySession.allocate(SDL_Event.sizeof());

      // Enable text input
      SDL_StartTextInput();

      // While application is running
      boolean quit = false;
      while (!quit) {
        // Handle events on queue
        while (SDL_PollEvent(sdlEvent) != 0) {
          // User clicked the quit button
          if (SDL_Event.type$get(sdlEvent) == SDL_QUIT()) {
            quit = true;
          }
          // Handle keypress with current mouse position
          else if (SDL_Event.type$get(sdlEvent) == SDL_TEXTINPUT()) {
            var textSeg = SDL_TextInputEvent.text$slice(sdlEvent);
            char c = textSeg.getUtf8String(0).charAt(0);
            if (c == 'q') {
              quit = true;
            }
          }
        }

        //// Invoke OpenGL rendering code
        sdlFoo.render(memorySession);
        sdlFoo.update(memorySession);
      }

      //Disable text input
      SDL_StopTextInput();
    }

The code of the render method is really simple for the purpose of this example, basically it clears the screen with some color, then the quadrilateral shape, with some rotations.

  private void render(MemorySession memorySession) {
    //Clear color buffer
    glClear(GL_COLOR_BUFFER_BIT());

    // Rotate The cube around the Y axis
    glRotatef(0.4f,0.0f,1.0f,0.0f);
    glRotatef(0.2f,1.0f,1.0f,1.0f);
    glColor3f(0.0f,1.0f,0.0f);

    glBegin( GL_QUADS() );
      glVertex2f( -0.5f, -0.5f );
      glVertex2f( 0.5f, -0.5f );
      glVertex2f( 0.5f, 0.5f );
      glVertex2f( -0.5f, 0.5f );
    glEnd();
  }

This code is not quite fancy, in order to go in to more OpenGL details go to other tutorials like this one. The last thing to do is to call the SDL_GL_SwapWindow in order to tell SDL that the OpenGL rendering is done.

  private void update(MemorySession memorySession) {
    // Update a window with OpenGL rendering
    SDL_GL_SwapWindow(gWindow);
  }

And we're done, now to run this code and see it in action we need the usual options add incubating module, but we also need to tell the JDK where to look for the SDL2 library. Indeed the default library lookup location does not include additional path like /usr/local/lib. It's possible to see the lookup location with the java.library.path system property.

/Users/brice/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.

It's possible to make the JDK look for additional location via the JAVA_LIBRARY_PATH environment variable. As we need to add the location described above : JAVA_LIBRARY_PATH=:/usr/local/lib

Also on macOs, and only for a graphical applications, it is also required to have this option -XstartOnFirstThread. The command line should look like :

env JAVA_LIBRARY_PATH=:/usr/local/lib java \
  -cp .:classes \
  -XstartOnFirstThread \
  --enable-native-access=ALL-UNNAMED \
  --enable-preview \
  --source 19 \
  src/SDLFoo.java

This should display a window like this:

This example shows that porting a complete C or C++ program to Java using Panama is possible. However, this requires a sizeable effort to understand the API of a native library, in particular what comes to mind in this regard is pointer or reference passing.

Moreover, thinking about an allocation strategy will come to mind, since the above program uses a naive approach by using MemorySegment.allocateNative, while for performance reasons it might be worth it to consider SegmentAllocator and its different strategies.

Conclusion

In part 3, we looked down at what jextract actually does to generate bindings, in particular how CLinker uses MethodHandles to perform calls to native functions. The time example makes use of a struct datatype whose reference, ie the memory address is passed to the native function. This last example mark a stepping stone in that a Java program has all building blocks to use a complete native API set. That's what has been done with the SDL / OpenGL example, it uses all items learned in this series.

The new API offered as part of the JEP 419 effort is yet another refinement toward JNI succession.

The last installment (Part 4), is about callbacks where native C functions can call Java methods.

Topics:

Related Articles

View All

Author(s)

  • Avatar photo
    Brice Dutheil

    Brice Dutheil is a software engineer, aficionado of open source software. Previously a Mockito core contributor, now a Dad of 2.

  • Carl Dea

    Carl Dea is a Senior Developer Advocate at Azul. He has authored Java books and has been developing software for 20+ years with many clients, from Fortune 500 companies to ... Learn more

Comments (0)

Your email address will not be published. Required fields are marked *

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Save my name, email, and website in this browser for the next time I comment.

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard