Ich bin vor einigen Tagen zufällig über Mozillas (relativ) neue Programmiersprache Rust gestolpert und war direkt angetan. Natürlich war die erste Frage, die sich mir stellte, ob man Rust wohl auch von Vala aus nutzen kann, da es für geschwindigkeitskritische Anwendung sehr gut geeignet scheint.

Wie in der Anleitung gut beschrieben ist, lassen sich Rust-Bibliotheken von anderen Sprachen aus nutzen, darunter natürlich auch C. Da Vala zu C kompiliert wird, lag der Verdacht also nah, dass es auch möglich sein muss, Rust von Vala aus zu nutzen. Und der Weg dorthin wird durch diese Annahme eigentlich auch schon vorgegeben.

Schreiben wir uns also eine kleine Rust-Bibliothek, die uns als Test dienen soll. Ich habe von Anfang an Cargo genutzt, da es sich einfach bedienen lässt und lästige Routineaufgaben übernimmt. Erstellen wir also ein leeres Cargo-Projekt:

cargo new vala_test

Nachdem wir in das erzeugte Verzeichnis vala_test gewechselt sind, müssen wir zunächst

[lib]
name = "valatest"
crate-type = ["dylib"]

an die Datei Cargo.toml anhängen, um Cargo mitzuteilen, dass wir eine dynamisch gelinkte Bibliothek (crate-type = ["dylib"]) mit dem Namen libvalatest.so (name = "valatest") erstellen wollen.

Anschließend öffnen wir die bereits vorhandene lib.rs im Ordner src und fügen folgenden Quelltext ein:

pub struct TestType {
    int1: i32,
    int2: u8,
}

#[no_mangle]
pub extern fn test(int1: i32, int2: i64, uint1: u8, text: char, slice: &[u32], test_type: &TestType) {
    println!("int1: {}", int1);
    println!("int2: {}", int2);
    println!("uint1: {}", uint1);
    println!("text: {}", text);

    for element in slice {
        println!("slice: {}", element);
    }

    println!("TestType int1: {}", test_type.int1);
    println!("TestType int2: {}", test_type.int2);
}

Wie man leicht erkennt, handelt es sich bloß um eine einfache Funktion, die die übergebenen Paramter auf der Konsole ausgibt. Aber genau das ist es, was wir brauchen, um zu sehen, ob unser Programm richtig funktioniert. Zum weiteren Testen habe ich außerdem ein struct eingefügt.

Wichtig ist hier vor allem das pub extern, das angibt, dass diese Funktion von außerhalb genutzt werden soll, sowie #[no_mangle], das verhindert, dass die automatische, interne Namensänderung von Rust stattfindet.

Nun muss die Bibliothek noch kompiliert werden. Dies erledigt der Befehl cargo build, das Shared Object befindet sich danach im Ordner target/debug.

In der Rust-Dokumentation wird richtig erklärt, dass man natürlich eine C-Header-Datei benötigt, um auf die Funktionen der Rust-Bibliothek zugreifen zu können. Schreiben wir uns also so eine Header-Datei libvalatest.h:

#ifndef LIBVALATEST_H_
#define LIBVALATEST_H_

typedef struct TestType {
    int int1;
    unsigned char int2;
} TestType;

void test(int int1, long int2, unsigned char uint1, char text, unsigned int slice[], int slice_length, TestType *test_type);

#endif

Wie man sieht, ist auch dies relativ trivial. Es müssen lediglich die Rust-Typen auf ihre entsprechenden C-Typen umgemapped werden. Nun folgt der Standard-Vala-Weg, also das Schreiben des VAPIs libvalatest.vapi:

[CCode (cheader_filename = "libvalatest.h")]
namespace ValaTest {
    [CCode (cname = "TestType", has_type_id = false)]
    public struct TestType {
        public int int1;
        public uint8 int2;
    }

    [CCode (cname = "test")]
    public void test (int int1, int64 int2, uint8 uint1, char text, uint[] slice, TestType testType);
}

Wie dem aufmerksamen Betrachter nun vielleicht schon aufgefallen ist, hat die Funktion test in der C-Header-Datei einen zusätzlichen Parameter vom Typ int. Dieser wird von Vala automatisch eingefügt, wenn in einem VAPI vor einem Array kein [CCode (array_length = false)] steht. Und nach meiner Erfahrung in der Zusammenstellung dieses Beispiels sollte man das auch nicht tun. Fehlt die Länge des Arrays in der Funktion, läuft die Schleife for element in slice in Rust bis zum Erreichen eines Segmentation faults (ich vermute durch den gesamten dem Programm zur Verfügung stehenden Speicher). Warum das so ist, und wie Rust intern die Länge des Arrays aus dem Funktionsaufruf übernimmt kann ich nicht beantworten. Hier wäre jemand mit mehr Erfahrung in Rust wohl der bessere Ansprechpartner.

Nun folgt abschließend ein kleines Vala-Programm main.vala, dass die Rust-Funktion aufruft und den Datentyp nutzt:

using ValaTest;

void main (string[] args) {
    TestType testType = { 123456789, 255 };

    uint[] slice = { 1, 2, 255 };

    test (1, 2, 3, 'T', slice, testType);
}

Der Befehl zum Kompilieren ist diesmal ein wenig länger, da alle Komponenten nicht in Standardverzeichnissen liegen:

valac --vapidir=./ --pkg=libvalatest main.vala -X -I./ -X -L./target/debug -X -lvalatest

Zudem muss das Verzeichnis target/debug noch zu den Verzeichnissen hinzugefügt werden, in denen nach Shared Objects gesucht wird. Dies erledigt ein:

export LD_LIBRARY_PATH=./target/debug

Gestartet wird das Programm nun mit ./main und tatsächlich, auf der Konsole erscheint die erwartete Ausgabe:

int1: 1
int2: 2
uint1: 3
text: T
slice: 1
slice: 2
slice: 255
TestType int1: 123456789
TestType int2: 255

Es ist also sehr gut möglich, von Vala aus Rust zu nutzen, und ich denke, ich werde das in nächster Zeit auch mal tun. Im Laufe dessen werde ich hier natürlich weitere Anleitungen, zum Beispiel zu weiteren Typen wie String/str, veröffentlichen.

Bis dahin erst mal viel Spaß mit den neuen Möglichkeiten!