Matlab engine - cz. 3

Po dłuższej przerwie zabrałem się nareszcie do napisania trzeciej, ostatniej części krótkiego poradnika obsługi silnika Matlaba z poziomu programu napisanego w C/C++. W tej części przedstawię prosty program konsolowy, który będzie wykorzystywać najważniejsze funkcje udostępnianego API. Jeśli nie wiesz o czym mówię, zapraszam do lektury dwóch poprzednich części poradnika (część pierwsza i część druga).

Nie będę tu przedstawiał całości kodu, gdyż przy pomocy strony internetowej i tak nikt nie będzie analizował blisko stu linijek. Skupię się jedynie na jego niektórych elementach, które postaram się możliwie najbardziej zrozumiale wyjaśnić.

Dla zachowania w niektórych miejscach prostoty i elegancji oraz – nie będę nikogo oszukiwał – z powodu osobistych preferencji, program jest napisany w języku C++. Wiem, że używanie tegoż do pisania programów, które nie są OO jest uważane przez niektórych speców za herezję; ja jednak do tej grupy nie należę i chwała mi za to.

Opis programu

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.