Koduj w Javie, uruchamiaj w C++ dzięki GraalVM
Java i C ++ pozostają jednymi z najbardziej popularnych języków programowania. Oba są inaczej zaprojektowane i różnią się charakterystyką oraz, w zależności od problemu, jeden może działać lepiej od drugiego. Trzeba je jednak w pewnym momencie zintegrować. Dotyczy to, np. wywołania metody Javy w C++. Konieczność integracji tych dwóch języków nie jest jednak niczym nowym, czego przykładem jest ten artykuł z 1996 roku. Od tamtego czasu oba te języki niezwykle się rozwinęły.
Tutaj nauczymy się, jak wywoływać metodę Javy z C++ przy użyciu natywnego obrazu GraalVM. Jeśli po raz pierwszy usłyszałeś o GraalVM, to być może zainteresuje Cię strona internetowa tego projektu. Jest to maszyna wirtualna, której można używać do uruchamiania aplikacji Java. Opiera się ona na JVM, ale jest rozszerzona o dodatkowe funkcje, np. o kompilację z wyprzedzeniem, zmniejszone zapotrzebowanie na pamięć i inne optymalizacje.
Co więcej, GraalVM zapewnia wtyczkę do natywnych obrazów, której można użyć do skompilowania aplikacji Java do aplikacji natywnej. Oznacza to, że aplikacja może działać bez konieczności instalowania i uruchamiania maszyny JVM. Co ciekawe, możesz także użyć tej wtyczki, aby skompilować aplikację Java jako współdzieloną bibliotekę i załadować ją z kodu C++. Przydaje się to gdy piszesz głównie w C++, ale chcesz użyć istniejącej biblioteki Java na jakiejś jej części.
Jak to działa?
Zanim przejdziemy do konkretów, zobaczmy, jak to w ogóle działa. Właściwie to będziemy robić następujące rzeczy:
- Napiszemy metodę w Javie.
- Skompilujemy kod Javy do współdzielonej biblioteki C++ (
yourlib.so
) i nagłówka (yourlib.h
) przy użyciu GraalVM. - Załadujemy bibliotekę i nagłówek do projektu C++.
W przykładzie tym zasymulujemy sytuację, w której chcesz użyć funkcji zewnętrznej biblioteki Java w projekcie C++. Jesteśmy szczególnie zainteresowani użyciem jednej z funkcji matematycznych z biblioteki Google Guava.
Przygotowywanie projektu w Javie
Najpierw musimy skonfigurować projekt maven dla naszej javowej metody. Budowanie naszego projektu w maven pomaga nam zaimportować zależności, a także wykorzystać wtyczkę GraalVM do stworzenia biblioteki współdzielonej. Po utworzeniu projektu dodaj zależności do biblioteki Guava (lub innej biblioteki, której potrzebujesz) i bibliotekę GraalVM do pliku pom.xml
:
<dependencies>
<!-- https://mvnrepository.com/artifact/org.graalvm.nativeimage/svm -->
<dependency>
<groupId>org.graalvm.nativeimage</groupId>
<artifactId>svm</artifactId>
<version>19.3.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
</dependencies>
Potem dodajemy następującą wtyczkę, aby zbudować kod Java jako bibliotekę C++:
<build>
<finalName>libmymath</finalName>
<plugins>
<plugin>
<groupId>com.oracle.substratevm</groupId>
<artifactId>native-image-maven-plugin</artifactId>
<version>20.0.0</version>
<executions>
<execution>
<goals>
<goal>native-image</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<buildArgs>--shared -H:Name=libmymath</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
Spójrzmy na <finalName>libmymath</finalName>
, <version>20.0.0</version>
oraz <buildArgs>--shared -H:Name=libmymath</buildArgs>
. W tagu <finalName>
wpisz nazwę swojej biblioteki. Zwróć uwagę na wersję podaną w znaczniku <version>
, ponieważ będzie ona potrzebna później. W <buildArgs>
określamy parametr shared oraz name, aby poinstruować GraalVM, do stworzenia biblioteki zamiast pliku wykonywalnego. Wpisz taką samą nazwę jak finalName
w parametrze Name
.
Teraz przygotujmy naszą metodę w Javie. Przyjmując na wejście liczbę całkowitą x
, jesteśmy zainteresowani obliczeniem najbliższej potęgi o wartości o 2 większej od x
. Na przykład, jeśli x wynosi 14, to zwróci 16, ponieważ 16 jest najbliższą potęgą o 2 większą niż 14. Poniżej znajduje się fragment naszej funkcji. Zwróć uwagę, że musimy umieścić dodatkowy parametr, który jest wątkiem typu IsolateThread
, aby połączyć ze sobą wykonanie kodu w Java i C++. Parametr ten musi znajdować się na pierwszej pozycji. Co więcej, trzeba określić nazwę metody w adnotacji CEntryPoint
. Będzie to nazwa metody, do której odwołujemy się w naszym kodzie C++. Możesz sprawdzić kompletny projekt Java w tym repozytorium.
import com.google.common.math.IntMath;
import org.graalvm.nativeimage.IsolateThread;
import org.graalvm.nativeimage.c.function.CEntryPoint;
public class MyMath {
@CEntryPoint (name = "ceilingPowerOfTwo")
public static int ceilingPowerOfTwo(IsolateThread thread, int x) {
return IntMath.ceilingPowerOfTwo(x);
}
}
Przygotowywanie GraalVM
Po przygotowaniu naszego projektu musimy uruchomić mvn package
, aby skompilować naszą metodę Java do biblioteki współdzielonej. Ponieważ funkcja ta jest dostępna tylko w GraalVM, będziemy musieli skonfigurować GraalVM i skierować nasz JAVA_HOME
na binarkę GraalVM. To dlatego trzeba wykonać następujące czynności:
- Pobierz GraalVM. Upewnij się, że masz właściwą architekturę, wersję Javy oraz wersję GraalVM podaną w pliku
pom.xml
. W tym przykładzie użyjemy GraalVM 20.0.0 dla Java 8. - Zainstaluj rozszerzenie native-image za pomocą pliku binarnego gu. W środowisku uniksowym możesz to zrobić za pomocą:
<path_to_graalvm_directory>graalvm-ce-java8–20.0.0 2bin/gu install native-image
. - Zaktualizuj
JAVA_HOME
, aby wskazywał pobrany katalog GraalVM. W środowisku uniksowym możesz użyć eksportuJAVA_HOME=<path_to_graalvm_directory/graalvm-ce-java8–20.0.0 2/>
.
Kompilacja biblioteki współdzielonej
Możesz teraz wywołać instalację mvn package
, aby zbudować bibliotekę. Wygeneruje to kilka plików, a mianowicie: graal_isolate.h
, graal_isolate_dynamic.h, libmymath.h
, libmymath_dynamic.h
i libmymath.so
(lub w innych formatach w innym systemie operacyjnym). Poza naszą własną biblioteką wygenerowana zostanie również biblioteka Graal VM C++.
Importowanie biblioteki do kodu C++
Skorzystajmy teraz z naszej biblioteki w prostym projekcie C++. Umieśćmy wygenerowany plik w docelowym katalogu tego projektu C++. Możesz na przykład utworzyć folder include i umieścić w nim swoje pliki.
#include <iostream>
#include <libmymath.h>
int main() {
graal_isolate_t *isolate = NULL;
graal_isolatethread_t *thread = NULL;
if (graal_create_isolate(NULL, &isolate, &thread) != 0) {
fprintf(stderr, "initialization error\n");
return 1;
}
printf("Result> %d\n",ceilingPowerOfTwo(thread, 14));
return 0;
}
Skompiluj kod C++ za pomocą następującego polecenia i spróbuj go uruchomić.
g++ ceilingPowerOfTwoCpp.cpp -L includes/ -I includes/ -lmymath -o ceilingPowerOfTwoCpp
Uwaga: może być konieczne ustawienie katalogu includes
na LD_LIBRARY_PATH
przy użyciu:
export LD_LIBRARY_PATH=<path_to_includes_directory>
Podsumowanie
Interoperacyjność jest nieuniknionym wymogiem w świecie, gdzie języki programowania rozwijają się równocześnie. Funkcja współdzielonej biblioteki GraalVM Native Image zapewnia łatwy sposób osadzenia metody Javy w programie C++. Umożliwia to pisanie złożonych funkcji zdefiniowanych przez użytkownika oraz korzystanie z zewnętrznych bibliotek. Dobrym przykładem może być opracowanie frameworku do przetwarzania danych, który zapewnia API, które jest zarówno wysokopoziomowe, jak i wysoce wydajne.
Jeśli chcesz dowiedzieć się więcej o GraalVM, zachęcam do odwiedzenia strony projektu. Pełny kod z tego artykułu jest dostępny tutaj.
Oryginał tekstu w języku angielskim przeczytasz tutaj.