Biblioteka FFI - łączymy Ruby z C
18 komentarzy | Kategorie: C, JRuby, Ruby, Techblog | trackbackTagi: c extension ffi inline rozszerzenie ruby ruby/dl
Przez ostatnie dni miałem okazję zapoznać się z biblioteką FFI. (Swoją drogą, kenai.com, czyli strona na której jest hostowany projekt, to próba stworzenia przez firmę Sun systemu, podobnego do sourceforge, githuba itp. Całość zupełnie za darmo, o pełnych możliwościach można poczytać na stronie projektu. Aplikacja jest oparta o framework Ruby on Rails i uruchamiana przy pomocy JRubiego.) Biblioteka FFI służy do łatwego łączenia kodu Rubiego z bibliotekami C.
Rozszerzenia w C - różne podejścia
Zanim opiszę zalety i sposób wykorzystania FFI, chciałbym napisać kilka zdań na temat samych rozszerzeń C dla Rubiego. Otóż istnieje kilka możliwości w tej kwestii. Jedną z podstawowych jest wykorzystanie wewnętrznego API interpretera Rubiego napisanego w C (popularnie zwanego MRI lub cRuby). Jeśli zaglądałeś kiedykolwiek w kod źródłowy MRI i widziałeś źródła np. pliku array.c, to kod takiego rozszerzenia pisze się praktycznie tak samo. Z tego też powodu jesteśmy zmuszeni do poznania, choćby w minimalnym stopniu, wewnętrznego API C Rubiego (zobacz http://www.eqqon.com/index.php/Ruby_C_Extension).
Aby ułatwić choć trochę tworzenie takich rozszerzeń powstał projekt RubyInline. Jego dwoma największymi zaletami jest możliwość osadzania kodu bezpośrednio w kodzie Rubiego, a także automatyczna kompilacja takiego kodu dopiero w momencie odpalenia (skompilowane rozszerzenie jest zapisywane w katalogu ~/.ruby_inline/ by nie kompilować go za każdym uruchomieniem). Pozostałe zasady zostają takie same (ciągle operujemy na tym samym API).
~ ext_ruby_inline01.rbrequire "rubygems"
require "inline"
class MyMath
inline do |builder|
builder.c "
long add(int a, int b) {
return a + b;
}"
end
end
math = MyMath.new
puts math.add(100, 300)
Wyjście:400
Z projektów, które starają się rozwiązać podobne problemy, wymienię jeszcze SWIG (który potrafi generować kod rozszerzenia na podstawie plików nagłówkowych .h) i Rice (pozwala na wygodne mapowanie klas C++ na klasy Rubiego). Zainteresowanych odsyłam jednak na strony domowe.
Wszystkie wymienione biblioteki wymagają napisania łączącego kodu (tzw. glue code) w C oryginalnej biblioteki C i języka Ruby. Być może nie wszyscy wiedzą, ale wraz z Ruby dostajemy bibliotekę, która pozwala na dynamiczne wywoływanie kodu z, już zbudowanej, współdzielonej biblioteki (pliki o rozszerzeniach so, dll, dynalib w zależności od platformy). Jest to biblioteka Ruby/DL. Oto przykład jej wykorzystania.
require "dl/import"
module Libc
extend DL::Importable
dlload "/lib/libc.so.6"
extern "int strlen(const char *)"
end
puts Libc.strlen("foo")
Wyjście:3
Mam nadzieję, że jeszcze się nie niecierpliwisz, że piszę o wszystkim tylko nie o FFI. Rzecz jest jednak bardzo ważna, ponieważ chciałem zwrócić uwagę na pewną cechę, łączącą wyżej wymienione biblioteki - działają tylko z MRI (cRuby). Jeszcze do nie dawna nie było z tym problemów. Przy obecnym wysypie interpreterów Rubiego, fakt, że dana biblioteka działa tylko z jednym z nich, jest ogromną wadą. Pora zatem na przedstawienie tytułowego FFI, który jak się domyślacie, potrafi współpracować nie tylko z MRI.
FFI = Foreign Function Interface
Biblioteka ta pozwala na dynamiczne wywołanie metody z biblioteki współdzielonej, podobnie jak Ruby/DL, z tą różnicą, że na chwilę obecną dostępne są wersje działające z MRI 1.8/1.9, JRuby i Rubinius. Dodatkowo posiada bardzo ładne i wygodne API.
Oto prosty przykład użycia tej biblioteki.
~ ext_ruby_ffi01.rbrequire "rubygems"
require "ffi"
module Libc
extend FFI::Library
attach_function :strlen, [:string], :int
end
puts Libc.strlen("foo")
Wyjście:3
Jak już wspomniałem, FFI ma bardzo ładne API. Dotyczy to także callbacków (czyli w C wskaźniki do funkcji). Jak przystało na wersję w Rubym robimy to za pomocą bloków! Poniższy przykład wykorzystuje funkcję qsort ze standardowej biblioteki C, która do porównywania elementów tablicy wykorzystuje przekazany wskaźnik do funkcji.
require "rubygems"
require "ffi"
module Libc
extend FFI::Library
callback :cmp, [:pointer, :pointer], :int
attach_function :qsort, [:pointer, :int, :int, :cmp], :void
end
ARRAY_SIZE = 5
p = MemoryPointer.new(:int, ARRAY_SIZE)
p.put_array_of_int32(0, [10, 20, 3, 9, 5])
Libc.qsort(p, ARRAY_SIZE, 4) do |p1, p2|
p1.get_int32(0) <=> p2.get_int32(0)
end
puts p.get_array_of_int32(0, ARRAY_SIZE)
Wyjście:3
5
9
10
20
Mamy także możliwość bardzo łatwego mapowania struktur do klas Rubiego. Spróbujmy napisać własną bibliotekę dzieloną w C i użyć z poziomu Rubiego.
~ mylib.c#include <stdlib.h>
#include <stdio.h>
struct user_info {
char *name;
int age;
};
struct user_info *user_info_create() {
struct user_info *ui = malloc(sizeof(struct user_info));
ui->name = NULL;
ui->age = 0;
}
void user_info_free(struct user_info *ui) {
if (ui->name) {
free(ui->name);
}
free(ui);
}
void user_info_randomize_age(struct user_info *ui) {
ui->age = random() % 100;
}
void user_info_print_array(struct user_info **users, int count) {
struct user_info *ui;
int i;
for (i = 0; i < count; i++) {
ui = users[i];
printf("%s ma lat %d\n", ui->name ? ui->name : "(null)", ui->age);
}
}
Do skompilowania powyższego kodu (plik mylib.c) posłużyłem się poleceniem gcc -O2 -fPIC -shared -Wl,-soname,libsimplemath -o mylib.so mylib.c
Naszym zadaniem jest zmapowanie struktury user_info do klasy w Rubym. Zaprezentowany poniżej kod nie powinien sprawiać trudności, chociaż sposób operowania na klasie FFI::MemoryPointer może wydawać się z początku dosyć dziwny.
require "rubygems"
require "ffi"
module Mylib
extend FFI::Library
ffi_lib File.dirname(__FILE__) + "/mylib." + FFI::Platform::LIBSUFFIX
class UserInfo < FFI::Struct
layout :name, :pointer,
:age, :int
end
attach_function :user_info_create, [], :pointer
attach_function :user_info_free, [:pointer], :void
attach_function :user_info_randomize_age, [:pointer], :void
attach_function :user_info_print_array, [:pointer, :int], :void
end
users = []
5.times do |i|
p = Mylib.user_info_create()
ui = Mylib::UserInfo.new(p)
ui[:age] = rand(50) + 10
ui[:name] = FFI::MemoryPointer.from_string("foo%d" %i)
users << ui
end
users_ptr = FFI::MemoryPointer.new(:pointer, users.size)
users.each_with_index do |user, i|
users_ptr[i].put_pointer(0, user)
end
Mylib.user_info_print_array(users_ptr, users.size)
ui = users.first
puts "\n#{ui[:name].read_string} ma lat #{ui[:age]}"
Mylib.user_info_randomize_age(ui)
puts "#{ui[:name].read_string} ma teraz lat #{ui[:age]}"
Wyjście:foo0 ma lat 38
foo1 ma lat 14
foo2 ma lat 57
foo3 ma lat 52
foo4 ma lat 17
foo0 ma lat 38
foo0 ma teraz lat 83
Zagadnienia związane z FFI są zbyt obszerne (choćby kwestia zwalniania pamięci) żeby opisać je tu wszystkie, zatem na tym poprzestanę. Po więcej przykładów odsyłam na wiki projektu FFI oraz dodatkowe artykuły w języku angielskim (linki podam na końcu wpisu).
Wydajność
Do pisania takich rozszerzeń może zachęcić nas także wydajność, ale nie powinniśmy tego robić pochopnie (ktoś chętny na przepisanie railsów na C?;-)). Dla świętego spokoju zrobiłem mały (i zapewne naiwny) test wydajności. Jak zwykle w takim wypadku nie bierz tego zbyt dosłownie, zawsze dokonuj własnych pomiarów.
~ libfib.clong fib(int n) {
if (n < 2) {
return n;
} else {
return fib(n - 1) + fib(n - 2);
}
}
~ ext_ruby_ffi04.rb
require "rubygems"
require "ffi"
require "inline"
class MylibFFI
extend FFI::Library
ffi_lib File.dirname(__FILE__) + "/libfib." + FFI::Platform::LIBSUFFIX
attach_function :fib, [:int], :long
end
class MylibRuby
def self.fib(n)
if n < 2
return n
else
return fib(n - 1) + fib(n - 2)
end
end
end
class MylibInline
inline do |builder|
builder.prefix %Q{
long _fib(int n) {
if (n < 2) {
return n;
} else {
return _fib(n - 1) + _fib(n - 2);
}
}
}
builder.c_raw_singleton %Q{
VALUE fib(int argc, VALUE *args, VALUE self) {
if (argc != 1) {
rb_raise(rb_eArgError, "1 argument expected");
}
VALUE n = args[0];
return LONG2NUM(_fib(NUM2INT(n)));
}
}
end
end
require "benchmark"
LOOP = 100000
n = 10
Benchmark.bmbm(15) do |make|
puts "n = #{n}"
make.report("pure ruby") do
LOOP.times do
MylibRuby.fib(n)
end
end
make.report("ruby + inline") do
LOOP.times do
MylibInline.fib(n)
end
end
make.report("ruby + FFI") do
LOOP.times do
MylibFFI.fib(n)
end
end
end
Wyjście:
user system total real
pure ruby 17.200000 5.690000 22.890000 ( 23.281636)
ruby + inline 0.130000 0.020000 0.150000 ( 0.147392)
ruby + FFI 0.150000 0.010000 0.160000 ( 0.171384)
Jak widać nie powinno być dużej różnicy między klasycznym rozszerzeniem a takim napisanym przy pomocy FFI. Różnica ta jeszcze bardziej będzie się zacierać im więcej czasu upłynie bezpośrednio na poziomie kodu C.
Dla kogo FFI?
Chyba nie trzeba przekonywać nikogo o korzyściach płynących z wykorzystania tej biblioteki, zamiast jednego z wymienionych na początku wpisu sposobów. Brak dodatkowej kompilacji, krótszy kod (który najczęściej w rozszerzeniach polegał na przekształcaniu typów między C a Ruby), przenośność między różnymi interpreterami Rubiego, niezależność od wewnętrznego API interpretera (ile rozszerzeń w C działały bez zmian w ruby 1.9?) to jego główne atuty. Jeśli kiedykolwiek przyjdzie Ci napisać takie rozszerzenie to w pierwszej kolejności wypróbuj FFI.
Linki.
- Strona projektu FFI (polecam przejrzeć źródła, w szczególności "speki")
- Przykłady na wiki FFI
- Bridging MRI, JRuby & Rubinius with FFI
- FFI for Ruby Now Available
- Rubinius' Foreign Function Interface
- On the Rubinius FFI - bardzo obszerny artykuł, polecam!
- Ruby FFI Brings Native Library Access to JRuby, MRI
Jedna uwaga. Jeśli mamy jakoś stałą która jest gwarantowana przez POSIX że istnieje ale nie wiemy ile ona wynosi to chyba nadal pozostaje nam PORI (Plain Old Ruby-Inline).
Zacytuję fragment własnego bloga
(tomash.wrug.eu)
Także nie, nie uważam żeby celowanie w zgodność z JRuby czy Rubiniusem było naprawdę tego warte. Zwłaszcza że rozchodzi się głównie o pisanie interfejsu, middlemana pomiędzy Rubym a już istniejącą i działającą biblioteką C.
Radarek, czekam na Twoją recenzję D i perspektywę pisania z użyciem RuDy ;)
Poza technicznymi brukowcami warto jeszcze śledzić bezpośrednie źródła, czyli np blogi developerów Rubiniusa ;) Ja nie śledziłem rok, miałem podobne zdanie co do Rubiniusa jak w załączonym cytacie, po oczytaniu, zmieniłem zdanie ;) Poczytaj co dzieje sie w Rubiniusie.
Rubinius zaczął jakoś równo z JRuby, a wciąż nie odpala Railsów. Poza korzyściami „formalnymi” z jego powstawania (specyfikacja języka itp.) nie widzę żadnych powalających zalet tego projektu (JRuby chociaż ma wydajność i integrację z javowymi bibliotekami). Także osobiście wolałbym, żeby ta ekipa (Ezra, Yehuda itd.) skupiła się na Merbie niż na implementacji rubiego nieniosącej ze sobą konkretniejszych korzyści.
InfoQ i Techcruncha nie czytam, więc wypraszam sobie fragment o technicznych brukowcach ;)
No, przedewszystkim nie istnieje coś takiego jak JRails ;-) albo RRails :P (Rubinius Rails), Rails nie stanowi mainstreamu świata ruby. To Rails jest pisane w Ruby a nie odwrotnie. Więc to że obecnie Rubinius nie odpala Rails oznacza tylko tyle że Rubinius nie stanowi alternatywy dla MRI dziś.
Natomiast jeśli chodzi o ocenianie tego co Ezra i Yehuda planuje w stosunku do Rails, to tutaj też uważam że bardzo się mylisz w ocenie tego. W dużym uproszczeniu rails i merb to frameworki MVC, tylko że Merb skupia uwage głównie na ostatniej literze tego skrótu. Rails to cały pakiet out of box, rozwijany przez ponad 6 lat? Stanowi znacznie dojrzalsze i bogatsze rozwiązanie od Merba, uznali że szybciej będzie zaimplementować teraz w Rails lepiej wszystko to co osiągneli w Merbie, niż przez najbliższe 2-3 lata próbować nadgonić Rails mając znikome środki ludzkie na to. Pamietaj że to wszystko jest napędzane przez siłe programistów commitujących do projektu, a nie będących core tego projektu. To tyle. rozpisuje sie już zabardzo.
Chodziło mi bardziej o to że powołujesz sie tylko na jeden z faktów pomijasz aktualny stan rubiniusa i to jakie możliwości ma ten projekt.
Nie neguję możliwości i potencjału Rubiniusa. Tylko wiesz, to jak ze Stingiem: od dwudziestu lat się świetnie zapowiada, a póki co robi po prostu słabą muzykę ;)
I o ile widziałem realną potrzebę powstania JRuby i stojące za nim przesłanki, o tyle realnej potrzeby powstania/rozwijania Rubiniusa nie widzę.
Railsy są „tylko jedną z aplikacji” w Ruby, ale z racji swojej popularności (większość pracy dla rubiowców to praca w rails) i maksymalnego wyżyłowania możliwości rubiego, są praktycznie „ultimate” celem każdej implementacji rubiego.
Tomash, mam wrażenie, że patrzysz z bardzo wąskiej perspektywy. MRI jest i będzie, przynajmniej przez najbliższe kilka lat, wiodącą implementacją Rubiego. I to jest normalne, zawsze jedna z implementacji będzie zgarniać ~90% „rynku”. Każda z implementacji ma swój konkretny cel, ale nie wydaje mi się, żeby jakakolwiek miała na celu zastąpienie MRI. Nawet Evan Phoenix zapytany czy Rubinius w przyszłości zastąpi MRI, odpowiedział że nie wie, ale na pewno nie taki jest jego cel.
Jakikolwiek rozkład popularności by nie był, to każda z tych implementacji będzie posiadać miejsca, w których będzie używana i w których żadna inna nie będzie mogła zostać użyta. Np. firma X przez lata wypracowała sposób odpalania aplikacje na jvm i nie chce poza to środowisko wyjść.
Chyba nie trzeba tłumaczyć jak wielkie korzyści mogą płynąć z tego, że ruby może być odpalany na jvm lub .net? Zwiększa się ekosystem, community itp.
Rubinius z kolei to próba stworzenia VM Rubiego, który będzie posiadać jak najmniej natywnego kodu w C/C++, a jak najwięcej w Ruby. Do tego dołóżmy fakt, że implementacja była tworzona od podstaw, a więc Evan mógł uczyć się na błędach Matza i spółki. Czego dowodem może być łatwość zmian, których już dokonywał nie raz. Dlaczego GC Rubiego nie jest zmieniane od tylu lat? Bo nie są w stanie tego zrobić (bo np. wszystkie extensiony przestałyby działać itp).
Bo patrzysz na teraźniejszość, a nie na przyszłość. Nawet Koichi SASADA (twórca YARV) wyraził opinię w swojej prezentacji na rubyconf 2008, że Rubinius jest projektem dającym największą nadzieję na dobrą przyszłość Rubiego. Po pierwsze Rubinius jest jedyną implementacją pisaną od podstaw. Każda inna bazuje już na gotowym VM, więc nie musi się np. męczyć z implementacją GC, wielowątkowością itp. Trzeba też zdawać sobie sprawę, że taki VM nie powstaje w rok, dwa. To nie jest ot taki sobie projekcik. Dlatego nie wymagaj, żeby już teraz Rubinius był gotowy na produkcyjne środowisko. Pewnie i za rok nie będzie.
Odnośnie jeszcze FFI.
No i właśnie odpowiedziałeś na pytanie dlaczego (odrzucając nawet argument zgodności FFI z innymi interpreterami) warto używać FFI. W większości chodzi o glue code. Nie wiem jak Ty, ale ja wolę napisać:
niż
I do tego jeszcze kompilacja.
A to i tak najprostszy przypadek. FFI zostało przyjęte bardzo dobrze przez programistów i dziwię się Twojemu podejściu…
Nie napisałem o najważniejszym.
Wspomniałem o różnych implementacjach, wspomniałem o tym, że każda z nich ma swoje miejsce.
Sęk w tym, żeby te różne implementacji nie podzieliły środowiska Rubiego, bo w końcu język jest ciągle ten sam. Chodzi o to, żeby nie było takich sytuacji, że autor danej biblioteki musiał napisać w README „ta biblioteka działa tylko z implementacją X”, bo to tylko wprowadza podział. Podział, którego trzeba unikać. I FFI pięknie się w tą ideę wpasowuje. Czy takie argumenty przemawiają do Ciebie Tomashu?:)
Było by fajnie jakbyś gdzięś udostępnił na stornie małą podpowiedź do tego jak formatować wpisy :) Bo za każdym razem jak próbuje :P to mi nie wychodzi.
@Radarek: dobra, przekonałeś mnie :) W takim razie szkoda, że EngineYard musiało zwolnić pracujących nad Rubiniusem programistów.
@Paweł: pod okienkiem komentarza masz link do składni Textile, z której możesz korzystać.
Chociaż nie ukrywam, że wolałbym jak w Pythonie: góra dwa interpretery (domyślny w C oraz Jython), więc pisząc rozszerzenie nie trzeba korzystać z jakichś kosmicznych obejść typu FFI (zupełnie mnie nie przekonuje kod, który pokazałeś), tylko można napisać rozszerzenie w dwóch językach (C i Java) i cacy.
Tak zresztą są napisane np. hpricot i mongrel i chciałbym, żeby wciąż tak było. Rubinius przecież może (co myślę że nastąpi i jest tylko kwestią czasu) udostępniać API w 100% zgodne z API MRI :)
@Tomash: Przeczytaj – API MRI utrudnia pracę nad GC. A FFI nie jest ‘kosmiczne’ tylko jest dosyć wygodne. ‘Bezpośrednio’ wywołujesz funkcję C bez konieczności pisania wrappera do niej. Dzięki temu nie używamy 3 języków (C, Java i Ruby) tylko jednego. Przenośność to dodatkowy bonus.
Nie tyle API MRI, co jego implementacja w MRI — taka subtelna różnica, ale tak naprawdę te udostępniane funkcje są całkiem sensownym zestawem (chodzi mi o nazwy i parametry – Python ma podobne API), po prostu pytanie jak bardzo „na surowo” są wywoływane przez interpreter (bez opakowywania itd.).
te wyniki na wyjściu są na ruby 1.8?
jeżeli tak to są g … warte w kontekście ruby 1.9 !!!
W teście nie chodziło o dokładne porównanie prędkości, raczej o pokazanie o ile rzędów kod w C będzie szybszy. Ruby1.9 nie zmienia w tej sytuacji aż tak bardzo.
Cześć. Od jakiegoś czasu przeglądam twojego bloga i chyba wybiorę ten język. Niestety dzisiaj trafiłam na taki komentarz i już sama nie wiem czego użyć to tworzenia dynamicznych stron www:(
"ja mam same nieprzyjemne wspomnienia w kwestii ruby <.< wiecej zabawy niz z takim pythonem, skladnia tez jakos nie powala. Ogolnie ruby to takie cos dla masochistow a to cale zachwalane gem ostatnio mi sie wywalilo i musialem na google szukac sposobow zeby to naprawic oO w pythonie przynajmniej nic samo z siebie sie nie wywala <.<"Hey, kolejny swietny artykul, faktycznie uzywanie ffi jest duzo latwiejsze niz pisanie rozszerzen w C.
Dla zainteresowanych polecam ta prezentacje o FFI
Mountain West Ruby Conference 2009 – FFI – Jeremy Hinegardner
bardzo dobry artykul, musze sie przyjrzec tej bibliotece