Na předchozí článek navážeme, protože se budeme zabývat dalšími detaily technologie FFI. Řekneme si, jak sdílet pole s nativními knihovnami, a taky si ukážeme nastavení projektu spravovaného systémem Cargo při použití FFI.
Na předchozí článek navážeme, protože se budeme zabývat dalšími detaily technologie FFI. Řekneme si, jak sdílet pole s nativními knihovnami, a taky si ukážeme nastavení projektu spravovaného systémem Cargo při použití FFI.
2. Vypsání adresy ukazatele na straně Rustu i céčka
3. Předání adresy pole do funkce v nativní knihovně
4. Druhý demonstrační příklad – výpočet sumy prvků pole
5. Nedetekovaný problém při předání datové struktury přes ukazatel
6. Použití nástroje Cargo při práci s projekty používajícími nativní knihovny
7. Nastavení projektu spravovaného nástrojem Cargo
9. Změny, které je nutné provést ve zdrojových kódech
10. Překlad a spuštění projektu
11. Zjednodušení skriptu pro překlad s použitím pluginu gcc
13. Repositář s demonstračními příklady
Již v úvodních částech seriálu o programovacím jazyku Rust jsme si řekli, že textové řetězce jsou interně ukládány s využitím kódování UTF-8, což sice může znít překvapivě, ovšem přináší to i některé výhody. Autoři tohoto jazyka správně poukazují na to, že v současnosti prakticky všechny webové služby, XML soubory, JSON soubory, velká část HTML stránek atd. stejně již kódování UTF-8 standardně používají, takže nemá význam neustále provádět konverzi mezi tímto kódováním a například UCS-4 (UTF-32). Navíc je při zpracování rozsáhlých XML souborů formát UTF-8 výhodnější z hlediska spotřeby operační paměti. Největší nevýhodou použití UTF-8 je nemožnost získat a vrátit n-tý znak v řetězci v konstantním čase (a na některých architekturách též obtížná práce s jednotlivými bajty). Pokud by se operace pro získání n-tého znaku prováděla velmi často, lze samozřejmě použít vhodný objekt, který například „obaluje“ pole čtyřbajtových širokých znaků reprezentovaných v UCS-4/UTF-32.
V demonstračním příkladu popsaném v navazující kapitole bude použit řetězec obsahující znaky s českými akcenty, takže z výpisu bude patrné, jakým způsobem jsou tyto znaky reprezentovány i na straně céčkového programu (resp. na straně nativní knihovny).
V předchozí části seriálu o Rustu jsme si řekli, že řetězce se do volaných nativních knihoven předávají přes ukazatel. To mj. znamená, že vlastní hodnota uložená v proměnné typu ukazatel na straně Rustu bude shodná s adresou uloženou v parametru typu char * či const char * na straně céčka. Toto tvrzení si můžeme snadno ověřit, protože adresu samozřejmě můžeme vypsat jak v céčkovém programu, tak i v té části, která je naprogramovaná v Rustu. Céčková část aplikace bude vypadat například takto (jak je patrné, stále budeme volat funkci vracející délku řetězce v bajtech):
#include <stdint.h> #include <stdio.h> int32_t string_length(const char *str) { printf("C side pointer: %p\n", str); int32_t len = 0; for (; *str; str++, len++) ; return len; }
V té části aplikace, která je naprogramovaná v Rustu, získáme ukazatel s využitím metody CString.as_ptr() a ihned poté můžeme vypsat jeho hodnotu (tedy adresu):
fn test(string :&'static str) { match CString::new(string) { Ok(string) => { let pointer = string.as_ptr(); println!("Rust side pointer: {:p}", pointer); unsafe { println!("string length = {}", string_length(pointer)); } } Err(error) => { println!("CString::new() error: {:?}", error); } } }
Pro zjištění, jak přesně se chovají znaky s akcenty v řetězcích, budeme funkci test() volat takto:
test("Hello world!"); test(""); test("ěščřžýáíé");
Celá rustovská část zdrojového kódu vypadá takto:
use std::ffi::CString; use std::os::raw::c_char; #[link(name = "ffi4")] extern { fn string_length(str: *const c_char) -> i32; } fn test(string :&'static str) { match CString::new(string) { Ok(string) => { let pointer = string.as_ptr(); println!("Rust side pointer: {:p}", pointer); unsafe { println!("string length = {}", string_length(pointer)); } } Err(error) => { println!("CString::new() error: {:?}", error); } } } fn main() { test("Hello world!"); test(""); test("ěščřžýáíé"); }
Pro překlad obou částí aplikace a pro její následné spuštění opět použijeme pomocný soubor Makefile (ale níže si vysvětlíme lepší způsob spočívající ve využití projektu Cargo):
CC=gcc CFLAGS=-Wall -ansi -fPIC RUSTC=rustc APP=ffi4 PROGNAME=$(APP) LIBNAME=lib$(APP).so RUST_SRC=$(APP).rs LIB_SRC=$(APP).c OBJ_FILE=$(APP).o # Vychozi pravidlo pro vytvoreni vysledne spustitelne aplikace. all: $(LIBNAME) $(PROGNAME) # Pravidlo pro slinkovani vsech objektovych souboru a vytvoreni # vysledne spustitelne aplikace. $(PROGNAME): $(RUST_SRC) $(LIBNAME) $(RUSTC) -L . $< # Pravidlo pro preklad knihovny $(LIBNAME): $(OBJ_FILE) $(CC) -shared -Wl,-soname,$(LIBNAME) -o $(LIBNAME) $< %.o: %.c $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ clean: rm -f *.o rm -f *.so rm -f $(PROGNAME) run: export LD_LIBRARY_PATH=.;./$(APP)
Po překladu a spuštění tohoto příkladu můžeme získat například následující výstup. Konkrétní adresy řetězce se samozřejmě mohou odlišovat, obě však budou shodné (na straně Rustu i na straně nativní knihovny), protože se skutečně předává adresa řetězce a nikoli jeho obsah:
Rust side pointer: 0x2b8e67a26000 C side pointer: 0x2b8e67a26000 string length = 12 Rust side pointer: 0x2b8e67a1d020 C side pointer: 0x2b8e67a1d020 string length = 0 Rust side pointer: 0x2b8e67a1c0a0 C side pointer: 0x2b8e67a1c0a0 string length = 18
Povšimněte si, že vypočtená délka řetězce „ěščřžýáíé“ je osmnáct bajtů, zatímco samotný řetězec má jen devět znaků. Je to samozřejmě způsobeno použitým kódováním znaků. Malou úpravou příkladu lze kódy znaků snadno získat:
#include <stdio.h> int32_t string_length(const char *str) { printf("C side pointer: %p\n", str); int32_t len = 0; for (; *str; str++, len++) printf("%02x ", (unsigned char)*str); ; return len; }
S tímto výsledkem:
Rust side pointer: 0x2b3d2be26000 C side pointer: 0x2b3d2be26000 48 65 6c 6c 6f 20 77 6f 72 6c 64 21 string length = 12 Rust side pointer: 0x2b3d2be1d020 C side pointer: 0x2b3d2be1d020 string length = 0 Rust side pointer: 0x2b3d2be1c0a0 C side pointer: 0x2b3d2be1c0a0 c4 9b c5 a1 c4 8d c5 99 c5 be c3 bd c3 a1 c3 ad c3 a9 string length = 18
Poznámka: ve skutečnosti není příliš dobré používat standardní výstup společně na straně C a Rustu, protože se kvůli bufferování výstupu mohou řádky promíchat.
Nyní si ukažme, jakým způsobem se do nativní funkce předá pole, konkrétně pole prvků typu f32. Již na začátku si však musíme uvědomit, že mezi poli v Rustu a céčku existuje mnoho rozdílů – pole v Rustu nejvíce odpovídají céčkovým polím s délkou známou překladači (vytvořenými přes []), zatímco dynamicky vytvořeným céčkovým polím ideově odpovídají řezy (slices), u nichž není délka známá v době překladu.
V předchozí části tohoto seriálu jsme si uvedli tabulku s převodem základních (primitivních) datových typů jazyka Rust a odpovídajících typů v céčku. Nyní si do této tabulky přidáme i informace o datových typech určených pro reprezentaci číselných hodnot reprezentovaných v systému plovoucí řádové čárky (floating point). V Rustu se jedná o typy f32 a f64, na straně céčka pak o typy float a double:
Typ v C | Typ v Rustu | Význam |
---|---|---|
int8_t | i8 | celé číslo se znaménkem (signed) o šířce 8 bitů |
int16_t | i16 | celé číslo se znaménkem (signed) o šířce 16 bitů |
int32_t | i32 | celé číslo se znaménkem (signed) o šířce 32 bitů |
int64_t | i64 | celé číslo se znaménkem (signed) o šířce 64 bitů |
uint8_t | u8 | celé číslo bez znaménka (unsigned) o šířce 8 bitů |
uint16_t | u16 | celé číslo bez znaménka (unsigned) o šířce 16 bitů |
uint32_t | u32 | celé číslo bez znaménka (unsigned) o šířce 32 bitů |
uint64_t | u64 | celé číslo bez znaménka (unsigned) o šířce 64 bitů |
float | f32 | numerická hodnota s plovoucí řádovou čárkou podle IEEE 754 s jednoduchou přesností |
double | f64 | numerická hodnota s plovoucí řádovou čárkou podle IEEE 754 s dvojitou přesností |
Pole prvků typu f32 budeme předávat, podobně jako tomu bylo u řetězců, přes ukazatel. Jelikož se bude předávat pouze „datová část“ pole bez dalších atributů, musíme všechny další informace nativní funkci předávat explicitně. Jedinou další požadovanou informací bude délka pole (u řetězců byla tato délka stanovena nepřímo vzdáleností bajtu s kódem nula od začátku řetězce). Námi vytvořená nativní funkce spočítá součet všech prvků pole; funkci budeme explicitně předávat délku i ukazatel na první prvek v poli:
#include <stdint.h> #include <stdio.h> float sum(uint32_t len, const float *array) { float s = 0.0f; int i; for (i=0; i < len; i++) { s += array[i]; } return s; }
Poznámka: ve skutečnosti není vložení hlavičkového souboru stdio.h v tomto zdrojovém kódu nutné, pokud si ovšem opět nebudete chtít vypsat konkrétní předané parametry.
V rustovské části aplikace je nejprve nutné správně deklarovat hlavičku volané nativní funkce. Použijeme přitom typy std::os::raw::c_float a std::os::raw::c_uint:
#[link(name = "ffi5")] extern { fn sum(len: c_uint, arr: *const c_float) -> c_float; }
Dále pro dané pole získáme ukazatel s využitím metody as_ptr(), vypočteme délku pole metodou len() a tyto údaje předáme nativní funkci. Celá část, v níž se pracuje s ukazatelem, bude vložena do bloku unsafe:
let array : [f32; 5] = [1.0, 2.0, 3.0, 4.0, 5.0]; unsafe { let pointer = array.as_ptr(); let len : u32 = array.len() as u32; let s = sum(len, pointer); println!("sum = {}", s); }
Celý zdrojový kód může vypadat následovně:
use std::os::raw::c_float; use std::os::raw::c_uint; #[link(name = "ffi5")] extern { fn sum(len: c_uint, arr: *const c_float) -> c_float; } fn main() { let array : [f32; 5] = [1.0, 2.0, 3.0, 4.0, 5.0]; unsafe { let pointer = array.as_ptr(); let len : u32 = array.len() as u32; let s = sum(len, pointer); println!("sum = {}", s); } }
Po překladu a spuštění dostaneme lakonickou zprávu o součtu prvků pole:
sum = 15
Pokud se pokusíme přeložit tento program:
fn main() { let array : [f32; 5] = [1.0, 2.0, 3.0, 4.0, 5.0]; for i in 1..6 { array[i] = 0.0; } }
vypíše překladač chybové hlášení, protože se pokoušíme o zápis do pole, které je neměnitelné (immutable):
error: cannot assign to immutable indexed content `array[..]` --> ffi5.rs:12:9 | 12 | array[i] = 0.0; | ^^^^^^^^^^^^^^ error: aborting due to previous error
Ve skutečnosti se ovšem jedná „pouze“ o omezení kladená překladačem. Pokud například upravíme nativní část aplikace tak, aby funkce kromě součtu prvků ještě prvky pole postupně vynulovala, nebude to překladačem zachyceno (ostatně do funkce se předává pouhý ukazatel):
#include <stdint.h> #include <stdio.h> float sum(uint32_t len, float *array) { float s = 0.0f; int i; for (i=0; i < len; i++) { s += array[i]; array[i] = 0.0f; } return s; }
Rustovská část aplikace bude vypadat shodně s předchozím demonstračním příkladem:
use std::os::raw::c_float; use std::os::raw::c_uint; #[link(name = "ffi6")] extern { fn sum(len: c_uint, arr: *const c_float) -> c_float; } fn main() { let array : [f32; 5] = [1.0, 2.0, 3.0, 4.0, 5.0]; println!("array: {:?}", array); unsafe { let pointer = array.as_ptr(); let len : u32 = array.len() as u32; let s = sum(len, pointer); println!("sum = {}", s); } println!("array: {:?}", array); }
Pokud tento program spustíte, vypíše s velkou pravděpodobností následující výsledek:
array: [1, 2, 3, 4, 5] sum = 15 array: [0, 0, 0, 0, 0]
Povšimněte si, že nativní funkce změnila hodnoty zapsané v poli, i když je pole zdánlivě neměnitelné! Navíc může nastat další problém – prvky pole mohou být uloženy v kódovém segmentu a při pokusu o jeho modifikaci dojde k pádu programu (což by bylo zcela korektní chování). Jinými slovy – při použití FFI ztrácíme poměrně velkou část výhod Rustu spočívající v přísných kontrolách prováděných překladačem.
Použití souborů Makefile nebo dokonce shellových skriptů určených pro překlad a slinkování aplikace složené z céčkové části a rustovské části je poměrně nešikovné a v praxi se s ním příliš často nesetkáme. Mnohem častěji se používá nástroj Cargo, který mj. podporuje i tvorbu vlastních build skriptů.
Zkusme si tedy nejprve vytvořit prázdný projekt spravovaný systémem Cargo. Projekt bude sloužit k vytvoření spustitelné binární aplikace a proto musíme použít přepínač –bin:
cargo new --bin ffi1
Po spuštění předchozího příkazu získáme následující adresářovou strukturu projektu:
. ├── Cargo.toml └── src └── main.rs
Do projektu přesuneme soubor ffi1.c (s funkcí pro součet dvou celých čísel) a umístíme ho do podadresáře src. Navíc vytvoříme (například pomocí touch) soubor nazvaný build.rs, který bude umístěn ve stejném adresáři, v jakém se nachází projektový soubor Cargo.toml. Výsledek by měl vypadat následovně:
. ├── build.rs ├── Cargo.toml └── src ├── ffi1.c └── main.rs
Projektový soubor Cargo.toml v mém případě vypadal následovně (ve vašem případě se samozřejmě bude lišit řádek authors, ovšem to není v tuto chvíli příliš důležité):
[package] name = "ffi_project1" version = "0.1.0" authors = ["Pavel Tisnovsky <ptisnovs@redhat.com>"] [dependencies]
Do projektového souboru musíme přidat další direktivu, která říká, že se před překladem a před linkováním projektu má spustit i takzvaný build skript (ve skutečnosti to skript není, protože se jedná o zdrojový kód psaný v Rustu a překládaný do strojového kódu). Přidaný řádek je zvýrazněn:
[package] name = "ffi_project1" version = "0.1.0" authors = ["Pavel Tisnovsky <ptisnovs@redhat.com>"] build = "build.rs" [dependencies]
Tento řádek má vliv jen na průběh překladu, nikoli na spouštění samotné aplikace.
Samotný soubor build.rs vlastně představuje zdrojový kód běžného programu napsaného v Rustu, který ovšem přes svůj standardní výstup může předávat parametry překladači Rustu (typicky cesty ke knihovnám atd.). Úkolem tohoto souboru bude provést tři kroky, které jsme předtím implementovali přes soubor Makefile nebo jednoduchými shellovými skripty:
gcc -c src/ffi1.c -fPIC -o ffi1.o ar rcsu libffi1.a ffi1.o
Připomeňme si, že z Rustu je možné relativně snadno spouštět nové procesy a předávat jim parametry. Právě tuto funkci nyní využijeme pro napodobení předchozího shell skriptu. Nejprve zjistíme jméno adresáře, kam se ukládají výsledné binární soubory (tento adresář může být odlišný podle typu překladu):
let out_dir = env::var("OUT_DIR").unwrap();
Následně spustíme překladač gcc a utilitu pro práci se statickými knihovnami ar, samozřejmě s předáním správných parametrů:
// preklad Command::new("gcc").args(&["src/ffi1.c", "-c", "-fPIC", "-o"]) .arg(&format!("{}/ffi1.o", out_dir)) .status().unwrap(); // import objektoveho souboru do staticke knihovny Command::new("ar").args(&["crus", "libffi1.a", "ffi1.o"]) .current_dir(&Path::new(&out_dir)) .status().unwrap();
Posledním krokem je předání parametrů překladači a linkeru. To se provádí poměrně elegantním způsobem přes standardní výstup – každý řádek vypisovaný aplikací build začínající textem „cargo:“ je zpracován, rozpoznán, převeden na správný přepínač a předán překladači a linkeru Rustu. Většina možných voleb je popsána na stránce http://doc.crates.io/build-script.html#outputs-of-the-build-script, nás však dnes bude zajímat prakticky jen volba rustc-link-lib=[KIND=]NAME, která je transformována do přepínače -l, s nímž jsme se již seznámili (za KIND lze doplnit static nebo dynlib) a volba ustc-link-search= transformovaná do přepínače -L (cesta ke knihovnám):
// predame prekladaci parametry println!("cargo:rustc-link-search=native={}", out_dir); println!("cargo:rustc-link-lib=static=ffi1");
Úplná podoba souboru build.rs je tedy následující:
// build.rs use std::process::Command; use std::env; use std::path::Path; fn main() { let out_dir = env::var("OUT_DIR").unwrap(); // preklad Command::new("gcc").args(&["src/ffi1.c", "-c", "-fPIC", "-o"]) .arg(&format!("{}/ffi1.o", out_dir)) .status().unwrap(); // import objektoveho souboru do staticke knihovny Command::new("ar").args(&["crus", "libffi1.a", "ffi1.o"]) .current_dir(&Path::new(&out_dir)) .status().unwrap(); // predame prekladaci parametry println!("cargo:rustc-link-search=native={}", out_dir); println!("cargo:rustc-link-lib=static=ffi1"); }
Další změnu je zapotřebí provést ve zdrojových kódech. Netýká se to ovšem céčkové části, která zůstává stále stejná (ostatně zde není k žádné změně důvod):
#include <stdint.h> int32_t add(int32_t x, int32_t y) { return x+y; }
V rustovské části aplikace je nutné odstranit řádek:
#[link(name = "ffi1", kind= "static")]
Pokud by tento řádek ve zdrojovém kódu zůstal, došlo by k pokusu o slinkování knihovny dvakrát, což by vedlo k chybě při kompilaci projektu:
$ cargo build Compiling ffi_project1 v0.1.0 (file:///home/tester/ffi_project1) error: linking with `cc` failed: exit code: 1 | = note: "cc" "-Wl,--as-needed" ...vynecháno... = note: /home/tester/ffi_project1/target/debug/build/ffi_project1-14178b3c9909eb85/out/libffi1.a(ffi1.o): In function `add': ffi1.c:(.text+0x0): multiple definition of `add' /home/tester/ffi_project1/target/debug/build/ffi_project1-14178b3c9909eb85/out/libffi1.a(ffi1.o):ffi1.c:(.text+0x0): first defined here collect2: error: ld returned 1 exit status error: aborting due to previous error error: Could not compile `ffi_project1`. To learn more, run the command again with --verbose.
Nová podoba rustovské části se tedy ve skutečnosti nepatrně zjednodušila:
extern { fn add(x:i32, y:i32) -> i32; } fn main() { let x:i32 = 1; let y:i32 = 2; let z = unsafe { add(x, y) }; println!("{} + {} = {}", x, y, z); }
Po výše provedených úpravách je překlad a slinkování projektu velmi snadné a zajistí ho příkaz:
cargo build Compiling ffi_project1 v0.1.0 (file:///home/tester/ffi_project1) Finished debug [unoptimized + debuginfo] target(s) in 1.1 secs
Spuštění se provede stejně snadno:
cargo run Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/ffi_project1` 1 + 2 = 3
Překlad do finální podoby (s optimalizacemi atd.):
cargo build --release Compiling ffi_project1 v0.1.0 (file:///home/tester/ffi_project1) Finished release [optimized] target(s) in 1.10 secs
Build skript z předchozího projektu explicitně volal překladač céčka a utilitu ar, což je ovšem poměrně špatný přístup k řešení problému (prakticky stejně špatný jako naše soubory Makefile), a to z toho důvodu, že zbytečně explicitně určujeme, jaký překladač se má volat, jaké se mu mají předávat parametry atd. To ovšem vede k tvorbě potenciálně nepřenositelných zdrojových kódů. Mnohem lepší je použít modul (v systému Cargo se jim říká crate) http://alexcrichton.com/gcc-rs/gcc/index.html. Jméno tohoto modulu je mírně zavádějící, protože může podporovat i další typy překladačů. Vzhledem k tomu, že je tento modul používán pro sestavení aplikace, přidává se do sekce [build-dependencies] a nikoli [dependencies]:
[build-dependencies] gcc = "0.3"
Do projektového souboru Cargo.toml je zapotřebí přidat dva zvýrazněné řádky:
name = "ffi_project2" version = "0.1.0" authors = ["Pavel Tisnovsky <ptisnovs@redhat.com>"] build = "build.rs" [dependencies] [build-dependencies] gcc = "0.3"
Musíme upravit i soubor build.rs, který se nyní zjednoduší, protože bude ve funkci main obsahovat jen jediný příkaz – volání funkce gcc::compile_library(). Tato funkce již zajistí vše ostatní, tedy i předání parametrů překladači a linkeru Rustu:
// balicek gcc zajisti preklad a vytvoreni knihovny za nas extern crate gcc; fn main() { // preklad a vytvoreni knihovny gcc::compile_library("libffi2.a", &["src/ffi2.c"]); }
Překlad se nemění:
cargo build Compiling gcc v0.3.50 Compiling ffi_project2 v0.1.0 (file:///home/tester/temp/presentations/rust/ffi/ffi_project2) Finished debug [unoptimized + debuginfo] target(s) in 3.42 secs
Spuštění bude taktéž stejné:
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/ffi_project2` 1 + 2 = 3
Všechny dnes popisované demonstrační příklady a podpůrné skripty byly, ostatně podobně jako ve všech předchozích částech tohoto seriálu, uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/presentations. Demonstrační příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý repositář (ovšem u projektů je lepší mít celý repositář, abyste nemuseli pracně stahovat všechny potřebné soubory):
Pavel Tišnovský vystudoval VUT FIT a v současné době pracuje ve společnosti Red Hat, kde vyvíjí nástroje pro OpenShift.io.
Informace nejen ze světa Linuxu. ISSN 1212-8309
Copyright © 1998 – 2018 Internet Info, s.r.o. Všechna práva vyhrazena. Powered by Linux.
Při poskytování služeb nám pomáhají cookies. Používáním webu s tím vyjadřujete souhlas.