Maison > Java > javaDidacticiel > le corps du texte

rs Construire un framework JNI

Patricia Arquette
Libérer: 2024-10-02 06:10:30
original
410 Les gens l'ont consulté

rs Building a JNI Framework

Hé ! C'est mon deuxième post, techniquement, mais c'est mon premier vrai. (Je vais juste ignorer le post t3d.)

Je n'ai pas non plus utilisé ce compte depuis une éternité mais peu importe

Dans cet article, je vais expliquer ce qu'est rs4j, comment l'utiliser et comment je l'ai construit.

Qu'est-ce que c'est?

rs4j est une bibliothèque Rust que j'ai créée pour faciliter la création de bibliothèques Java qui utilisent du code natif écrit en Rust. Il génère du code JNI (Java Native Interface) pour accomplir cela.

D'accord, c'est cool, mais pourquoi devrais-je m'en soucier ?

rs4j vous permet de décharger le travail de calcul élevé vers un environnement d'exécution beaucoup plus rapide (vous regarde, garbage collector) au lieu de tout exécuter dans la JVM et de ruiner les performances. Les mods Minecraft comme Create Aeronautics (ou Create Simulated, pour être plus correct) utilisent cette technique pour effectuer certains de leurs calculs physiques qui seraient autrement très lents avec Java.

rs4j vous permet de créer facilement des interfaces natives comme celle-ci avec un minimum de code et de porter facilement des bibliothèques entières pour une utilisation avec Java avec un minimum de code.

D'accord, maintenant je suis intéressé, mais comment puis-je l'utiliser ?

L'utiliser est simple ! Suivez simplement ces étapes :

  1. Configurez votre type de bibliothèque :
# Cargo.toml

[lib]
crate-type = ["cdylib"]
Copier après la connexion
  1. Ajoutez rs4j à vos dépendances :
cargo add rs4j
Copier après la connexion
  1. Ajoutez rs4j à vos dépendances de build :
cargo add rs4j --build -F build # Enable the `build` feature

# Also add anyhow for error handling
cargo add anyhow --build
Copier après la connexion
  1. Configurez votre buildscript :
// build.rs

use rs4j::build::BindgenConfig;
use anyhow::Result;

fn main() -> Result<()> {
    // Make a new config
    BindgenConfig::new()

        // Set the package for export
        .package("your.package.here")

        // Where to save the Rust bindings
        .bindings(format!("{}/src/bindings.rs", env!("CARGO_MANIFEST_DIR")))

        // Where the input files are
        .glob(format!("{}/bindings/**/*.rs4j", env!("CARGO_MANIFEST_DIR")))?

        // Where to save java classes (is a directory)
        .output(format!("{}/java", env!("CARGO_MANIFEST_DIR")))

        // Enable JetBrains annotations (this is a TODO on my end)
        .annotations(true)

        // Go!
        .generate()?;

    Ok(())
}
Copier après la connexion
  1. Configurez votre script de post-build (facultatif) :

rs4j utilise un script de post-construction pour accomplir des actions après la construction.
Ceci est techniquement facultatif, mais recommandé.

# Cargo.toml

[features]
default = []
post-build = ["rs4j/build", "anyhow"]

[[bin]]
name = "post-build"
path = "post-build.rs"
required-features = ["post-build"]

[dependencies]
anyhow = { version = "[...]", optional = true } # Set the version to whatever you want
rs4j = "[...]" # Whatever you had before
Copier après la connexion
// post-build.rs

use anyhow::Result;
use rs4j::build::BindgenConfig;

fn main() -> Result<()> {
    let out_path = format!("{}/generated", env!("CARGO_MANIFEST_DIR"));
    let src_path = format!("{}/java/src/generated", env!("CARGO_MANIFEST_DIR"));

    BindgenConfig::new()
        // This should be the same as the normal buildscript
        .package("com.example")
        .bindings(format!("{}/src/bindings.rs", env!("CARGO_MANIFEST_DIR")))
        .glob(format!("{}/bindings/**/*.rs4j", env!("CARGO_MANIFEST_DIR")))?
        .output(&out_path)
        .annotations(false)

        // Run post-build actions
        .post_build()?

        // Copy it to your Java project
        .copy_to(src_path)?;

    Ok(())
}
Copier après la connexion
  1. Installer la CLI de rs4j (facultatif)

Ceci est facultatif si vous ne souhaitez pas utiliser le script de post-build.

cargo install rs4j --features cli
Copier après la connexion
  1. Construisez !

Modifiez tous les scripts comme ceci :

- cargo build
+ rs4j build # `rs4j build` supports all of `cargo build`'s arguments after a `--`.
Copier après la connexion

Syntaxe

Voici un aperçu de base de la syntaxe :

// This class, Thing, takes in one type parameter, `A`.
// You can omit this if it doesn't take any type parameters.
class Thing<A> {
    // This makes it so that Rust knows that the type for `A`
    // will have `Clone + Copy`. This doesn't change anything
    // on the Java side, it's just so that Rust will compile.
    bound A: Clone + Copy;

    // This will generate getters and setters for the field `some`.
    field some: i32;

    // Here, the Rust function's name is `new`, and Java will treat
    // it as a constructor.
    static init fn new(value: A) -> Thing;

    // This gets the value. Since this is in snake_case, rs4j will
    // automatically convert it into camelCase, renaming this to
    // `getValue` on the Java side.
    fn get_value() -> A;

    // This marks this function as mutable, meaning in Rust it will
    // mutate the struct, as if it took a `&mut self` as an argument.
    mut fn set_value(value: A);

    // You can even include trait methods, as long as Rust can find the
    // trait it belongs to!
    fn clone() -> A;
};
Copier après la connexion

Comment a-t-il été fabriqué ?

rs4j utilise un analyseur peg pour traiter son langage. Cet analyseur transforme directement la structure analysée en un arbre syntaxique abstrait, qui est transformé en code.

rs4j est fortement typé. J'ai une structure Type et une énumération TypeKind pour accomplir cela.

TypeTypes

Ceux-ci sont analysés à l'aide de ce code :

parser! {
    /// The rs4j parser.
    pub grammar rs4j_parser() for str {
        ...

        // Type kinds

        rule _u8_k() -> TypeKind = "u8" { TypeKind::U8 }
        rule _u16_k() -> TypeKind = "u16" { TypeKind::U16 }
        rule _u32_k() -> TypeKind = "u32" { TypeKind::U32 }
        rule _u64_k() -> TypeKind = "u64" { TypeKind::U64 }
        rule _i8_k() -> TypeKind = "i8" { TypeKind::I8 }
        rule _i16_k() -> TypeKind = "i16" { TypeKind::I16 }
        rule _i32_k() -> TypeKind = "i32" { TypeKind::I32 }
        rule _i64_k() -> TypeKind = "i64" { TypeKind::I64 }
        rule _f32_k() -> TypeKind = "f32" { TypeKind::F32 }
        rule _f64_k() -> TypeKind = "f64" { TypeKind::F64 }
        rule _bool_k() -> TypeKind = "bool" { TypeKind::Bool }
        rule _char_k() -> TypeKind = "char" { TypeKind::Char }
        rule _str_k() -> TypeKind = "String" { TypeKind::String }
        rule _void_k() -> TypeKind = "()" { TypeKind::Void }
        rule _other_k() -> TypeKind = id: _ident() { TypeKind::Other(id) }
        rule _uint_k() -> TypeKind = _u8_k() / _u16_k() / _u32_k() / _u64_k()
        rule _int_k() -> TypeKind = _i8_k() / _i16_k() / _i32_k() / _i64_k()
        rule _float_k() -> TypeKind = _f32_k() / _f64_k()
        rule _extra_k() -> TypeKind = _bool_k() / _char_k() / _str_k() / _void_k()

        ...
    }
}
Copier après la connexion

Comme vous pouvez le voir, il existe une règle différente pour chaque type primitif, puis une règle fourre-tout. Cela me permet de vérifier et de générer facilement le code correct.

Vous pouvez voir plus de l'analyseur ici.

Codegen

rs4j utilise un système de codegen personnalisé qui utilise fortement format!() pour créer le code. Bien que ce ne soit pas le plus correct ou le plus sûr, cela crée un code correct dans presque tous mes tests (le seul problème concerne les génériques, sur lesquels je travaille).

Le codegen est réalisé avec chaque nœud AST ayant ses propres fonctions pour le transformer en code Java et Rust.

Implémentation native

Dans votre lib.rs, vous devez inclure!() votre fichier binds.rs, qui contient les implémentations natives.

Chaque structure pour laquelle vous générez des liaisons sera enveloppée avec JNI. Voici un exemple de ce à quoi cela ressemble :

class MyOtherStruct {
    field a: String;
    field b: MyStruct;

    static init fn new() -> Self;

    fn say_only(message: String);
    fn say(p2: String);
    fn say_with(p1: MyStruct, p2: String);
};
Copier après la connexion
// lib.rs

...

#[derive(Debug)]
pub struct MyOtherStruct {
    pub a: String,
    pub b: MyStruct,
}

impl MyOtherStruct {
    pub fn new() -> Self {
        Self {
            a: String::new(),
            b: MyStruct::new(),
        }
    }

    pub fn say_only(&self, message: String) {
        println!("{}", message);
    }

    pub fn say(&self, p2: String) {
        println!("{}{}", self.b.a, p2);
    }

    pub fn say_with(&self, p1: MyStruct, p2: String) {
        println!("{}{}", p1.a, p2);
    }
}

include!("bindings.rs");

// bindings.rs
// #[allow(...)] statements have been removed for brevity.

#[allow(non_camel_case_types)]
pub struct __JNI_MyOtherStruct {
    pub a: String,
    pub b: *mut MyStruct,
}

impl __JNI_MyOtherStruct {
    pub unsafe fn of(base: MyOtherStruct) -> Self {
        Self {
            a: base.a.clone(),
            // yes, this is an intentional memory leak.
            b: Box::leak(Box::new(base.b)) as *mut MyStruct,
        }
    }

    pub unsafe fn to_rust(&self) -> MyOtherStruct {
        MyOtherStruct {
            a: self.a.clone(),
            b: (&mut *self.b).clone(),
        }
    }

    pub unsafe fn __wrapped_new() -> Self {
        let base = MyOtherStruct::new();

        Self::of(base)
    }

    pub unsafe fn __wrapped_say_only(&self, message: String) -> () {
        MyOtherStruct::say_only(&self.to_rust(), message).clone()
    }

    pub unsafe fn __wrapped_say(&self, p2: String) -> () {
        MyOtherStruct::say(&self.to_rust(), p2).clone()
    }

    pub unsafe fn __wrapped_say_with(&self, p1: MyStruct, p2: String) -> () {
        MyOtherStruct::say_with(&self.to_rust(), p1, p2).clone()
    }
}
Copier après la connexion

Lorsqu'un objet est construit, il appelle la méthode enveloppée qui fuit intentionnellement chaque objet imbriqué pour obtenir son pointeur. Cela me permet d'accéder à l'objet quand j'en ai besoin, dans n'importe quel contexte.

Toutes les méthodes sont enveloppées pour permettre à JNI de les appeler beaucoup plus facilement.

Code JNI

En parlant de ça, le code JNI ressemble à ceci :

// This is a field, here's the getter and setter.
// #[allow(...)] statements have been removed for brevity.

#[no_mangle]
pub unsafe extern "system" fn Java_com_example_MyOtherStruct_jni_1set_1a<'local>(
    mut env: JNIEnv<'local>,
    class: JClass<'local>,
    ptr: jlong,
    val: JString<'local>,
) -> jlong {
    let it = &mut *(ptr as *mut __JNI_MyOtherStruct);
    let val = env.get_string(&val).unwrap().to_str().unwrap().to_string();

    it.a = val;

    ptr as jlong
}

#[no_mangle]
pub unsafe extern "system" fn Java_com_example_MyOtherStruct_jni_1get_1a<'local>(
    mut env: JNIEnv<'local>,
    class: JClass<'local>,
    ptr: jlong,
) -> jstring {
    let it = &*(ptr as *mut __JNI_MyOtherStruct);
    env.new_string(it.a.clone()).unwrap().as_raw()
}
Copier après la connexion

C'est un truc assez standard pour la caisse jni, sauf pour accéder à l'objet. Ce &*(ptr as *mut __JNI_MyOtherStruct) peut sembler dangereux, et c'est parce qu'il l'est. Ceci est cependant intentionnel, car le pointeur devrait toujours valide s'il est effectué correctement.

Remarquez qu'à la fin du setter, il renvoie le pointeur de l'objet. C'est prévu. Cela permet à Java de réinitialiser son pointeur interne, en gardant une trace du dernier pointeur valide.

Libérer la mémoire

Libérer de la mémoire récupère essentiellement le pointeur puis le supprime. Cela libère également tous les champs non primitifs.

// #[allow(...)] statements have been removed for brevity.

#[no_mangle]
pub unsafe extern "system" fn Java_com_example_MyOtherStruct_jni_1free<'local, >(_env: JNIEnv<'local>, _class: JClass<'local>, ptr: jlong) {
    // Reclaim the pointer
    let it = Box::from_raw(ptr as *mut __JNI_MyOtherStruct);

    // Reclaim the other field
    let _ = Box::from_raw(it.b);
}
Copier après la connexion

Il existe cependant un bug connu avec cette méthode, à savoir qu'elle finira toujours par perdre de la mémoire s'il y a un objet imbriqué à plus d'un niveau de profondeur. J'ai quelques idées sur la façon de résoudre ce problème, mais je me suis concentré sur autre chose.

Le côté Java

Chaque classe Java générée par rs4j héritera de deux autres interfaces, ParentClass et NativeClass.

Voici la définition des deux :

// NativeClass.java

package org.stardustmodding.rs4j.util;

public interface NativeClass {
    long getPointer();
}

// ParentClass.java

package org.stardustmodding.rs4j.util;

public interface ParentClass {
    void updateField(String field, long pointer);
}
Copier après la connexion

Chaque cours est composé de quelques parties, dont :

  • JNI Methods
// Notice how all of these functions take a `long ptr` as an argument. This is the pointer to the underlying struct in Rust.

// This is a constructor - it takes no pointer but returns one.
private native long jni_init_new();

// Methods
private static native void jni_say_only(long ptr, String message);
private static native void jni_say(long ptr, String p2);
private static native void jni_say_with(long ptr, long p1, String p2);

// Getters & Setters
private static native long jni_set_a(long ptr, String value);
private static native String jni_get_a(long ptr);
// Notice how this field isn't primitive, so it uses the pointer instead.
private static native long jni_set_b(long ptr, long value);
private static native long jni_get_b(long ptr);

// Freeing memory
private static native void jni_free(long ptr);
Copier après la connexion
  • Fields
// The pointer to the Rust object
private long __ptr = -1;
// If this is a field in another class, it keeps track of it for updating purposes
private ParentClass __parent = null;
// The name of the field in the other class
private String __parentField = null;
Copier après la connexion
  • Constructors
public MyOtherStruct() {
    // Sets the pointer using the constructor
    __ptr = jni_init_new();
}
Copier après la connexion
  • Methods
// Notice how these all just call the JNI method, providing the pointer.

public void sayOnly(String message) {
    jni_say_only(__ptr, message);
}

public void say(String p2) {
    jni_say(__ptr, p2);
}

public void sayWith(MyStruct p1, String p2) {
    jni_say_with(__ptr, p1.getPointer(), p2);
}
Copier après la connexion
  • Fields
// Notice how the setters all update the field in the parent. This allows the user to have Java-like behavior, where modifying a class that is a property of another will update that reference.

public void setA(String value) {
    __ptr = jni_set_a(__ptr, value);

    if (__parent != null) {
        __parent.updateField(__parentField, __ptr);
    }
}

public String getA() {
    return jni_get_a(__ptr);
}

public void setB(MyStruct value) {
    // .getPointer() gets the underlying pointer, this is from the NativeClass interface.
    __ptr = jni_set_b(__ptr, value.getPointer());

    if (__parent != null) {
        __parent.updateField(__parentField, __ptr);
    }
}

public MyStruct getB() {
    // Essentially this is a glorified cast.
    return MyStruct.from(jni_get_b(__ptr), this, "b");
}
Copier après la connexion
  • Default constructors
// Just creates an instance from a pointer.
private MyOtherStruct(long ptr) {
    __ptr = ptr;
}

// Creates an instance from a pointer, with a parent
private MyOtherStruct(long ptr, ParentClass parent, String parentField) {
    __ptr = ptr;
    __parent = parent;
    __parentField = parentField;
}

// These are for other classes to "cast" to this class.

public static MyOtherStruct from(long ptr) {
    return new MyOtherStruct(ptr);
}

public static MyOtherStruct from(long ptr, ParentClass parent, String parentField) {
    return new MyOtherStruct(ptr, parent, parentField);
}
Copier après la connexion
  • And finally, default methods.
// I'M FREE!!!!
// This is ESSENTIAL for memory management, as Rust will otherwise never know when to free the memory that was leaked.
public void free() {
    jni_free(__ptr);
}

// Override from NativeClass.
@Override
public long getPointer() {
    return __ptr;
}

// Override from ParentClass.
@Override
public void updateField(String field, long pointer) {
    // `b` is non-primitive, so when it's updated it also has to be updated here.
    if (field == "b") {
        __ptr = jni_set_b(__ptr, pointer);
    }
}
Copier après la connexion

Wrapping Up

This project is probably one of my proudest projects right now, as it's taken so much work and is proving to be pretty useful for me. I hope you'll check it out and play around with it, too!

Anyway, see you in the next one! I'll try to post more often if I can!

Special Thanks

Thanks to @RyanHCode for giving me a few tips on this!

Links

  • GitHub Repository: https://github.com/StardustModding/rs4j
  • Crates.io Page: https://crates.io/crates/rs4j
  • Docs.rs Page: https://docs.rs/rs4j

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal
À propos de nous Clause de non-responsabilité Sitemap
Site Web PHP chinois:Formation PHP en ligne sur le bien-être public,Aidez les apprenants PHP à grandir rapidement!