[C][LabVIEW] Własne DLL vs LabVIEW oraz zabawy ptaszkiem czyli integracja LV z brokerem Kafka
: środa 17 lip 2019, 22:02
☘ ♬ ♬ ♬ Moja muzyka do kodowania ♬ ♬ ♬ ☘
♫ ♩ ♪ Batushka ♩ ☘ ♪ Litourgiya ♪ ♩ ♫
https://youtu.be/xgfa5UlZAL8
No wiem, grunt to zapodać dobry tytuł ... no ale inaczej to nikt by tutaj nie zajrzał, a temat jest uważam ciekawy.
Post ten to swego rodzaju kontynuacja zagadnień stosu ELK i możliwości jego zasilania danymi z poziomu aplikacji przygotowanych w LabVIEW. Poprzednio pokazałam, jak pisać w LV bezpośrednio do Elasticsearch, całość niby działała, ale wydajność na poziomie ~100 komunikatów na sekundę, to ... no cóż, bywało lepiej. Tu zmienimy podejście i dostawimy nowy komponent - system dystrybucji komunikatów (broker) Kafka. Oczywiście lekko nie będzie, ponieważ integracja LV i Kafki wymaga pewnych ceregieli, ale całość jest do ogarnięcia. A więc najpierw...
Tematem Kafki jako takiej zajmiemy się za chwilę, póki co trzeba mi odczarować jeden drobny aspekt kodowania w LabVIEW, a mianowicie zagadnienie dołączania zewnętrznych (własnych lub obcych) bibliotek DLL do naszej aplikacji. Nie widzę potrzeby strzępić dzioba, przy tak dobrych opracowaniach dostępnych online, zatem polecam:
https://knowledge.ni.com/KnowledgeArticleDetails?id=kA00Z0000019Ls1SAE&l=pl-PL
An Overview of Accessing DLLs or Shared Libraries from LabVIEW
How to Call Win32Dynamic Link Libraries (DLLs)from LabVIEW
Writing Win32 Dynamic LinkLibraries (DLLs) and Calling Themfrom LabVIEW
Odnośnie narzędzia do kodowania pod Windows - większość opracowań dotyka różnych wersji Microsoft Visual C, ja nieco na przekór zaproponuję przygotowanie biblioteki w pakiecie DEV-C++. Narzędzie to ma pewnie i swoje wady, ale jest w tym akurat przypadku bardzo dobrą alternatywą dla MSVC, no i jest za gratis.
Cały eksperyment sprowadza się do utworzenia projektu biblioteki DLL dla platformy Windows 32 bit, dostaniemy wstępnie zakodowany szkielet funkcji głównej DllMain(). Cała nasza praca to wypełnienie tego treścią (docelowo - połączeniem z Kafką), póki co poprzestańmy na klasycznym demku - wywołaniu systemowej funkcji MessageBox().
W pliku nagłówkowym deklarujemy funkcję sayHello() opatrując wymaganymi do eksportu dyrektywami:
W pliku kafkalv.cpp definiujemy ciało funkcji, jak widać - trywialne:
Fajne jest to, że DEV-C++ samodzielnie zadba o przygotowanie pliku z listą funkcji do eksportu w nasze DLL-ce, taki oto pliczek powstaje przy okazji i proszę w nim nie grzebać ręcznie, raczej poprawiać deklarację funkcji, gdy LabVIEW będzie miało problemy z widocznością czy do nich dostępem:
Teraz jest też dobra okazja opowiedzieć o pokrace programistycznej czyli widocznej w wywołaniach powyżej funkcji log(). Niespecjalnie jestem dumna z tego...czegoś, ale poza kilkoma wadami (tu szczególnie: koszt wykonania) ona ma tę zaletę, że skutecznie działa, zrzuca dane na dysk natychmiast i nie blokuje dostępu do generowanego pliku logu. Aby nie bawić się w kotka i myszkę z funkcją logującą, przyjęłam że plik logu będzie zrzucany w tym samym katalogu, w którym znajduje się plik *.dll. Stąd obecność funkcji GetModuleFileName() i wywołanie na jej potrzeby GetDllHandle(). Całość zwraca ścieżkę do aktualnie załadowanej instancji biblioteki DLL, nawet jeżeli proces ją wołający powoływany jest z zupełnie innego foldera. Tu dokumentacja funkcji systemowej
VirtualQuery, a funkcja GetDllHandle() poniżej:
Sama funkcja logująca, akceptująca zmienną listę parametrów wygląda tak:
Przejdźmy teraz do wnętrzności funkcji DllMain(), prostych ale ze względu na wywołania funkcji log() pozwalających zaobserwować co LabVIEW wyczynia z podłożoną mu zewnętrzną biblioteką DLL:
Kreseczki przy process attach/detach to początek i koniec segmentu logu, zaraz zobaczymy w praktyce jak to działa. Malunki poniżej to minimalistyczna aplikacja w LabVIEW wywołująca wspomnianą funkcję sayHello() z naszej biblioteki, widzimy efekt działania jak i okna konfiguracyjne kostki
Call Library Function Node:
Budowanie takiej aplikacji w LV, jak również zupełnie prostą koderkę w DEV-C++ widzimy na żywo na filmiku:
https://youtu.be/q1FUSa6IrRo
Fragment logu aplikacji, który powstał w ramach demka:
I jak to czytamy, a mianowicie:
- już podczas konfigurowania kostki `Call Library Function Node` biblioteka DLL jest ładowana do pamięci i wywoływana jest DllMain() z fdwReason=DLL_PROCESS_ATTACH
- DLL_THREAD_ATTACH to uruchomienie funkcji składowych biblioteki, LV obsługuje to w wątku wskazanym konfiguracją - u mnie wątek interfejsu użytkownika (UI)
- DLL_THREAD_DETACH to zwolnienie biblioteki w kontekście bieżącego wątku
- finalnie DLL_PROCESS_DETACH - odładowanie biblioteki, następuje przy zamknięciu okna *.VI bieżącej aplikacji korzystającej z naszej biblioteki. Jeżeli miałabym odpalone to cudo w dwóch identycznych okienkach LV - detach zostałby zawołany z chwilą zamknięcia ostatniego z nich, to oczywiste.
W ramach napisów końcowych - garstka wniosków.
Po pierwsze - tworzenie własnych, nawet najprostszych DLL do wykorzystania w LabVIEW jest dla ludzi i nie trzeba ku temu wypaśnych komercyjnych narzędzi, z powodzeniem wystarczą darmowe. Po drugie warto chwilę czasu poświęcić na zaznajomienie się z cyklem życia takiej DLL w kontekście życia aplikacji w LV, dowiemy się wtenczas jakie zależności czasowe wiążą odpowiednie wywołania do wnętrza biblioteki, to ważne ze względu na alokację i zwalnianie zasobów, choćby tak podstawowych jak zamykanie uchwytów otwartych przez DLL plików. Po trzecie, tego w sumie nie pokazałam, ale należy mieć świadomość, że kod funkcji z DLL wykonuje się w kontekście przestrzeni adresowej procesu aplikacji LabVIEW, a nie żadnym tam odizolowanym 'sandbox' czy innej piaskownicy. Czyli jeżeli damy ciała w naszej zewnętrznie ładowanej funkcji to mamy prawie pewność, że główna aplikacja LV także ulegnie destabilizacji, szczególnie spektakularne są tu strzały typu '0xC0000005: Access violation reading location' które objawiają się...przy zamykaniu dłużej działającej aplikacji LabVIEW. Po prostu trzeba uważać.
No i słowo przejściowe - taki jakby mostek do drugiej części.
Często bywa tak, że w LabVIEW chcemy wykorzystać API innego produktu, czy to bazy danych czy może jakiegoś dziwacznego, ale cennego funkcjonalnie sterownika do sprzętu, bywa różnie. I często zdarza się tak, że owe API jest tak masakrystycznie pokręcone, struktury danych, którymi operuje są wręcz perwersyjnie zbudowane, normalnie - przesiadka z nich na LV to tylko siąść i ryczeć.
I wtenczas co robimy?
Ano przykrywamy te narzucone wywołania własną warstwą autorskiego oprogramowania (wrapperem), opakowując całą tę upierdliwość zewnętrznie narzuconego interfejsu własnymi, prostymi wywołaniami. Nad którymi w miarę panujemy i co ważne - możemy je modyfikować, zależnie od bieżących potrzeb. I o tym będzie dalej.
#slowanawiatr
♫ ♩ ♪ Batushka ♩ ☘ ♪ Litourgiya ♪ ♩ ♫
https://youtu.be/xgfa5UlZAL8
No wiem, grunt to zapodać dobry tytuł ... no ale inaczej to nikt by tutaj nie zajrzał, a temat jest uważam ciekawy.
Post ten to swego rodzaju kontynuacja zagadnień stosu ELK i możliwości jego zasilania danymi z poziomu aplikacji przygotowanych w LabVIEW. Poprzednio pokazałam, jak pisać w LV bezpośrednio do Elasticsearch, całość niby działała, ale wydajność na poziomie ~100 komunikatów na sekundę, to ... no cóż, bywało lepiej. Tu zmienimy podejście i dostawimy nowy komponent - system dystrybucji komunikatów (broker) Kafka. Oczywiście lekko nie będzie, ponieważ integracja LV i Kafki wymaga pewnych ceregieli, ale całość jest do ogarnięcia. A więc najpierw...
Tematem Kafki jako takiej zajmiemy się za chwilę, póki co trzeba mi odczarować jeden drobny aspekt kodowania w LabVIEW, a mianowicie zagadnienie dołączania zewnętrznych (własnych lub obcych) bibliotek DLL do naszej aplikacji. Nie widzę potrzeby strzępić dzioba, przy tak dobrych opracowaniach dostępnych online, zatem polecam:
Odnośnie narzędzia do kodowania pod Windows - większość opracowań dotyka różnych wersji Microsoft Visual C, ja nieco na przekór zaproponuję przygotowanie biblioteki w pakiecie DEV-C++. Narzędzie to ma pewnie i swoje wady, ale jest w tym akurat przypadku bardzo dobrą alternatywą dla MSVC, no i jest za gratis.
Cały eksperyment sprowadza się do utworzenia projektu biblioteki DLL dla platformy Windows 32 bit, dostaniemy wstępnie zakodowany szkielet funkcji głównej DllMain(). Cała nasza praca to wypełnienie tego treścią (docelowo - połączeniem z Kafką), póki co poprzestańmy na klasycznym demku - wywołaniu systemowej funkcji MessageBox().
W pliku nagłówkowym deklarujemy funkcję sayHello() opatrując wymaganymi do eksportu dyrektywami:
kafkalv.h pisze:Kod: Zaznacz cały
#define DLL_EXPORT __declspec(dllexport)
extern "C" {
void DLL_EXPORT sayHello( char *, char * );
}
W pliku kafkalv.cpp definiujemy ciało funkcji, jak widać - trywialne:
kafkalv.cpp pisze:Kod: Zaznacz cały
void DLL_EXPORT sayHello( char *pText, char *pCaption ){
log( "calling with: %s, %s", pText, pCaption );
MessageBox( 0, pText, pCaption, MB_ICONINFORMATION );
log( "done", NULL);
}
Fajne jest to, że DEV-C++ samodzielnie zadba o przygotowanie pliku z listą funkcji do eksportu w nasze DLL-ce, taki oto pliczek powstaje przy okazji i proszę w nim nie grzebać ręcznie, raczej poprawiać deklarację funkcji, gdy LabVIEW będzie miało problemy z widocznością czy do nich dostępem:
libkafkalv.def pisze:Kod: Zaznacz cały
EXPORTS
...
sayHello @5
Teraz jest też dobra okazja opowiedzieć o pokrace programistycznej czyli widocznej w wywołaniach powyżej funkcji log(). Niespecjalnie jestem dumna z tego...czegoś, ale poza kilkoma wadami (tu szczególnie: koszt wykonania) ona ma tę zaletę, że skutecznie działa, zrzuca dane na dysk natychmiast i nie blokuje dostępu do generowanego pliku logu. Aby nie bawić się w kotka i myszkę z funkcją logującą, przyjęłam że plik logu będzie zrzucany w tym samym katalogu, w którym znajduje się plik *.dll. Stąd obecność funkcji GetModuleFileName() i wywołanie na jej potrzeby GetDllHandle(). Całość zwraca ścieżkę do aktualnie załadowanej instancji biblioteki DLL, nawet jeżeli proces ją wołający powoływany jest z zupełnie innego foldera. Tu dokumentacja funkcji systemowej
kafkalv.cpp pisze:Kod: Zaznacz cały
HMODULE GetDllHandle() {
static int dummy = 0;
MEMORY_BASIC_INFORMATION mbi;
if( !VirtualQuery( &dummy, &mbi, sizeof( mbi ) ) ) {
return NULL;
}
return (HMODULE)mbi.AllocationBase;
}
Sama funkcja logująca, akceptująca zmienną listę parametrów wygląda tak:
kafkalv.cpp pisze:Kod: Zaznacz cały
void log( const char *fmt, ... ) {
FILE *hLog = NULL;
char szFullPath[0xFF];
char szDir[0xFF];
char szLogFileName [0xFF];
va_list argptr;
SYSTEMTIME systime;
GetLocalTime( &systime );
GetModuleFileName ( GetDllHandle(), szFullPath, 0xFF );
char *pLastSlash = strrchr( szFullPath, '\\' );
*pLastSlash = 0x00;
sprintf(
szLogFileName,
"\\kafkalv-%04d%02d%02d.txt",
systime.wYear,
systime.wMonth,
systime.wDay
);
strcat ( szFullPath, szLogFileName );
hLog = fopen( szFullPath, "a" );
if (!hLog) {
return;
}
fprintf(
hLog,
"[%04d-%02d-%02dT%02d:%02d:%02d.%03d][%s][%d] ",
systime.wYear,
systime.wMonth,
systime.wDay,
systime.wHour,
systime.wMinute,
systime.wSecond,
systime.wMilliseconds,
__FILE__,
__LINE__
);
va_start( argptr, fmt) ;
vfprintf ( hLog, fmt, argptr );
va_end(argptr);
fprintf( hLog, "\n" );
fclose( hLog );
}
Przejdźmy teraz do wnętrzności funkcji DllMain(), prostych ale ze względu na wywołania funkcji log() pozwalających zaobserwować co LabVIEW wyczynia z podłożoną mu zewnętrzną biblioteką DLL:
kafkalv.cpp pisze:Kod: Zaznacz cały
BOOL WINAPI DllMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID lpvReserved) {
switch( fdwReason ) {
case DLL_PROCESS_ATTACH: {
log ( "hInst:%08X, DLL_PROCESS_ATTACH ------------------------------ ", hinstDLL );
break;
}
case DLL_PROCESS_DETACH: {
log ( "hInst:%08X, DLL_PROCESS_DETACH ------------------------------ ", hinstDLL );
break;
}
case DLL_THREAD_ATTACH: {
log ( "hInst:%08X, DLL_THREAD_ATTACH", hinstDLL );
break;
}
case DLL_THREAD_DETACH:{
log ( "hInst:%08X, DLL_THREAD_DETACH", hinstDLL );
break;
}
}
return TRUE;
}
Kreseczki przy process attach/detach to początek i koniec segmentu logu, zaraz zobaczymy w praktyce jak to działa. Malunki poniżej to minimalistyczna aplikacja w LabVIEW wywołująca wspomnianą funkcję sayHello() z naszej biblioteki, widzimy efekt działania jak i okna konfiguracyjne kostki
Budowanie takiej aplikacji w LV, jak również zupełnie prostą koderkę w DEV-C++ widzimy na żywo na filmiku:
https://youtu.be/q1FUSa6IrRo
Fragment logu aplikacji, który powstał w ramach demka:
kafkalv-20190717.txt pisze:Kod: Zaznacz cały
[2019-07-17T16:45:40.318][kafkalv.cpp][60] hInst:62500000, DLL_PROCESS_ATTACH ------------------------------
[2019-07-17T16:46:06.236][kafkalv.cpp][60] hInst:62500000, DLL_THREAD_ATTACH
[2019-07-17T16:46:27.614][kafkalv.cpp][60] calling with: to jest tekst w okienku, to jest tytuł okienka
[2019-07-17T16:46:33.889][kafkalv.cpp][60] done
[2019-07-17T16:46:35.857][kafkalv.cpp][60] calling with: to jest tekst w okienku, to jest tytuł okienka
[2019-07-17T16:46:37.484][kafkalv.cpp][60] done
[2019-07-17T16:46:47.513][kafkalv.cpp][60] hInst:62500000, DLL_THREAD_DETACH
[2019-07-17T16:46:51.903][kafkalv.cpp][60] hInst:62500000, DLL_PROCESS_DETACH ------------------------------
I jak to czytamy, a mianowicie:
- już podczas konfigurowania kostki `Call Library Function Node` biblioteka DLL jest ładowana do pamięci i wywoływana jest DllMain() z fdwReason=DLL_PROCESS_ATTACH
- DLL_THREAD_ATTACH to uruchomienie funkcji składowych biblioteki, LV obsługuje to w wątku wskazanym konfiguracją - u mnie wątek interfejsu użytkownika (UI)
- DLL_THREAD_DETACH to zwolnienie biblioteki w kontekście bieżącego wątku
- finalnie DLL_PROCESS_DETACH - odładowanie biblioteki, następuje przy zamknięciu okna *.VI bieżącej aplikacji korzystającej z naszej biblioteki. Jeżeli miałabym odpalone to cudo w dwóch identycznych okienkach LV - detach zostałby zawołany z chwilą zamknięcia ostatniego z nich, to oczywiste.
W ramach napisów końcowych - garstka wniosków.
Po pierwsze - tworzenie własnych, nawet najprostszych DLL do wykorzystania w LabVIEW jest dla ludzi i nie trzeba ku temu wypaśnych komercyjnych narzędzi, z powodzeniem wystarczą darmowe. Po drugie warto chwilę czasu poświęcić na zaznajomienie się z cyklem życia takiej DLL w kontekście życia aplikacji w LV, dowiemy się wtenczas jakie zależności czasowe wiążą odpowiednie wywołania do wnętrza biblioteki, to ważne ze względu na alokację i zwalnianie zasobów, choćby tak podstawowych jak zamykanie uchwytów otwartych przez DLL plików. Po trzecie, tego w sumie nie pokazałam, ale należy mieć świadomość, że kod funkcji z DLL wykonuje się w kontekście przestrzeni adresowej procesu aplikacji LabVIEW, a nie żadnym tam odizolowanym 'sandbox' czy innej piaskownicy. Czyli jeżeli damy ciała w naszej zewnętrznie ładowanej funkcji to mamy prawie pewność, że główna aplikacja LV także ulegnie destabilizacji, szczególnie spektakularne są tu strzały typu '0xC0000005: Access violation reading location' które objawiają się...przy zamykaniu dłużej działającej aplikacji LabVIEW. Po prostu trzeba uważać.
No i słowo przejściowe - taki jakby mostek do drugiej części.
Często bywa tak, że w LabVIEW chcemy wykorzystać API innego produktu, czy to bazy danych czy może jakiegoś dziwacznego, ale cennego funkcjonalnie sterownika do sprzętu, bywa różnie. I często zdarza się tak, że owe API jest tak masakrystycznie pokręcone, struktury danych, którymi operuje są wręcz perwersyjnie zbudowane, normalnie - przesiadka z nich na LV to tylko siąść i ryczeć.
I wtenczas co robimy?
Ano przykrywamy te narzucone wywołania własną warstwą autorskiego oprogramowania (wrapperem), opakowując całą tę upierdliwość zewnętrznie narzuconego interfejsu własnymi, prostymi wywołaniami. Nad którymi w miarę panujemy i co ważne - możemy je modyfikować, zależnie od bieżących potrzeb. I o tym będzie dalej.
#slowanawiatr