Crea tu propia librería nativa pra Java con Rust y J4RS

Published on Jun 20, 2025 | Categories: lowlevel development tutorial | Tags: native java cerberus

Descubre cómo integrar la potencia y seguridad de Rust directamente en tus proyectos Java. En esta publicación, te guiaremos paso a paso en la creación de una biblioteca nativa, utilizando el framework J4RS para una interoperabilidad sencilla y eficiente. Aprende a aprovechar lo mejor de ambos mundos para optimizar el rendimiento y la fiabilidad de tus aplicaciones.

¡Hola Java!

El ecosistema Java prospera gracias a su promesa de "escribe una vez, ejecuta en cualquier lugar", una hazaña lograda en gran medida por la Máquina Virtual de Java (JVM). Esta potente máquina de computación abstracta actúa como un intermediario crucial, transformando el código de bytes Java compilado en instrucciones que el sistema operativo de tu computadora puede entender. Cuando ejecutas una aplicación Java, la JVM carga dinámicamente las clases y bibliotecas necesarias, interpretándolas o compilándolas justo a tiempo (JIT) en código máquina nativo para su ejecución. Este proceso de carga dinámica es increíblemente flexible, permitiendo que las aplicaciones accedan a una vasta gama de funcionalidades proporcionadas por la Biblioteca de Clases de Java y las dependencias de terceros.

Sin embargo, hay escenarios en los que incluso la JVM altamente optimizada podría no ser suficiente. Para tareas que exigen un rendimiento máximo absoluto, acceso de bajo nivel al sistema o interacción directa con el hardware, entran en juego las bibliotecas nativas. Estas bibliotecas, típicamente escritas en lenguajes como C, C++ o, en nuestro caso, Rust, se compilan directamente en código máquina para un sistema operativo y arquitectura específicos. Cuando una aplicación Java necesita utilizar una de estas bibliotecas, se basa en mecanismos como la Interfaz Nativa de Java (JNI) para tender un puente. Aunque JNI ofrece una inmensa potencia, su complejidad puede ser un obstáculo. Aquí es donde frameworks como J4RS (Java para Rust) simplifican el proceso, proporcionando una forma más intuitiva e idiomática de Rust para crear e interactuar con bibliotecas nativas. En este artículo, exploraremos cómo aprovechar la velocidad y la seguridad de Rust para extender tus aplicaciones Java, construyendo bibliotecas nativas que se integren perfectamente con tu código base existente, gracias a la elegancia de J4RS.

¿Qué es Rust?

Rust es un lenguaje de programación de sistemas que enfatiza la seguridad, la velocidad y la concurrencia. Lanzado por primera vez en 2015, ha ganado rápidamente terreno y consistentemente encabeza las encuestas de desarrolladores como uno de los lenguajes de programación "más queridos". Su sintaxis resultará familiar a los desarrolladores de C/C++, pero introduce conceptos innovadores que cambian fundamentalmente la forma de abordar la programación de bajo nivel.

El desafío de la interoperabilidad: Java Native Interface (JNI)

Hemos hablado del "porqué"—por qué querrías código nativo junto a tu Java—pero ahora vamos a sumergirnos en el "cómo". El mecanismo estándar proporcionado por el Kit de Desarrollo de Java (JDK) para que Java interactúe con código escrito en otros lenguajes (como C, C++ y, efectivamente, Rust) es la Java Native Interface (JNI). Piensa en JNI como un sofisticado traductor y puente, que permite a tu aplicación Java, ejecutándose dentro de la JVM, invocar funciones en una biblioteca nativa, y viceversa.

Cómo funciona JNI (La forma tradicional)

Para entender J4RS, es útil comprender primero los fundamentos de JNI "puro":

  1. Declaración de métodos nativos en Java: Comienzas declarando un método en tu clase Java con la palabra clave native , pero sin una implementación. Esto le dice a la JVM que el código real del método reside en una biblioteca nativa. También usarás típicamente System.loadLibrary("your_native_library_name"); para cargar el código nativo compilado en tiempo de ejecución.
        
    public class MyJavaApp {
        static {
            System.loadLibrary("my_rust_library"); // Carga libmy_rust_library.so/.dll/.dylib
        }
    
        public native String greetFromNative(String name); // Declaración de método nativo
    
        public static void main(String[] args) {
            MyJavaApp app = new MyJavaApp();
            System.out.println(app.greetFromNative("World"));
        }
    }
        
        
  2. Generación de archivos de cabecera C/C++:: Históricamente, después de compilar tu clase Java, usarías la herramienta javah (ahora a menudo integrada en compiladores como javac o IDEs) para generar un archivo de cabecera C/C++ (.h). Este archivo de cabecera contendría las firmas de función exactas a las que tu implementación nativa necesita adherirse, asegurando la compatibilidad con el método Java.
  3. Implementación de métodos nativos: Luego escribes la implementación real de estas funciones en C o C++. Estas implementaciones reciben tipos JNI especiales (como JNIEnv*, jobject para los objetos java o jstring para las cadenas de texto de java)
    
    // Ejemplo de una función C generada por JNI para el método Java anterior
    JNIEXPORT jstring JNICALL Java_MyJavaApp_greetFromNative
      (JNIEnv *env, jobject obj, jstring javaName) {
        // Obtener cadena C de cadena Java
        const char *cName = (*env)->GetStringUTFChars(env, javaName, 0);
        // ... hacer algo con cName ...
        char result[256];
        sprintf(result, "Hello %s from C!", cName);
        (*env)->ReleaseStringUTFChars(env, javaName, cName); // Liberar memoria
    
        return (*env)->NewStringUTF(env, result); // Devolver una nueva cadena Java
    }
    
    
  4. Compilación en una biblioteca compartida: Finalmente, el código C/C++ se compila en una biblioteca compartida específica de la plataforma (por ejemplo, .so en Linux, .dll en Windows, .dylib en macOS). Esta biblioteca compilada es lo que tu aplicación Java carga en tiempo de ejecución.

Las complejidades y trampas del JNI puro

Si bien JNI proporciona el puente necesario, trabajar directamente con él puede ser complicado y propenso a errores, especialmente para desarrolladores que no están profundamente familiarizados con la gestión de memoria en C/C++ y las llamadas al sistema de bajo nivel.

  • Código repetitivo (Boilerplate): Incluso para tareas simples, JNI requiere una cantidad significativa de código repetitivo. Convertir entre tipos de Java y nativos (por ejemplo, de String a char* y viceversa, o de int[] a jintArray y luego a un arreglo de C) implica numerosas llamadas a funciones JNI, asignación y liberación manual de memoria. Esta verbosidad puede hacer que tu código se vuelva difícil de leer y mantener rápidamente.
  • Propenso a errores e inseguro: La mayor desventaja del JNI puro es su inseguridad inherente. Estás operando a un nivel bajo, con acceso directo a la memoria.
    • Gestión manual de memoria: A diferencia del recolector de basura de Java, eres responsable de gestionar explícitamente la memoria asignada en el código nativo. Olvidar liberar recursos (como cadenas obtenidas con GetStringUTFChars) provoca fugas de memoria.
    • Punteros directos: Los errores con punteros pueden causar fallos de segmentación o crashes de la JVM, derribando toda tu aplicación. Depurar estos fallos, que se originan en el código nativo pero se manifiestan en la JVM, puede ser extremadamente difícil.
    • Seguridad en hilos (Thread safety): Manejar la seguridad en hilos al cruzar el límite de JNI requiere una sincronización cuidadosa, lo cual es fácil de hacer mal.
  • Marshalling de tipos tedioso: Traducir tipos de datos complejos entre el sistema de tipos de Java y los tipos de C/C++ es tedioso. Objetos, arreglos e incluso colecciones requieren múltiples llamadas JNI para acceder a sus campos, métodos o elementos, lo que lleva a código verboso y propenso a errores.
  • Curva de aprendizaje pronunciada: JNI tiene su propia API extensa, reglas y buenas prácticas. Dominarlo toma un tiempo y esfuerzo considerables, a menudo requiriendo profundizar en detalles de C/C++ a los que los desarrolladores de Java podrían no estar acostumbrados.
  • Depuración desafiante: Cuando surge un problema que abarca tanto el código Java como el nativo, la depuración se vuelve significativamente más compleja. A menudo necesitas usar depuradores nativos específicos de la plataforma (como GDB o LLDB) junto con depuradores de Java, haciendo que el proceso de depuración sea engorroso.

Dadas estas complejidades, el uso directo de JNI suele reservarse para escenarios donde se requiere un control extremo o una integración específica con una biblioteca nativa, y el equipo de desarrollo tiene experiencia en C/C++. Para muchos otros casos, se desea una abstracción de nivel más alto. Precisamente aquí es donde J4RS (Java For Rust) entra en juego, con el objetivo de simplificar esta compleja interacción entre Java y Rust.

Simplificando la Interoperabilidad con J4RS (Java Para Rust)

Como hemos visto, si bien JNI proporciona el puente fundamental entre Java y el código nativo, su naturaleza de bajo nivel puede introducir una complejidad significativa, código repetitivo y potencial de errores. Aquí es donde entran en juego frameworks como J4RS (Java Para Rust). J4RS es una solución potente y elegante diseñada para simplificar drásticamente el proceso de llamar código Rust desde Java, e incluso código Java desde Rust, abstrae los detalles intrincados de JNI.

¿Por qué J4RS? Abstracción de la complejidad de JNI

El valor fundamental de J4RS reside en su capacidad para proporcionar una interfaz de nivel superior y más idiomática para la comunicación entre lenguajes. En lugar de manejar manualmente los punteros JNIEnv* de JNI y las conversiones de tipo explícitas, J4RS ofrece un conjunto de abstracciones que hacen que la interacción se sienta mucho más natural tanto para los desarrolladores de Java como para los de Rust.

Características clave de J4RS

  • Conversión automática de tipos: Uno de los mayores inconvenientes de JNI es la serialización de tipos de datos entre Java y los lenguajes nativos. J4RS maneja muchas conversiones comunes automáticamente. Por ejemplo, un String de Java se puede consumir directamente como un String de Rust, y los tipos primitivos se asignan sin problemas. Esto reduce drásticamente la cantidad de código de conversión manual que necesita escribir. Para tipos más complejos, J4RS a menudo aprovecha la serialización (por ejemplo, a través de Serde en Rust) para facilitar el intercambio de datos.
  • API más sencilla: J4RS proporciona una API concisa e intuitiva tanto en el lado de Rust como en el de Java. En Rust, utilizará atributos específicos (como #[j4rs] o #[call_from_java]) para marcar las funciones que deben exponerse a Java. En el lado de Java, las interacciones se gestionan a través de una instancia de J4rs, lo que le permite cargar bibliotecas, crear objetos Java e invocar métodos con una sintaxis mucho más limpia que las llamadas JNI sin procesar.
  • Enfoque idiomático de Rust: J4RS tiene como objetivo que el lado de Rust de la integración se sienta lo más "Rust-y" posible. Esto significa que a menudo puede escribir funciones Rust con tipos familiares y mecanismos de manejo de errores (como Result), y J4RS se encarga del pegamento JNI subyacente.
  • Manejo de errores optimizado: Propagar errores limpiamente a través del límite Java-Rust es crucial. J4RS simplifica esto permitiendo que los tipos Result de Rust (que encapsulan el éxito o el fracaso) se traduzcan en excepciones de Java. Si una función Rust devuelve un Err, J4RS puede lanzar automáticamente una InvocationException (o una excepción personalizada) en el mundo Java, haciendo que la gestión de errores sea más robusta y explícita.
  • Llamadas bidireccionales: J4RS admite la llamada de Java desde Rust y la llamada de Rust desde Java. Si bien nuestro enfoque principal aquí es Rust desde Java, la capacidad de su código Rust para interactuar con la JVM (por ejemplo, instanciar objetos Java, llamar métodos Java, usar el vasto ecosistema de Java) agrega otra capa de flexibilidad.
  • Gestión de dependencias: J4RS puede ayudar con la gestión de dependencias de Java, incluida la carga de artefactos JAR de repositorios Maven, lo que simplifica la configuración de sus proyectos híbridos de Java-Rust.

Cómo funciona J4RS a alto nivel

En esencia, J4RS aprovecha JNI, pero lo hace entre bastidores. Cuando usted expone una función Rust a Java usando J4RS, el framework esencialmente genera o proporciona el código repetitivo JNI necesario para usted. Este código actúa como un contenedor alrededor de su lógica Rust pura. Cuando Java llama a un método expuesto por J4RS:

  1. El lado de Java realiza una llamada a un método proxy proporcionado por J4RS.
  2. J4RS, utilizando su implementación interna de JNI, serializa los argumentos de Java en tipos compatibles con Rust.
  3. Luego invoca su función Rust real.
  4. Al recibir el resultado de Rust (que podría ser un valor simple o un Result para el manejo de errores), J4RS lo serializa de nuevo en un tipo compatible con Java.
  5. Si se produjo un error en Rust, J4RS lo traduce en una excepción de Java, que luego se lanza en su aplicación Java.

Esta abstracción reduce significativamente la cantidad de código JNI de bajo nivel que necesita escribir y administrar, lo que le permite concentrarse en la lógica de negocios dentro de sus componentes Rust y Java. En las siguientes secciones, le guiaremos a través de la configuración de un proyecto y la construcción de un ejemplo práctico para demostrar J4RS en acción.

Configuración de su entorno de desarrollo

Antes de que podamos empezar a construir nuestra biblioteca nativa, necesitamos asegurarnos de que nuestro entorno de desarrollo esté correctamente configurado. Esto implica instalar Rust y Java, y luego configurar nuestros archivos de proyecto para que tanto los componentes de Rust como los de Java trabajen juntos sin problemas con J4RS.


---
config:
    theme: base
    layout: elk
---
graph TD
    subgraph Entorno de Desarrollo
        A[Su Computadora] --> B(Toolchain de Rust 
& Cargo); A --> C(Java JDK
& Maven/Gradle); end subgraph Estructura del Proyecto D[my-rust-library/] --> E(Cargo.toml
cdylib, j4rs); D --> F(src/lib.rs); G[my-java-app/] --> H(pom.xml
dependencia j4rs); G --> I(src/main/java/...); end E -- "Construye" --> J[libmy_rust_library.so/.dll/.dylib]; H -- "Carga" --> J; J -- "Llamado por" --> I; style D fill:#e0f2f7,stroke:#3498db,stroke-width:2px; style G fill:#fffde7,stroke:#f1c40f,stroke-width:2px; style J fill:#d4edda,stroke:#28a745,stroke-width:2px; style B fill:#a2d9ce,stroke:#1abc9c,stroke-width:2px; style C fill:#f8d7da,stroke:#dc3545,stroke-width:2px;

Figura 1: Estructura del proyecto y dependencias de alto nivel

Una vez completados estos pasos de configuración inicial, su entorno estará listo. Tenemos un proyecto Rust configurado para construir una biblioteca nativa y un proyecto Java listo para consumirla usando J4RS. En la siguiente sección, escribiremos nuestra primera función Rust y la llamaremos desde Java.

1. Instalación de Rust

La forma más fácil y recomendada de instalar Rust es usando rustup, el instalador oficial de la cadena de herramientas de Rust. Rustup gestiona las versiones de Rust y las herramientas asociadas, lo que simplifica la actualización de Rust, el cambio entre canales estables/beta/nocturnos y la adición de objetivos de compilación cruzada.

  • En Linux o macOS: Abra su terminal y ejecute el siguiente comando:
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    Siga las instrucciones en pantalla. Es posible que se le pida su contraseña.
  • En Windows: Descargue y ejecute rustup-init.exe desde rustup.rs. Siga las instrucciones del instalador gráfico. También es posible que necesite instalar las Visual Studio C++ Build Tools cuando se le solicite, ya que Rust las necesita para la vinculación en Windows.

Después de la instalación, abra una nueva terminal o símbolo del sistema y verifique la instalación:

rustc --version
cargo --version

Debería ver los números de versión tanto para el compilador de Rust (rustc) como para Cargo, el gestor de paquetes de Rust.

2. Instalación de Java JDK

Necesitará un Kit de Desarrollo de Java (JDK) instalado para compilar y ejecutar su aplicación Java. J4RS requiere Java 8 o posterior. Si no tiene uno, puede descargar un JDK de varios proveedores (por ejemplo, Oracle, OpenJDK, Adoptium/Eclipse Temurin).

Verifique su instalación de Java:

java -version
javac -version

Asegúrese de que ambos comandos devuelvan un número de versión, lo que indica que el JDK está correctamente instalado y configurado en la PATH de su sistema.

3. Configuración de su proyecto

Crearemos un proyecto de varias partes: una biblioteca Rust y una aplicación Java que la consume. Recomendamos usar una herramienta de automatización de compilación como Maven o Gradle para la parte de Java, ya que simplifica la gestión de dependencias.

3.1. Creación de un nuevo proyecto Rust (Biblioteca)

Navegue a la carpeta de su proyecto deseado en su terminal y cree un nuevo proyecto de biblioteca Rust usando Cargo:

cargo new my-rust-library --lib
cd my-rust-library

Este comando crea un nuevo directorio my-rust-library con un Cargo.toml básico y src/lib.rs.

3.2. Adición de la dependencia J4RS a Cargo.toml

Abra el archivo Cargo.toml en su directorio my-rust-library y añada las dependencias j4rs y j4rs_derive bajo la sección [dependencies]. Además, y de forma crucial, especifique el tipo de crate como cdylib (biblioteca dinámica compatible con C) para que Java pueda cargarla.

# Cargo.toml para su biblioteca Rust

[package]
name = "my-rust-library"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"] # Esto es esencial para crear una biblioteca compartida cargable por Java

[dependencies]
j4rs = "0.22.0" # Use la última versión disponible en crates.io
j4rs_derive = "0.22.0" # Crate complementario para macros, misma versión que j4rs
serde = { version = "1.0", features = ["derive"] } # Para serialización opcional de tipos complejos
serde_json = "1.0" # Para serialización opcional de tipos complejos

Nota: Las versiones de j4rs y j4rs_derive siempre deben coincidir. Las dependencias serde y serde_json son altamente recomendables para manejar estructuras de datos más complejas entre Rust y Java, ya que J4RS a menudo aprovecha Serde para esto.

3.3. Configuración de un proyecto Java (Ejemplo de Maven)

Fuera de su directorio my-rust-library, cree un nuevo proyecto Java. Para simplificar, usaremos Maven. Puede usar un IDE o la línea de comandos:

# En el directorio padre (por ejemplo, junto a my-rust-library)
mvn archetype:generate -DgroupId=com.example.app -DartifactId=my-java-app -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
cd my-java-app
3.4. Adición de la dependencia J4RS a pom.xml (Java)

Abra el archivo pom.xml en su directorio my-java-app y añada la dependencia de J4RS Java a la sección <dependencies>:

<!-- pom.xml para su aplicación Java -->

<dependencies>
    <dependency>
        <groupId>io.github.astonbitecode</groupId>
        <artifactId>j4rs</artifactId>
        <version>0.22.0</version> <!-- Use la misma versión mayor.menor que su crate Rust j4rs -->
    </dependency>

    <!-- Dependencia estándar de JUnit, a menudo incluida por el arquetipo -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Es crucial que la versión de J4RS en su pom.xml de Java sea compatible con la versión utilizada en su Cargo.toml de Rust. Generalmente, mantener la misma versión mayor.menor es una apuesta segura.

Una vez completados estos pasos de configuración inicial, su entorno estará listo. Tenemos un proyecto Rust configurado para construir una biblioteca nativa y un proyecto Java listo para consumirla usando J4RS. En la siguiente sección, escribiremos nuestra primera función Rust y la llamaremos desde Java.

Construyendo su primera biblioteca nativa con Rust y J4RS: Una guía paso a paso

Con nuestro entorno de desarrollo configurado, ¡es hora de escribir algo de código! En esta sección, crearemos una función Rust simple, la compilaremos en una biblioteca nativa y luego la invocaremos desde nuestra aplicación Java usando J4RS.

Escenario: Una función de saludo simple

Comencemos con una variación clásica de "¡Hola, mundo!". Nuestra función Rust tomará un nombre (un String) como entrada y devolverá un saludo personalizado (otro String). Esto demuestra el paso básico de cadenas y los valores de retorno, que son tareas comunes de interoperabilidad.

Paso 1: Definir la función nativa en Rust

Abra el archivo src/lib.rs de su biblioteca Rust (ubicado en el directorio my-rust-library) y reemplace su contenido con el siguiente código:



// src/lib.rs en my-rust-library

use j4rs::{Jvm, Instance, InvocationArg, JvmBuilder};
use j4rs_derive::call_from_java;

/// Esta función es invocable desde Java.
/// Toma un String de Rust (que J4RS convierte de un String/Instance de Java)
/// y devuelve un String de Rust (que J4RS convierte de nuevo a un String/Instance de Java).
#[call_from_java("com.example.app.MyJavaApp.greetFromRust")] // Especifique el nombre completo de la clase y el método Java
pub fn greet_from_rust(jvm: Instance, arguments: Vec<Instance>) -> Result<Instance, String> {
    // Las funciones de J4RS invocables desde Java siempre toman 'jvm: Instance' y 'arguments: Vec>Instance<'.
    // La instancia 'jvm' le permite interactuar con la JVM desde Rust.
    // 'arguments' contiene los parámetros pasados desde Java, envueltos en instancias de J4RS.

    let jvm_rust: Jvm = JvmBuilder::new().build().unwrap();

    let name_instance = arguments.get(0).ok_or("Se esperaba un argumento (nombre)".to_string())?;
    let name: String = jvm_rust.to_rust(name_instance)?;

    let greeting = format!("¡Hola, {} desde Rust!", name);

    // Convierta el String de Rust de nuevo a una instancia de J4RS para Java.
    let result_instance = jvm_rust.create_instance("java.lang.String", &[InvocationArg::try_from(greeting)?])?;
    
    Ok(result_instance)
}


Analicemos este código Rust:

  • use j4rs::...: Importamos los componentes necesarios del crate J4RS.
  • #[call_from_java("com.example.app.MyJavaApp.greetFromRust")]: Este es el atributo central de J4RS. Le indica a J4RS que exponga esta función Rust a Java, especificando el nombre completo de la clase Java y el nombre del método que la invocará. Así es como J4RS sabe cómo generar el código repetitivo de JNI.
  • pub fn greet_from_rust(jvm: Instance, arguments: Vec<Instance>) -> Result<Instance, String>:
    • Firma: Todas las funciones Rust invocables desde Java a través de J4RS deben adherirse a esta firma específica. Reciben un jvm: Instance (que representa el contexto de la JVM) y arguments: Vec<Instance> (un vector de argumentos Java, cada uno envuelto en una Instance de J4RS).
    • Tipo de retorno: Se espera que la función devuelva un Result<Instance, String>. Esto nos permite devolver un resultado exitoso (envuelto en una Instance para Java) o un error (un String que describe el fallo, que J4RS convertirá en una excepción de Java).
  • let name: String = jvm_rust.to_rust(name_instance)?;: Esta línea demuestra la conversión automática de tipos de J4RS. Extraemos el primer argumento del vector arguments y usamos jvm_rust.to_rust() para convertir la Instance de Java (que representa un String de Java en este caso) en un String nativo de Rust. El operador ? es para la propagación de errores.
  • let result_instance = jvm_rust.create_instance(...): Para devolver un valor a Java, convertimos nuestro String de Rust de nuevo en un String de Java. Esto se hace creando una nueva instancia de String de Java usando jvm_rust.create_instance(), envolviendo nuestra cadena Rust como un InvocationArg.

Paso 2: Compilar el código Rust en una biblioteca compartida

Navegue a su directorio my-rust-library en la terminal y compile el proyecto. Compilaremos en modo de lanzamiento para un rendimiento optimizado.


# En el directorio my-rust-library
cargo build --release

Tras la compilación exitosa, Cargo generará la biblioteca compartida nativa en el directorio target/release/. El nombre del archivo variará según su sistema operativo:

  • Linux: libmy_rust_library.so
  • Windows: my_rust_library.dll
  • macOS: libmy_rust_library.dylib

Anote la ruta exacta a esta biblioteca generada, ya que Java necesitará encontrarla.

Paso 3: Llamar a la función nativa desde Java

Ahora, cambiemos a nuestro proyecto Java. Abra el archivo App.java (ubicado en my-java-app/src/main/java/com/example/app/) y modifique su contenido de la siguiente manera:


// src/main/java/com/example/app/App.java en my-java-app

package com.example.app;

import org.astonbitecode.j4rs.api.j4rs.J4rs;
import org.astonbitecode.j4rs.api.Instance;
import org.astonbitecode.j4rs.api.InvocationException;

public class MyJavaApp {

    public static void main(String[] args) {
        try {
            // Inicializar J4rs. Esto crea una instancia de J4RS que permite la interacción con Rust.
            J4rs j4rs = J4rs.getInstance();

            // --- IMPORTANTE: Cargar la biblioteca compartida de Rust ---
            // Debe proporcionar la ruta absoluta a su biblioteca Rust compilada.
            // Reemplace este marcador de posición con la ruta real encontrada en el Paso 2.
            String rustLibraryPath = "/path/to/my-rust-library/target/release/libmy_rust_library.so";
            // Para Windows: "C:\\path\\to\\my-rust-library\\target\\release\\my_rust_library.dll"
            // Para macOS: "/path/to/my-rust-library/target/release/libmy_rust_library.dylib"

            j4rs.loadLibrary(rustLibraryPath);
            System.out.println("Biblioteca Rust cargada exitosamente desde: " + rustLibraryPath);

            // Invocar la función Rust.
            // El primer argumento de callNative() es el nombre completo del método Java
            // que corresponde al atributo #[call_from_java] en Rust.
            // Los argumentos subsiguientes son los parámetros reales a pasar a la función Rust.
            Instance<String> greetingInstance = j4rs.callNative(
                "com.example.app.MyJavaApp.greetFromRust",
                "Alice" // El nombre a pasar a Rust
            );

            // Convertir la instancia devuelta de nuevo a un String de Java.
            String greeting = greetingInstance.get();

            System.out.println("Recibido de Rust: " + greeting);

        } catch (InvocationException e) {
            System.err.println("Error durante la invocación nativa: " + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            System.err.println("Se produjo un error inesperado: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Puntos clave en el código Java:

  • J4rs j4rs = J4rs.getInstance();: Esta línea obtiene la instancia singleton del framework J4rs, que es su punto de entrada para todas las interacciones.
  • j4rs.loadLibrary(rustLibraryPath);: ¡Esto es crítico! Debe indicarle explícitamente a J4RS dónde encontrar su biblioteca compartida de Rust compilada. Reemplace la ruta del marcador de posición con la ruta absoluta real que anotó en el Paso 2.
  • j4rs.callNative("com.example.app.MyJavaApp.greetFromRust", "Alice");: Así es como se invoca la función Rust.
    • El primer argumento es un String que coincide con la ruta completa que especificó en el atributo #[call_from_java(...)] en su código Rust.
    • Los argumentos posteriores son los valores reales que desea pasar a su función Rust. J4RS maneja automáticamente la conversión de primitivas de Java y objetos comunes (como String) a los tipos Instance esperados por la función Rust.
  • Instance<String> greetingInstance = ...; String greeting = greetingInstance.get();: J4RS devuelve los resultados envueltos en un objeto Instance. Luego, usa el método get() para recuperar el objeto Java real.
  • try...catch (InvocationException e): J4RS envuelve cualquier error que se origine en el lado de Rust (es decir, si su función Rust devuelve un Err) en una InvocationException en Java, proporcionando una forma limpia de manejar los fallos.

Paso 4: Ejecutar su aplicación Java

Ahora, guarde todos sus archivos y compile su aplicación Java. Navegue a su directorio my-java-app en la terminal:


# En el directorio my-java-app
mvn clean install exec:java -Dexec.mainClass="com.example.app.MyJavaApp"

Debería ver una salida similar a esta:

Biblioteca Rust cargada exitosamente desde: /path/to/my-rust-library/target/release/libmy_rust_library.so
Recibido de Rust: ¡Hola, Alice desde Rust!

¡Felicidades! Ha creado con éxito su primera biblioteca nativa con Rust y la ha invocado desde Java usando J4RS. Este ejemplo demuestra el flujo básico, pero J4RS es capaz de mucho más, incluido el manejo de estructuras de datos complejas y llamadas bidireccionales.