To try out the Java 25 FFM API, I wanted to call C++ code through a C wrapper.
To do that, I created a small C++ class and a C wrapper following an example from here.
Printer.h
#pragma once
#include <iostream>
class Printer
{
public:
void print(const std::string& message);
};
Printer.cpp
#include "Printer.h"
void Printer::print(const std::string& message)
{
std::cout << message << "\n";
}
PrinterWrapper.h
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef struct Printer Printer;
__declspec(dllexport)
Printer* newPrinter();
__declspec(dllexport)
void Printer_print(Printer*, const char []);
__declspec(dllexport)
void deletePrinter(Printer*);
#ifdef __cplusplus
}
#endif
PrinterWrapper.cpp
#include "Printer.h"
#include "PrinterWrapper.h"
extern "C" {
Printer* newPrinter()
{
return new Printer();
}
void Printer_print(Printer* printer, const char message[])
{
printer->print(message);
}
void deletePrinter(Printer* printer)
{
delete printer;
}
}
Printer.java
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
public class Printer {
private static final MethodHandle NEW_PRINTER;
private static final MethodHandle PRINTER_PRINT;
private static final MethodHandle DELETE_PRINTER;
private MemorySegment printerPointer;
static {
System.load("Path/to/shared/library");
Linker linker = Linker.nativeLinker();
SymbolLookup lookup = SymbolLookup.loaderLookup();
NEW_PRINTER = linker.downcallHandle(lookup.findOrThrow("newPrinter"),
FunctionDescriptor.of(ValueLayout.ADDRESS));
PRINTER_PRINT = linker.downcallHandle(lookup.findOrThrow("Printer_print"),
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, ValueLayout.ADDRESS));
DELETE_PRINTER = linker.downcallHandle(lookup.findOrThrow("deletePrinter"),
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS));
}
public Printer() {
try {
printerPointer = (MemorySegment) NEW_PRINTER.invoke();
} catch (Throwable e) {
e.printStackTrace();
}
}
public void print(String message) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment nativeString = arena.allocateFrom(message);
PRINTER_PRINT.invokeExact(printerPointer, nativeString);
} catch (Throwable e) {
e.printStackTrace();
}
}
public void delete() {
try {
DELETE_PRINTER.invokeExact(printerPointer);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
Main.java
public class Main {
public static void main(String[] args) {
Printer printer = new Printer();
printer.print("Hello World!");
printer.delete();
}
}
If I run the program, everything works fine. But, here is the unexpected behavior: If I change the variable printerPointer to MemorySegment.NULL or any zero-length memory segment (see zero-length memory segments in the java documentation) of any address, e.g. MemorySegment.ofAddress(73928298), the program still executes correctly.
First change:
public Printer() {
printerPointer = MemorySegment.NULL;
}
Second change:
public Printer() {
printerPointer = MemorySegment.ofAddress(73928298);
}
Both times, Hello World! is printed on the console. But I don't know why it is working both times, as my expectation was that it does not. Is there something wrong with the wrapper? (As I learned, calling C++ code produces undefined behavior). Or, is this intended, for whatever reason? Or, is it just a bug in the API?
The changed code invokes undefined behaviour.
Printer::print is effectively a class ("static") method rather than an instance method. The caller's pointer is passed in to it, but it's never used.
Printer::print doesn't use this (which has the value of the caller's pointer), either implicitly or explicitly. It doesn't access any data members of the object, and it doesn't call any virtual methods on the object. All it needs to know is the type of the variable printer, and that is known at compile-time.
That's why you get away with calling Printer::print on an invalid pointer. But it is undefined behaviour to do this. You can't rely on always getting this behaviour.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With