Sii Polska

SII UKRAINE

SII SWEDEN

  • Szkolenia
  • Kariera
Dołącz do nas Kontakt
Wstecz

Sii Polska

SII UKRAINE

SII SWEDEN

Wstecz

07.05.2025

Jak się dogadać z innymi językami? Wywoływanie obcych funkcji w kodzie C/C++

07.05.2025

Jak się dogadać z innymi językami? Wywoływanie obcych funkcji w kodzie C/C++

Wiele programistów, szczególnie wywodzących się ze środowiska embedded, specjalizuje się w językach C/C++. W tym artykule zachęcam do wyjścia poza utarty schemat i spojrzenia na możliwości rozszerzenia własnej aplikacji o funkcjonalność napisaną w innym języku wyższego poziomu. W tym przypadku mam na myśli języki Java/Python, ponieważ w mniejszym stopniu zależą od architektury, na której się wykonują.

Większość opisywanych przypadków skupia się na wywoływaniu kodu niższego poziomu. W przypadku Javy jest to Java Native Interface, a dla Pythona są to Extensions Modules. Brakuje natomiast opisów tego, jak zrobić rzecz odwrotną, czyli jak stworzyć środowisko dla działania języka wyższego poziomu. Zamierzam pokazać, jak komunikować się z tym innym światem w naszym programie i zobrazuję to na konkretnych przykładach dla tych dwóch języków.

Zapraszam do lektury!

Python

Aby program w języku C++ się skompilował, należy zadbać o dostęp do bibliotek z pakietu Python3. W CMakeLists.txt zajmują się tym następujące linie:

find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
#
# ...definicja projektu o nazwie w ${PRJ_NAME}...
#
target_include_directories(${PRJ_NAME} PRIVATE ${Python3_INCLUDE_DIRS})
target_link_libraries(${PRJ_NAME} PUBLIC ${Python3_LIBRARIES})

Oczywiście wcześniej należy mieć zainstalowane odpowiednie biblioteki w systemie. W Ubuntu jest to pakiet python3-dev.

Przykładowy kod w języku Python, który będzie uruchamiany w naszej aplikacji, jest pokazany poniżej. Będzie zapisany w pliku my_module.py. Zawiera prostą funkcję statyczną oraz klasę z metodą, która zwraca ciąg znaków. Klasa przechowuje w polu ‘name’ wartość, którą ustawia się w konstruktorze. Ponadto klasa zawiera metodę ‘greet’, która wypisuje na ekran komunikat z nadanym imieniem.

def hello():
    print("[Python]: Hello!")

def print_stuff(a,b):
    print("[Python]: Hello world! Your numbers are:", a, "and", b)
    return 5

class MyClass:
    def __init__(self, name):
        self.name = name
    
    def greet(self):
        return f"Hello, my name is {self.name}!" 

Minimalny program, który przygotowuje środowisko i załaduje nasz moduł, jest przedstawiony poniżej. Jest napisany w C++, ale kluczowe funkcje byłyby takie same w programie w C. Do samej inicjalizacji wystarczy jedynie wywołanie Py_Initialize(), natomiast pozostałe linie zapewniają poprawne znalezienie naszego programu pliku .py. Założenie jest takie, że znajduje się on w tym samym katalogu, co plik źródłowy programu kompilowanego.

W typowej konfiguracji cmake plik wykonywalny po skompilowaniu będzie znajdował się w innym miejscu i nie byłby w stanie znaleźć tego modułu. W tym celu ustawiona zostaje zmienna środowiskowa PYTHONPATH na podstawie makra __FILE__. Alternatywnie, można tą zmienną ustawić ręcznie w konsoli przed uruchomieniem programu i efekt będzie ten sam. Przedstawione podejście jest o tyle lepsze, że nie wymaga od użytkownika dodatkowych czynności.

#include <filesystem>
#define PY_SSIZE_T_CLEAN
#include <Python.h>

int main(int argc, char** argv)
{
    using std::filesystem::path;
    using std::filesystem::weakly_canonical;
    const auto src_dir {weakly_canonical(path(__FILE__)).parent_path()};
    static constexpr int OverwriteVariable = 1;
    setenv("PYTHONPATH", src_dir.c_str(), OverwriteVariable);
    
    Py_Initialize();

    PyObject* my_module = PyImport_ImportModule("my_module");
    if(my_module == nullptr){
        PyErr_Print();
        return -1;
    }

    Py_DECREF(my_module);

    Py_Finalize();
    return 0;
}

Następnie można już załadować nasz moduł za pomocą PyImport_ImportModule(). Niech nie umknie uwadze to, że nazwa modułu jest przekazywana bez rozszerzenia *.py. Jeżeli je dodamy, to funkcja zwróci NULL, a PyERR_Print() wypisze szczegóły problemu:

kod

Moduł jest zwracany jako wskaźnik na typ PyObject. Jest to uniwersalny typ, odzwierciedlający dynamiczny sposób typowania w Pythonie. Wszystkie obiekty są reprezentowane tak samo i dopiero w momencie wykonania jakiejś operacji jest sprawdzane, czy dana instancja pasuje do kontekstu.

Jedną z ostatnich operacji wykonanych w programie jest Py_DECREF() na obiekcie modułu. Jest to związane z zarządzaniem czasem życia projektu. W Pythonie obiekty są automatycznie niszczone, kiedy nie są już potrzebne. Służy do tego licznik referencji obiektu. W przypadku niskopoziomowego używania obiektów, należy nim zarządzać ręcznie, aby zapobiec wyciekom pamięci. Po wywołaniu tej funkcji licznik jest zmniejszany i jeżeli osiągnie 0, to obiekt zostanie zniszczony. Na samym końcu powinno się wywołać Py_Finalize(), które zwalnia zasoby środowiska uruchomieniowego Python.

Mechanizm automatycznej deaolokacji zasobów

Zanim przejdę do uruchamiania kolejnych przykładów, wprowadzę mechanizm automatycznej dealokacji zasobów. Język C++ umożliwia to dzięki technice RAII, która zwalnia programistę z ręcznego uruchamiania funkcji zwalniającej zasoby. Można to uzyskać poprzez specjalizację klasy std::unique_ptr z odpowiednim typem Deleter. Dzięki temu funkcja Py_DECREF() zostanie wywołana na każdym chronionym obiekcie, np. kiedy wyjście z funkcji nastąpi wcześniej na skutek wystąpienia błędu.

#include <memory>
static auto pyobject_deleter = [](PyObject* pObject)
{
    if(pObject){
        Py_DECREF(pObject);
    }
};
using PyObjectUniquePtr = std::unique_ptr<PyObject, decltype(pyobject_deleter)>;

Uzbrojeni w nowe narzędzie, możemy przejść do wywołania funkcji statycznej, czyli niepowiązanej z żadną instancją klasy. Dla zwięzłości przykładu, sprawdzanie poprawności załadowania modułu zostało pominięte. Funkcja jest po prostu kolejnym obiektem, który jest dostępny jako atrybut modułu. Do wywołania dowolnego obiektu używa się jednej funkcji z rodziny PyObject_Call*(). Jeżeli nie zamierzamy przekazywać żadnych parametrów, to optymalnym wyborem jest PyObject_CallNoArgs().

PyObjectUniquePtr my_module {PyImport_ImportModule("my_module")};
    PyObjectUniquePtr hello {PyObject_GetAttrString(my_module.get(), "hello")};
    PyObject_CallNoArgs(hello. Get());

Po uruchomieniu programu w konsoli pojawi się komunikat wygenerowany przez kod Pythona:

kod

A co, jeżeli chcemy przekazać jakąś wartość? Najpierw należy ją przekonwertować na instancję klasy PyObject. Biblioteka Pythona dostarcza cały zestaw takich funkcji. Ponadto istnieje więcej niż jedna metoda wywołania funkcji z kilkoma parametrami. Jedną z nich jest wariadyczna funkcja PyObject_CallFunctionObjArgs(), gdzie ostatnim parametrem zamykającym listę musi być NULL.

PyObjectUniquePtr print_stuff {PyObject_GetAttrString(my_module.get(), "print_stuff")};
    PyObjectUniquePtr arg1 {PyLong_FromLong(99)};
    PyObjectUniquePtr arg2 {PyLong_FromLong(42)};
    PyObjectUniquePtr ret_val{PyObject_CallFunctionObjArgs(print_stuff.get(), arg1.get(), arg2.get(), nullptr)};
    fmt::print("Function returned {}\n", PyLong_AsLong(ret_val.get()));

Wynik działania kodu:

kod

Alternatywnie, parametry można przekazać jako Pythonową tuplę, którą należy wcześniej stworzyć. Następnie należy wywołać funkcje PyObject_CallObject(). Do przetworzenia obiektów Pythona na zmienne typu integer służy funkcja PyLong_AsLong().

PyObjectUniquePtr args_tuple{PyTuple_Pack(2, PyLong_FromLong(99), PyLong_FromLong(42))};
    PyObjectUniquePtr ret_val {PyObject_CallObject(print_stuff.get(), args_tuple.get())};
    fmt::print("Function returned {}\n", PyLong_AsLong(ret_val.get()));

Wynik działania kodu:

kod

Operowanie na instancji obiektu

Kolejnym poziomem zaawansowania jest operowanie na instancji obiektu. Klasę odnajduje się tak samo jak zwykłą funkcję, poprzez znalezienie atrybutu w module. Żeby stworzyć jej instancję, należy wywołać uprzednio znaleziony obiekt, co jest równoznaczne z uruchomieniem konstruktora. W naszym przypadku klasa przyjmuje tekst, w tym przypadku „Bob”. Następnie z instancji klasy zostaje pobrana metoda „greet()”. Po jej wywołaniu, ciąg tekstowy zostaje wyciągnięty ze zwróconego obiektu za pomocą funkcji PyUnicode_AsUTF8().

PyObjectUniquePtr class_ref{PyObject_GetAttrString(my_module.get(), "MyClass")};
    PyObjectUniquePtr args {PyTuple_Pack(1, PyUnicode_FromString("Bob"))};
    PyObjectUniquePtr object {PyObject_CallObject(class_ref.get(), args.get())};
    PyObjectUniquePtr method {PyObject_GetAttrString(object.get(), "greet")};
    PyObjectUniquePtr result {PyObject_CallNoArgs(method.get())};
    fmt::print("Function returned: '{}'\n", PyUnicode_AsUTF8(result.get()));

Wynik działania kodu:

kod

Powyższe przykłady pokazują, jak łatwe jest przekazywanie danych do i z funkcji napisanych w języku Python. Inną możliwością integracji jest użycie biblioteki Boost.Python albo pybind. Zawierają one gotowe klasy kompatybilne z obiektami Pythona.

A jak wygląda sytuacja z Javą?

Java

Projekt CMake będzie wyglądał analogicznie – również trzeba dodać ścieżki z nagłówkami oraz zlinkować program z kilkoma bibliotekami z przestrzeni JNI. W systemie muszą być zainstalowane pakiety openjdk-8-jre oraz openjdk-8-jdk.

find_package(JNI REQUIRED)
#
# ...definicja projektu o nazwie w ${PRJ_NAME}...
#
target_include_directories(${PRJ_NAME} PRIVATE ${JAVA_INCLUDE_PATH} ${JAVA_INCLUDE_PATH2})

target_link_libraries(${PRJ_NAME} PUBLIC JNI::JNI JNI::AWT JNI::JVM)

Program w Javie mieści się w klasie MyClass, w pliku MyClass.class. Jego pierwsza wersja jest pokazana poniżej. Na początku zawiera tylko pole o wartości całkowitej ‘value_’ oraz konstruktor, które je ustawia. Klasa będzie rozbudowywana w miarę opisywania kolejnych zagadnień.

public class MyClass {
  public MyClass(int value)
  {
    this.value_ = value;
  }

  int value_;
}

Program należy skompilować do pliku *.java  poleceniem javac. W tym przypadku kod bajtowy znajdzie się w tym samym katalogu co plik wykonywalny C++. Taka organizacja oddziela od siebie pliki źródłowe oraz binarne.

Przygotowanie środowiska

Poniżej znajdziecie kod aplikacji C++, która ładuje środowisko Java oraz znajduje naszą klasę. Wszystkie potrzebne deklaracje zlokalizowane są w pliku nagłówkowym ‘jni.h’. Przygotowanie środowiska wymaga kilku kroków więcej w porównaniu do Pythona.

Jednym z ustawianych parametrów jest opcja ‘java.class.path’, do której jest dodany katalog pliku wykonywalnego. Jest on wyznaczany z nazwy programu przekazywanej przez pierwszy argument, czyli argv[0]. Wywołanie JNI_CreateJavaVM() inicjalizuje zarówno maszynę wirtualną Java, jak również środowisko uruchomieniowe. Wskaźnik env daje nam dostęp do całego szeregu funkcji umożliwiających interakcję z klasami.

Jedną z pierwszych operacji jest odszukanie naszej klasy za pomocą funkcji FindClass(). Jest to konieczne, aby dostać się do wszystkich elementów, również metod statycznych. Po uruchomieniu programu zasoby są czyszczone przez DestroyJavaVM().

#include <jni.h>
#include <filesystem>
#include <fmt/core.h>

int main(int argc, char *argv[])
{
    const auto bin_dir {weakly_canonical(path(argv[0])).parent_path()};
    std::string java_opt {fmt::format("-Djava.class.path={}", bin_dir.string())};
    
    JavaVMOption options {
        .optionString = const_cast<char*>(java_opt.c_str())
    };
    JavaVMInitArgs vm_args{ 
        .version = JNI_VERSION_1_8,
        .nOptions = 1,
        .options = &options,
        .ignoreUnrecognized = false
    }; 

    JavaVM *jvm{};       /* denotes a Java VM */
    JNIEnv *env{};       /* pointer to native method interface */
    JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);

    jclass java_class = env->FindClass("MyClass");
    if(java_class == nullptr)
    {
        fmt::print("Unable to find the class...\n");
        return -1;
    }
    jvm->DestroyJavaVM();
    return 0;
}

Jeżeli nie nastąpi błąd ładowania, to ten program nie wypisze nic na wyjście. Jest przedstawiony jedynie w celu pokazania, jak wygląda proces ustawiania środowiska oraz odnajdywania klasy napisanej w Javie.

public void print() {
      System.out.println("[JAVA]: Value " + this.value_);
  }
  
  public String get_string() {
    String returned_string = "Zażółć gęślą jaźń";
    System.out.println("[JAVA]: Returning string '" + returned_string + "' with len: " + returned_string.length());
    return returned_string;
  }

  public void print_string(String s) {
    System.out.println("[JAVA]: '" + s + "'");
  }

Java – pierwsze uruchomienie

Dostęp i uruchamianie funkcji napisanych w Javie jest nieco bardziej złożone, niż było to z programem w Pythonie. Należy zwrócić uwagę na dopasowanie sygnatury do wyszukiwanego obiektu. Dzieje się tak, ponieważ Java jest językiem o typowaniu statycznym. Oznacza to, że każda zmienna i metoda musi mieć typ ustalony w momencie tworzenia programu. W tym przypadku skupimy się na uruchomieniu funkcji statycznej ‘test()’

public class MyClass {
  // ... class definition ...
  static public void test() {
    System.out.println("[JAVA]: test");
  }
}

W Javie wszystkie funkcje, nawet statyczne, muszą należeć do jakiejś klasy. Żeby znaleźć taką funkcję, trzeba posłużyć się funkcją GetStaticMethodID(). Jako parametry przyjmuje ona identyfikator klasy, nazwę szukanej funkcji oraz jej sygnaturę. Składa się ona z ciągu znaków, w którym zakodowany jest zestaw przekazywanych parametrów. Wewnątrz nawiasów znajdują się parametry funkcji, a za nimi typ zwracanej wartości. Funkcja ‘test()’ nie przyjmuje ani nie zwraca żadnych wartości, dlatego nawiasy są puste, a zwracany typ ‘void’ jest symbolizowany znakiem V.  

Opis tego, jak różne typy są reprezentowane w sygnaturze, wraz z podsumowaniem w tabeli, znajdują się w dalszej części artykułu.

Jeżeli pasująca funkcja nie zostanie znaleziona, to zostanie zwrócony pusty wskaźnik ‘NULL’. Do uruchomienia znalezionej funkcji służy procedura ‘CallStaticVoidMethod()’, która przyjmuje na wejściu identyfikator klasy oraz wcześniej znaleziony identyfikator funkcji.

jclass java_class = env->FindClass("MyClass");
    jmethodID test_id = env->GetStaticMethodID(java_class, "test", "()V");
    env->CallStaticVoidMethod(java_class, test_id);

Po uruchomieniu aplikacji ‘java_test’, na ekranie powinien pojawić się następujący tekst wypisywany przez wywołaną funkcję:

kod

Bardziej dociekliwy czytelnik zwróci uwagę na specyficzną nazwę funkcji użytej do wywołania funkcji. Zawiera ona w swojej nazwie zarówno informację o tym, że wywoływana funkcja jest statyczna, oraz że zwraca typ ‘void’. Jest to kolejna rzecz, która musi się zgadzać z typem wywoływanej procedury.

Spróbujmy zatem wywołać bardziej skomplikowaną funkcję add_one(), która przyjmuje i zwraca typ całkowity.

public class MyClass {
  // ... class definition ...
  static public int add_one(int a){
    int b = a + 1;
    return b;
  }
}

Typ ‘int’ z Javy jest reprezentowany przez ‘jint’ w języku C++. Jest on typem prostym, czyli w trakcie jego tworzenia pamięć nie jest alokowana dynamicznie. Wystarczy stworzyć zmienną lokalną i można jej przypisać rezultat wywołania funkcji. Poniższy kod dodatkowo wypisuje jego rozmiar.

jmethodID add_one_id = env->GetStaticMethodID(java_class, "add_one", "(I)I");
    jint value{42};
    jint result = env->CallStaticIntMethod(java_class, add_one_id, value);
    fmt::print("Function returned {}, sizeof(jint) = {}\n", result,   sizeof(jint));

Wynik działania programu:

kod

Typy Java i ich właściwości

Podsumowanie typów Java i ich właściwości znajduje się w poniższej tabeli. Każdemu typowi jest przyporządkowany kod używany w sygnaturze oraz osobna funkcja służąca do wywołania funkcji. Poszczególne funkcje różnią się typem zwracanej wartości. Jeżeli chcemy wywołać funkcję statyczną, należy dodać do nazwy prefiks CallStatic zamiast samego Call, jak w powyższym przykładzie.

TypBity/ZnakTyp JavaKod parametruWywołanie
voidVCallVoidMethod
Boolean8/UjbooleanZCallBooleanMethod
byte8/SjbyteBCallByteMethod
char16/UjcharCCallCharMethod
short16/SjshortSCallShortMethod
int32/SjintICallIntMethod
long64/SjlongJCallLongMethod
float32jfloatFCallFloatMethod
double64jdoubleDCallDoubleMethod
Java ClassL fully-qualified-class;CallObjectMethod
Tablica [][ type (sic!)CallObjectMethod
Tab. 1 Podsumowanie typów Java i ich właściwości

Wyjaśnienia może jeszcze wymagać to, jak stworzyć sygnaturę bardziej skomplikowanej funkcji.

Typy podstawowe są reprezentowane przez pojedyncze wielkie litery. Jeżeli jest ich więcej, to występują one bezpośrednio po sobie, beż żadnych przerw czy separatorów. Jeśli parametr jest tablicą, to jego kod jest poprzedzany jednym znakiem ‘[‘. Jest to trochę nieintuicyjne, ale nie ma tam nawiasu zamykającego. Natomiast w przypadku typów obiektowych identyfikator zaczyna się od litery ‘L’, a następnie jest podana pełna nazwa typu. Jego definicja kończy się znakiem średnika.

Na przykład poniższa funkcja:  

long f (int n, String s, int[] arr);

powinna zostać wyszukana za pomocą identyfikatora:

(ILjava/lang/String;[I)J

Tworzenie Instancji Obiektu

Kolejnym krokiem jest stworzenie instancji obiektu. Jako że zaczynamy operować na typach obiektowych, to nie należy pomijać zwalniania zasobów w momencie zakończenia życia instancji obiektu.

Robi się to za pomocą funkcji DeleteLocalRef(). Analogicznie do przypadku Pythona, proponuję stworzyć prosty dealokator zasobów w postaci specjalizowanego smart pointera, który prezentuję na poniższym listingu.

auto ref_deleter = [&env](jobject obj){
        env->DeleteLocalRef(obj);
    };
    using JavaRefUniquePtr = std::unique_ptr<std::remove_pointer_t<jobject>, decltype(ref_deleter)>;

W odróżnieniu od poprzedniego przypadku, nie jest on bezstanowy. Lambda musi przechowywać wskaźnik na wcześniej zainicjalizowane środowisko uruchomieniowe env. Można by tego uniknąć, deklarując tą zmienną jako globalną, ale nie uznaję tego za rozwiązanie eleganckie.

Pod typem jobject znajduje się tak naprawdę wskaźnik, który jest usuwany szablonem remove_pointer_t. Ze względu na przechowywany stan, lambdę należy również podawać jako parametr podczas tworzenia nowych instancji wskaźników. Należy zwrócić uwagę na to, żeby dokonać wszystkich dealokacji przed wywołaniem DestroyJavaVM() na końcu programu. Taka pomyłka może się zdarzyć szczególnie w przypadku prostszych programów, które mieszczą się w jednej funkcji. Wtedy zasięg zmiennej automatycznej może sięgać poza to wywołanie, co spowoduje wystąpienie wyjątku i obszerny komunikat w konsoli.

jmethodID constructor = env->GetMethodID(java_class, "<init>", "(I)V");
    jint value{7};
    JavaRefUniquePtr java_object 
             {env->NewObject(java_class, constructor, value), ref_deleter};
    jmethodID method_id = env->GetMethodID(java_class, "print", "()V");
    env->CallVoidMethod(java_object.get(), method_id);
    // Directly access field in the class
    jfieldID field_id = env->GetFieldID(java_class, "value_", "I");
    jint current_value{env->GetIntField(java_object.get(), field_id)};
    fmt::print("Current value: {}\n", current_value);
    env->SetIntField(java_object.get(), field_id, current_value + 1);
    env->CallVoidMethod(java_object.get(), method_id);

Żeby stworzyć nowy obiekt, należy odnaleźć konstruktor, który jest funkcją specjalną o nazwie <init>. Posiada on sygnaturę zgodną z definicją – w tym przypadku przyjmuje jedną wartość typu integer. Następnie przekazuje się go do funkcji, która tworzy nowy obiekt – NewObject(). Potem wystarczy wyszukać interesującą nas metodę i ją wywołać. Potrzebna jest do tego zarówno instancja obiektu, jak również identyfikator metody.

Kolejne linie programu pokazują, jak można się dostać do pól w klasie. Służy do tego metoda GetFieldID(), która przyjmuje nazwę pola oraz jego typ zakodowany w znany już sposób. Wartość pola można pobrać albo ustawić odpowiednim wariantem funkcji Get/SetField(). Ponowne uruchomienie metody print() udowadnia to, że wartość pola się zmieniła.

Po wywołaniu, funkcja z Javy powinna wypisać na ekran:

kod

Typ znakowy string

Jednym z najpopularniejszych obiektów pojawiających się już w najprostszych programach są zmienne znakowe typu string. Przykład zwracania takiej wartości przez funkcję jest pokazany na poniższym listingu.

jmethodID method_id = env->GetMethodID(java_class, "get_string", "()Ljava/lang/String;");
    JavaRefUniquePtr obj {env->CallObjectMethod(java_object.get(), method_id), ref_deleter};
    jstring java_string = static_cast<jstring>(obj.get());
    const char* c_str = env->GetStringUTFChars(java_string, nullptr);
    fmt::print("Received from Java: '{}' \n", c_str);
    fmt::print("Java string UTFLength {}\n", env->GetStringUTFLength(java_string));
    fmt::print("Java string Length {}\n", env->GetStringLength(java_string));
    fmt::print("std::strlen {}\n", std::strlen(c_str));
    env->ReleaseStringUTFChars(java_string, c_str);

Ze stringami współpracują specjalne funkcje, ale konieczne jest wcześniejsze rzutowanie wskaźnika na typ jstring. Przykładowy string zawiera znaki spoza podstawowego zestawu ASCII, jednak nie jest to problem, gdyż Java używa kodowania UTF do jednoznacznego reprezentowania ciągów znakowych. Jeden znak na ekranie może zajmować więcej niż jeden bajt, co należy wziąć pod uwagę np. rezerwując bufor.

Z tego powodu istnieją dwa sposoby sprawdzania długości ciągu znakowego:

  • funkcja GetStringLength() zwraca ilość znaków pojawiających się na ekranie,
  • funkcja GetStringUTFChars() oblicza zajętość bufora przez daną zmienną i pokrywa się z wynikiem zwracanym przez funkcję biblioteki standardowej strlen().

Żeby dostać się do bufora z danymi stringa, należy wywołać GetStringUTFChars(), która zwróci wskaźnik typu char*. Po takim żądaniu dostępu do bufora nie należy zapomnieć o jego ręcznym zwolnieniu za pomocą ReleaseStringUTFChars().

Wynik działania programu:

kod

Własnoręczne stworzenie takiej zmiennej również nie nastręcza trudności. Wystarczy wywołać funkcję NewStringUTF(), która zwróci wskaźnik na nowo stworzoną zmienną wypełnioną przekazanym tekstem. Nie należy zapomnieć o analogicznym zwolnieniu zasobów obiektu za pomocą DeleteLocalRef(). Tak stworzony string można przekazać do funkcji w naszej klasie print_string(), która wypisze go do konsoli.

jmethodID method_id = env->GetMethodID(java_class, "print_string", "(Ljava/lang/String;)V");
    const char* c_str = "Hello from C++!";
    jstring java_string = env->NewStringUTF(c_str);
    env->CallVoidMethod(java_object.get(), method_id, java_string);
    env->DeleteLocalRef(java_string);

Na ekranie pojawi się:

kod

Podsumowanie

Artykuł poruszył jedynie podstawy zagadnienia współpracy pomiędzy C/C++ oraz językami wyższego poziomu. Moim zdaniem połączenie dwóch języków w jednej aplikacji jest zagadnieniem ciekawym samym w sobie.

Zgłębianie nietypowych tematów pozwala na zapoznanie się ze technicznymi niuansami, których może nawet nie bylibyśmy świadomi bez tego ćwiczenia. Czytanie dokumentacji oraz łączenie różnych środowisk pozwoliło mi lepiej poznać szczegóły działania zarówno języka Python jak i Java.

Jeżeli chodzi o praktyczne zastosowania prezentowanych rozwiązań, to jednym z nich jest użycie bibliotek, które akurat nie są dostępne pod konkretną platformą programistyczną. W takim wypadku ich przepisywanie i późniejsze utrzymywanie nie jest konieczne. Inną opcją jest danie możliwości użytkownikowi na rozszerzanie aplikacji poprzez pluginy napisane np. w Pythonie. Język ten jest popularny i bezpieczniejszy, ponieważ zmniejsza ryzyko popełnienia błędów związanych z zarządzaniem zasobami.

Mam nadzieję, że opisywany temat zadziałał inspirująco i pokazał nowe metody rozwiązywania różnych problemów, które napotykamy na co dzień jako programiści. Dalsze zgłębianie tych zagadnień jest ułatwione przez bogatą dokumentację.

oferty pracy

Odnośniki

***

Jeśli interesuje Cię tematyka embedded, zajrzyj również do innych artykułów naszych ekspertów 🙂

5/5
Ocena
5/5
Avatar

O autorze

Marcin Terpiłowski

Pasjonat elektroniki i oprogramowania. Jako deweloper pracuje od 10 lat, głównie w dziedzinie embedded. Stara się wykorzystać najnowsze standardy języka C++ do tworzenia jak najbardziej wydajnych aplikacji. Prywatnie miłośnik podróży i latania szybowcem

Wszystkie artykuły autora

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Może Cię również zainteresować

Dołącz do nas

Sprawdź oferty pracy

Pokaż wyniki
Dołącz do nas Kontakt

This content is available only in one language version.
You will be redirected to home page.

Are you sure you want to leave this page?