Rust Code in Julia aufrufen
Inhalt
Ich habe schon länger eine kleine Faszination für die Kombinierung von mehreren Programmiersprachen in einem Projekt. Ich finde es begeisternd, wie es geht, dass zwei in sich vollständige Sprachen miteinander interagieren. Da wird einem klar, dass die Sprachen selber lediglich Abstraktionen der zugrunde liegenden Datenverarbeitungsschritte sind. Und diese Daten sind nun mal immer die gleichen, Einsen und Nullen.
Die zwei Sprachen, mit denen ich mit hauptsächlich beschäftige, sind Rust und Julia. Wobei Rust eine Systemprogrammiersprache ist, die es einem ermöglicht sehr feine Kontrolle über die Operationen zu haben, was sie auch sehr schnell macht (oder es einem zumindest einfacher macht). Julia ist eine Sprache, die aus der Wissenschaft kommt und vor allem für rechenintensive Simulationen zum Einsatz kommt. Es überzeugt durch seinen einfachen Syntax, in dem Typen optional sind. Julia möchte so lesbar sein, dass es “nicht nur zur Kommunikation von Menschen und Maschinen dient, sondern auch zwischen Menschen”.
Warum sollte man das wollen?⌗
Es gibt mehrere Gründe, warum es sinnvoll sein kann eine andere Sprache für einen bestimmten Teil der Anwendung zu nutzen. Die Sprache ist ja nur das Werkzeug, und man nimmt das Werkzeug, das für die Arbeit am besten geeignet ist. Man will sich ja keine unnötige Arbeit machen. Es kann auch um Performanz gehen, auch wenn ich finde, dass die etwas überbewertet ist. Oder aber auch um die Verfügbarkeit von Bibliotheken in der jeweiligen Sprache. So war es bei mir zum Beispiel der Fall. Für eine Aufgabe im Bundesinformatikwettbewerb wollte ich eine Zahl in ein anderes Zahlensystem umrechnen, habe aber dafür kein Julia Paket gefunden. Also habe ich eine Rust Bibliothek genutzt und eine einfache “api” gebaut, mit der ich meinen Rust code aufrufen kann. Das wird hier auch als Beispiel dienen.
Erwähnenswert ist noch, dass es deutlich einfacher und sinnvoller ist Rust von Julia aus aufzurufen, als andersherum, denn Julia ist eine dynamische und vor allem interpretierte Sprache. Das bedeutet, dass der just-in-time-compiler immer den Quellcode vorliegen hat 12. Also bedient man sich einer kompilierten Sprache wie Rust. Man baut also die Rust Teile und verlinkt sie dann mit Julia. Und weil julia immer auf den Kompiler/Interpreter angewiesen ist, müsste man ihn immer mit in die Binärdatei einbauen, wenn man ihn nicht als Abhängigkeit haben möchte.
Lösungsweg⌗
Es ist recht simpel wirklich, denn beide Sprachen versprechen eine einfache Interaktion mit der Programmiersprache C. Für uns in diesem Fall relevant ist, dass Rust sich so kompilieren lässt, dass die Binärdateien/die Bibliotheken so aussehen, dass sie von C sein könnten. Jedes C Programm könnte sie aufrufen. Und Julia verspricht, dass man besonders leicht C Funktionen aufrufen kann.
- Rust so kompilieren, dass es aussieht wie eine C Bibliothek
- in Julia den “C Code” aufrufen.
Umsetzung⌗
In Rust deklariert man in der Cargo.toml
Datei, dass es eine dylib
ist. Also eine dynamisch
verlinkbare Bibliothek (eine .so
-Datei).
# Cargo.toml
[package]
name = "rust-from-julia"
version = "0.1.0"
edition = "2021"
author = ["Lovis Rentsch"]
[lib]
crate-type = ["dylib"]
Dann muss man noch vor der Funktion, die für Julia sichtbar sein soll das Attribut #[no_mangle]
verwenden.
// lib.rs
#[no_mangle]
pub fn hey() -> i64 {
if functioncall() {
println!("hey");
}
return 11;
}
In Julia kann man dann einfach das ccall
Macro verwenden um diese Funktion dann aufzurufen.
# main.jl
@ccall ".<path-to-libfile>.so".hey()::Int64
Der Aufbau des ccall
Makros ist wie folgt:
@ccall "<path-to-libfile>".function_name(argument_val::argument_type, ...)::return_type
Der Output wäre dann “hey
”.
Ich möchte noch darauf hinweisen, dass ich hier einen Funtioncall ausführe (zu einer Funktion,
die immer true
Antwortet). Diese Funktion ist aber für Julia nicht sichtbar. Das ist einerseits
sehr nützlich, denn man kann einen normalen Rust-Programmaufbau verwenden. Die Kontrolle wird
erst wieder an den Julia code übergeben, wenn die Funktion, die aufgerufen wurde endet.
No Mangle⌗
Zum Schluss möchte ich noch etwas auf das #[no_mangle]
Attribut ein gehen. Normalerweise
verwendet der Rust Kompiler nicht den namen aus dem Programmcode, sondern ein anderes
Identifizierungszeichen. Durch das Attribut wird es aus dem lesbaren code übernommen. Das ist
zum Beispiel wichtig, wenn man es duch anderen Code aufrufen will. In Julia haben wir ja auch
den Funktionsnamen verwendet. Außerdem exportiert der Kompiler den Code dann so, dass die
Binärwerte für alle sichtbar in der Datei liegen.