Matlab engine
—
Silnik Matlaba (Matlab engine) jest zbiorem współdzielonych bibliotek, które umożliwiają komunikację między silnikiem obliczeniowym Matlaba a zewnętrznymi programami napisanymi w C/C++ i Fortranie. W przeciwieństwie do mrowia sposobów oferowanych przez najnowsze wersje Matlaba (na tę chwilę 2011a z 8 kwietnia tego roku), które wykorzystują nowe toolboksy (jak MATLAB Builder JA), użycie silnika Matlaba oferuje całkowitą kompatybilność wsteczną. Nie jestem pewien czy wersje starsze niż 5.1 też udostępniały te biblioteki, jednak Matlab 5.3 jest nadal masowo eksploatowany w różnych przedsiębiorstwach, w szkołach i na uczelniach.
Niniejszy artykuł jest wstępem do krótkiego poradnika obsługi silnika Matlaba. Nie jest to dokumentacja – wszystkie funkcje udostępnianego API są dość dobrze udokumentowane. Jest to raczej ściągawka najczęstszych problemów, które pojawiają się przy pisaniu programów używających silnika do komunikacji z sesjami Matlaba.
Użycie bibliotek silnika Matlaba ma jedną bardzo poważną wadę. Wymaga posiadania
zainstalowanego Matlaba. Jeśli nie chcesz instalować Matlaba, być może
powinieneś zainteresować się, drogi Czytelniku, Matlabowskim środowiskiem MCR
(Matlab Compiler Runtime), które instaluje minimalną ilość bibliotek wymaganą do
wykonywania plików *.m. Tak czy siak, na potrzeby niniejszej prezentacji
potrzebować będziemy co zainstalowanego Matlaba w wersji 5.3. Dla celów
niniejszego artykułu wprowadzę również oznaczenie bezwzględnej ścieżki głównego
katalogu Matlaba: $MATLABROOT
.
Położenie
Wszystkie niezbędne do szczęścia biblioteki znajdują się w katalogu
$MATLABROOT/bin/$ARCH
gdzie $ARCH
oznacza architekturę komputera (np. glnx86
dla 32-bitowego Linuksa, mac dla Macintosha z procesorem PowerPC, win32 dla
32-bitowego Windowsa itp.). Należy pamiętać, że starsze wersje Matlaba (np.
wspomniana już 5.3) nie obsługują różnych architektur komputerów, więc wszystkie
niezbędne biblioteki znajdują się bezpośrednio w katalogu bin. Nowe wersje
teoretycznie zapewniają zgodność ze starszymi poprzez dołączenie w katalogu bin
odpowiedniego skryptu, lecz praktycznie błędy pojawiają się na poziomie
linkowania w czasie kompilacji, a później ustawiania odpowiednich ścieżek do
zmiennych środowiskowych.
Poza powyższymi kompilator będzie potrzebował jeszcze plików nagłówkowych do
silnika. Znajdują się one w katalogu: $MATLABROOT/extern/include
Dołączanie wszystkiego do projektu
W celu zbudowania projektu należy oczywiście ustawić odpowiednie flagi w naszym
ulubionym kompilatorze, czyli gcc. Dlaczego nie kompilujemy na sposób Matlaba,
czyli przy pomocy programu mex? Dlatego, że przysparza to jedynie problemów.
Przede wszystkim Matlab nie dostarcza kompilatora języka C++, więc musimy zmusić
ichnie rozwiązanie do współpracy z zewnętrznym kompilatorem tegoż języka.
Stosowanie takich wrapperów wydaje mi się bezcelowe, skoro mogę stworzyć
starego, dobrego Makefile’a. Poza tym polecenie mex -setup
lubi nie wykryć
kompilatora C++.
Oczywiście na początek:
g++ (...) -I$MATLABROOT/extern/include (...)`
Pominąłem w powyższym wszystkie inne flagi i pliki, bo zależą one od posiadanej architektury, pisanego programu i własnych preferencji.
Następnym krokiem jest to co przysparza, jak wynika z mych pobieżnych oględzin for internetowych, najwięcej problemów. Linkowanie wszystkiego.
Przede wszystkim należy przekazać linkerowi katalog, w którym zamieszczone są
biblioteki silnika. Robi się to przy pomocy przełącznika
-L$MATLABROOT/bin/$ARCH
. Następnie należy wskazać bezpośrednio dwie
biblioteki: libeng
oraz libmx
. Robi się to przy pomocy przełączników -leng
-lmx
. To nie wszystko. Obie te biblioteki korzystają z innych bibliotek, które
rezydują razem z nimi w katalogu (ldd -d libeng.so
). Najprościej uwzględniamy
je w linkerze przy pomocy przełącznika -rpath
lub -rpath-link
. Odpowiednia
opcja w linkerze wygląda następująco: -Wl,-rpath-link,$MATLABROOT/bin/$ARCH
.
Dokumentacja podaje wprawdzie, że używane biblioteki muszą rezydować w tym samym
katalogu co libeng
i libmx
, ale empirycznie sprawdziłem, że jest to kłamstwo
wierutne. Z drugiej strony, skoro i tak Matlab musi być zainstalowany
(licencja), to co za różnica, gdzie będą?
Alternatywą dla rpath, jeśli ktoś bardzo się gryzie z tą dyrektywą, jest zaktualizowanie ścieżek wyszukiwania bibliotek w katalogu /etc/ld.so.conf:
touch /etc/ld.so.conf/matlab.conf
echo $MATLABROOT/bin/$ARCH >> /etc/ld.so.conf/matlab.conf
Koniec końców, w przypadku użycia rpath-link, nasze polecenie będzie wyglądać następująco:
g++ -L$MATLABROOT/bin/$ARCH -leng -lmx -Wl,-rpath-link,$MATLABROOT/bin/$ARCH
(...)
Najczęściej używane funkcje silnika
W celu poinformowania programu o tym, że będziemy korzystać z funkcji API
silnika Matlaba, dołączamy do programu odpowiedni plik nagłówkowy: #include
"engine.h"
. Umożliwia on wykorzystanie funkcji
API,
dzięki którym możliwa staje się komunikacja z sesją Matlaba
Skoro już o tym mowa to warto taką sesję otworzyć. Służy do tego funkcja Engine
*engOpen(const char *startcmd)
. Parametr startcmd
jest komenda startową
Matlaba, czyli katalog ze skryptem startowym Matlaba albo będzie się znajdował w
zmiennej $PATH
, albo należy podawać ścieżkę do skryptu startowego.
Dokumentacja zastrzega sobie, że pod systemami z rodziny Windows parametr
startcmd
powinien być pusty (innymi słowy, wynosić NULL), jednak w moich
programach zawsze zapominałem tego zmieniać i wszystko działało bardzo dobrze.
Funkcja służąca do otwierania sesji Matlaba zwraca wskaźnik typu Engine
, który
jest używany we wszystkich komendach, które odwołują się do silnika
matematycznego środowiska Matlab. W przypadku zaś nieudanego otwarcia sesji
Matlaba zwracane jest NULL. W celu zamknięcia otwartej sesji Matlaba (próba
zamknięcia sesji nieotwartej może wywołać segfaulta) użyje się funkcji int
engClose(Engine *ep)
. Zwraca ona 0 w przypadku powodzenia i 1 w przypadku
niepowodzenia zamknięcia silnika.
Do przekazywania do Matlaba poleceń służy funkcja int engEvalString(Engine *ep,
const char *command)
, która do sesji Matlaba, na którą wskazuje wskaźnik *ep
,
przekazuje polecenie określone łańcuchem znaków command
. Zwracane wartości to
0 (poprawne wykonanie polecenia) lub 1 (wystąpienie błędu).
Drugą najczęściej używaną funkcją jest int engPutVariable(Engine *ep, const
char *name, const mxArray *pm)
. Pozwala ona na wprowadzenie do obszaru
zmiennych Matlaba tablicy typu mxArray
. Zadeklarowanie takiej tablicy wymaga
posiadania w pamięci innej tablicy (np. dwuwymiarowej tablicy liczb
zmiennoprzecinkowych), która zostanie przekopiowana przy użyciu np. funkcji
memcpy
. Ponieważ kilka linijek kodu jest wartych więcej niż tysiąc słów, to w
celu lepszego zaprezentowania typowego użycia funkcji engPutVariable
poniżej
zostaje zamieszczony fragment kodu odpowiadający za utworzenie w uruchomionej
sesji Matlaba zmiennej matlabArray
. Reprezentować ona będzie dwuwymiarową
macierz 2 na 3 kolejnych elementach równych 1, 2, 3, …
Engine *ep;
double MyArray[2][3] = { {1.0, 2.0, 3.0}, {4.0, 5.0, 6.0} };
// Deklaracja zmiennej array
mxArray *array = NULL;
// Tu następuje otwarcie sesji Matlaba itp.
// Inicjalizacja zmiennej array
array = mxCreateDoubleMatrix(2, 3, mxREAL);
memcpy((void *)mxGetPr(array), (void *)MyArray, sizeof(MyArray));
// Przesłanie zmiennej array do otwartej sesji Matlaba
// pod nazwą matlabArray.
engPutVariable(ep, "matlabArray", array);
// Dalsza część programu... `
Odpalenie silnika w programie konsolowym
Program może przyjmować kilka argumentów. Sposób obsługi listy argumentów
przekazywanych do funkcji main
dostępny jest szeroko w internecie i pozwolę
sobie nie powielać wielu lepszych poradników wprowadzających do tego
zagadnienia. Od siebie powiem tylko, że podejście przedstawione w niniejszym
programie jest chyba najbardziej prostym i typowym i bazuje na zwykłym
porównywaniu łańcuchów znakowych.
Pod tą całą otoczką znajduje się to co nas najbardziej interesuje, czyli fragmenty kodu odpowiedzialne za realizację obliczeń przez uruchomioną sesję Matlaba. Zanim jednak do obliczeń dojdzie, musimy uruchomić ów silnik:
Engine *ep;
if( !(ep = engOpen("matlab")) ){
std::cout << "Can't start MATLAB engine\n";
return -1;
}
Powyższy fragment kodu tworzy zmienną wskaźnikową, która już w następnej linijce
wskazuje na otwartą sesję Matlaba. Jeśli nie, to na stdout wysyłany jest
komunikat, a program zostaje zakończony. Warto zwrócić uwage na parametr funkcji
engOpen(...)
. W systemach podobnych do Uniksa jest nim nazwa
pliku uruchamialnego Matlaba rezydująca w zmiennej środowiskowej PATH
.
Dokumentacja podaje przy tym, że dla Windowsów przy otwieraniu sesji Matlaba
powinien zostać przekazany pusty łańcuch znaków, jednak przeprowadzone przeze
mnie testy behawioralne jeszcze nigdy nie wykazały błędu przy przekazywaniu
łańcucha niepustego.
Następnie prawdopodobnie chcielibyśmy mieć możliwość zbierania odpowiedzi Matlaba do bufora znakowego. Załatwi to następujący fragment kodu:
int BUFSIZE = 512;
char buffer[BUFSIZE];
engOutputBuffer(ep, buffer, BUFSIZE);
Powyższy fragment, raz zadeklarowany, spowoduje przekazanie odpowiedniej porcji
odpowiedzi Matlaba o wielkości BUFSIZE
do bufora znakowego. Wpisywanie
odpowiedzi do tablicy rozpoczyna się zawsze od jej pierwszego elementu. Tutaj
jednak są dwie uwagi. Uwaga pierwsza: Matlab nie zakańcza w prawidłowy sposób
(tzn. przez znak /0
) takiego łańcucha, więc musimy to zrobić na własny sposób,
np. poprzez ręczne wpisanie tego znaku do ostatniego elementu tablicy bądź
zapisanie całości zerami. Uwaga druga: z doświadczenia wiem, że wpisywanie
odpowiedzi lubi się krzaczyć przy długich odpowiedziach (np. długim opisie
funkcji) i buforze wielkości ponad 1024 znaki. Jak widać, 8 kb to zbyt
wiele dla nowoczesnych komputerów.
Jedna uwaga odnośnie błędów przekazywanych przez Matlaba (mogących wynikać np. z powodu błędnej składni przekazanego zapytania). Domyślnie są one przekazywane nie do zadeklarowanego bufora, a na standardowe wyjście błędów, stderr.
Po tych przygotowaniach możemy wreszcie zabrać się za obliczenia. Na pierwszy plan idzie wykonywanie dowolnej komendy przekazywanej w postaci łańcucha znaków:
std::cout << "Please enter command (q to quit this mode):\n>> ";
while( getline(std::cin, command) ) {
if( strcmp(command.c_str(), "q") == 0 ) break;
engEvalString(ep, command.c_str());
buffer[BUFSIZE-1] = 0;
std::cout << buffer;
std::cout << ">> ";
}
Powyższy fragment kodu spowoduje wyświetlenie się znaku zachęty oraz wykonanie wpisanego przez użytkownika zapytania, a następnie wyświetlenie jego wyniku, chyba że wprowadzono znak ‘q’, który przerywa pętlę i powoduje wyjście z tego trybu.
Według mnie przedstawiony powyżej sposób jest najwygodniejszym do komunikacji z
sesją Matlaba. Przykładowo możemy w ten sposób utworzyć zmienną w przestrzeni
zmiennych Matlaba (np. foo = [1 2 3]
spowoduje utworzenie trójelementowego
wektora foo). Doświadczenie jednak uczy, że operowanie na stringach, choć
wygodne, wcale nie musi porażać swoją szybkością. Z tego powodu Matlab pozwala
na deklarowanie w nim zmiennych w odmienny sposób:
mxArray *array = NULL;
double MyArray[2][3] = { {1.0, 2.0, 3.0}, {4.0, 5.0, 6.0} };
array = mxCreateDoubleMatrix(2, 3, mxREAL);
memcpy((void *)mxGetPr(array), (void *)MyArray, sizeof(MyArray));
engPutVariable(ep, "matArray", array);
engEvalString(ep, "matArray + (matArray.^3)");
Powyżej wstępnie zadeklarowany został wskaźnik na zmienną typu mxArray
oraz
dwuwymiarowa tablica rzeczywistych liczb zmiennoprzecinkowych, która zostanie
przekazana do sesji Matlaba (warto w tym miejscu przypomnieć, że Biblioteka
Standardowa C i
C++ pozwala na użycie liczb
zespolonych). Następnie inicjalizowana jest pusta tablica typu Matlaba
(mxArray
), do której kopiowana jest zawartość naszej tablicy typu double
.
Dopiero taka zmienna może zostać wprowadzona do przestrzeni zmiennych sesji
Matlaba przy pomocy funkcji engPutVariable(...)
, której drugim argumentem jest
nazwa jaką przyjmie zmienna w tejże przestrzeni.
Są to najważniejsze, a zarazem podstawowe elementy programu wykorzystującego API silnika Matlaba. Oczywiście sama biblioteka jest znacznie szersza i zawiera mnóstwo innych typów i funkcji, które nie zostały opisane w tym trzyczęściowym poradniku; uważam jednak, że zrozumienie tego jak działa załączony program pozwoli na ich swobodne wykorzystanie.