DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Floyd's Cycle Algorithm for Fraud Detection in Java Systems
  • Generics in Java and Their Implementation
  • The Two-Pointers Technique
  • Speeding Up Large Collections Processing in Java

Trending

  • It’s Not About Control — It’s About Collaboration Between Architecture and Security
  • Mastering Fluent Bit: Installing and Configuring Fluent Bit on Kubernetes (Part 3)
  • Why High-Performance AI/ML Is Essential in Modern Cybersecurity
  • Unlocking the Benefits of a Private API in AWS API Gateway
  1. DZone
  2. Data Engineering
  3. Data
  4. Pushing the JNI Boundaries: Java Meets Assembly

Pushing the JNI Boundaries: Java Meets Assembly

This lesson in assembly and Java will teach you how to use the Java Native Interface to work directly with an assembler.

By 
Pierre-Yves Bigourdan user avatar
Pierre-Yves Bigourdan
·
May. 31, 18 · Tutorial
Likes (26)
Comment
Save
Tweet
Share
21.6K Views

Join the DZone community and get the full member experience.

Join For Free

The Java Native Interface (JNI) is used by developers to call native code compiled from other languages. Most online resources, including the Javadocs, showcase examples based on either C or C++. Nevertheless, is it possible to go a level lower and call code compiled from assembly without any intermediary C or C++ layer?

This article will walk you through a simple example. No prior knowledge of the JNI is necessary and only basic understanding of assembly fundamentals will be enough for you to survive this journey. The original source code is available on GitHub alongside a minimal build script.

Example Outline

Consider the following Java class:

public class JNIArraySum {
    public static native long computeNativeArraySum(int[] array, int arrayLength);
}


The objective is to write an assembly implementation of this native method declaration that sums all elements within an integer array and returns the result as a long. An equivalent plain Java implementation would resemble the following:

public long computeArraySum(int[] array) {
    long sum = 0L;
    for (int value : array) sum += value;
    return sum;
}


Naming Conventions

When looking for a method within a native library, the JVM follows a well-defined naming convention, described in the documentation. In our case, we need to concatenate the prefix Java, the class name, and the method name, all separated by underscores: Java_JNIArraySum_computeNativeArraySum.

Let's slowly warm up and start writing a bit of assembly! This mangled method name has to be visible from other translation units, which can be achieved by declaring a symbol as follows:

global Java_JNIArraySum_computeNativeArraySum


The above uses NASM/Yasm syntax, but other assemblers may rely on different keywords, for instance public in FASM. On macOS, prepend an additional underscore to match the operating system's calling convention:

global _Java_JNIArraySum_computeNativeArraySum


Arguments Passed by Java

The JVM passes several arguments to the native function when called:

  • A pointer to JNIEnv, which we will come back to later.
  • A reference to the calling Java class or object.
  • The parameters defined in the Java method's declaration (int[] array and int arrayLength).

Depending on the targeted hardware architecture, these arguments can be held in registers, on the stack or even in other memory structures. In the x84-64 world, the first argument (JNIEnv) is put in the rdi register, the second one (calling the Java object) is put in rsi, the third one (int[] array) is put in rdx, and finally, the fourth one (int arrayLength) is put in rcx.

With this in mind, we can start writing the Java_JNIArraySum_computeNativeArraySum function:

Java_JNIArraySum_computeNativeArraySum:

  push rdx  ; save Java array pointer for later use
  push rdi  ; save JNIEnv pointer for later use
  push rcx  ; save array length for later use


Nothing fancy happening here: Some of the parameters are simply pushed onto the stack for later use. Don't forget to prefix the function name with an extra underscore if you're on macOS.

Data Type Mappings

Java data types need to be mapped to native data types as these may not have the same representation. The JNI provides a number of different functions to do so, all listed in the docs. The example at hand operates on a Java integer array that can be mapped to a native one using the GetIntArrayElements function. Once all necessary computations are performed on the native array, the JVM must be informed that those resources are no longer needed. This can be done with the ReleaseIntArrayElements function, which will free any memory buffers that were used for the native data type mapping.

These JNI functions seem promising, but how does one call them from assembly?

Calling JNI Functions

Remember the JNIEnv pointer passed in the rdi register? This pointer is itself a pointer to the JNI function table, which contains pointers to the individual interface functions. Lost with all these pointers? Things can be summed up thanks to this diagram:

JNIEnv pointers. Source: docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html

Let's follow the first arrow using assembly and store the table pointer in the rax register:

mov rax, [rdi]  ; get location of JNI function table


The index of each JNI function can be found in the documentation. To map an index to an actual position in memory, multiplying the index by the size of a pointer in the targeted architecture and adding the resulting offset to the address of the table will do the trick. In x84-64, each pointer is 8 bytes (64 bits). Therefore, to call GetIntArrayElements with index 187:

mov rsi, rdx  ; set array parameter for GetIntArrayElements
call [rax + 187 * 8]


The first instruction copies the Java integer array pointer to the register used to store the first argument of function calls, rsi. The second instruction maps this Java integer array to a native array and puts the resulting memory location in the rax register.

Summing Elements

We now have all the data structures required to compute the sum. The following code snippet loops through the array using rax as the current address, rcx as the upper bound and rbx as the accumulator for the sum:

pop rcx                   ; retrieve array length
lea rcx, [rax + 4 * rcx]  ; compute loop end address (after last array element)
mov r8, rax               ; copy native array pointer for later use
xor rbx, rbx              ; initialise sum accumulator
add_element:
  movsx r9, dword [rax]   ; get current element
  add rbx, r9             ; add to sum
  add rax, 4              ; move array pointer to next element
  cmp rax, rcx            ; has all array been processed?
  jne add_element


Note that the number 4 above corresponds to the number of bytes per integer.

The native array can now be released by calling the ReleaseIntArrayElements JNI function with index 195 :

pop rdi         ; retrieve JNIEnv
pop rsi         ; retrieve Java array pointer
push rbx        ; store sum result
mov rax, [rdi]  ; get location of JNI function table
mov rdx, r8     ; set elems parameter for ReleaseIntArrayElements
call [rax + 195 * 8]


To finish the computation, in addition to the final ret instruction, the result must be stored in rax:

pop rax  ; retrieve sum result
ret


Compiling and Running

We're done with all the assembly code! Feel free to pull down the full source if you've missed something. To compile it using NASM and targeting a Linux x86-64 system, a command similar to the following can be run:

nasm -f elf64 -o ArraySum.o ArraySum.asm


To compile with Yasm, invoke yasm instead of nasm. On macOS, replace elf64 by macho64.

The produced object files must then be linked, for instance using good old GCC or Clang:

gcc -shared -z noexecstack -o libArraySum.so ArraySum.o
clang -shared -o libArraySum.so ArraySum.o


Let's add a bit of sugar to the initial Java code in order to wire things together and print the result:

import java.io.File;

public class JNIArraySum {

    private static final int[] ARRAY_TO_SUM = { 2, 41, 92, 9, 52, 27, 20, 0, 22, 35, 3, 57, 33, 4, 40, 44, 59, 31, 71, 5 };

    public static void main(String[] args) {
        File file = new File("libArraySum.so");
        System.load(file.getAbsolutePath());

        long sum = computeNativeArraySum(ARRAY_TO_SUM, ARRAY_TO_SUM.length);
        System.out.println("The result of the sum is: " + sum); // expected result: 647
    }

    public static native long computeNativeArraySum(int[] array, int arrayLength);
}


Finally to compile the Java code and run it:

javac JNIArraySum.java
java JNIArraySum


Hurray, the program prints the expected result when run!

Image title

Conclusive Notes

Hopefully, you will now be able to write Java code that can interoperate with low-level assembly. Obviously, not many people in their right mind would want to sum an array this way, especially without any vectorization, but beyond its educational value, this example can be a basis for further experimentation! These techniques can also easily be extended to match different assembler or operating system requirements. Long live assembly and Java!

Java Native Interface Java (programming language) Assembly (CLI) Data Types operating system Pointer (computer programming) Data structure

Opinions expressed by DZone contributors are their own.

Related

  • Floyd's Cycle Algorithm for Fraud Detection in Java Systems
  • Generics in Java and Their Implementation
  • The Two-Pointers Technique
  • Speeding Up Large Collections Processing in Java

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!