Matlab engine

Table of Contents

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 -rpathlub -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.