Pull to refresh

C/C++ из Python (Kivy, ctypes) на Android

Reading time11 min
Views6.2K
main

Ранее я писал статью C/C++ из Python (ctypes), в ней описывается процесс запуска на Linux. На этот раз мне понадобилось повторить это уже на Android. В этой статье речь пойдет о сборке, необходимых инструментах, механизмах отладки и установки.


Код на C/C++ ни каких изменений не претерпел. Подробнее ознакомиться с описанием кода можно по ссылке на статью приведенной в начале данного материала.


C


test.c:


#include "test.h"

int a = 5;
double b = 5.12345;
char c = 'X';

int 
func_ret_int(int val) { 
    return val;
} 

double 
func_ret_double(double val) { 
    return val;
} 

char *
func_ret_str(char *val) { 
    return val;
} 

char
func_many_args(int val1, double val2, char val3, short val4) { 
    return val3;
} 

test_st_t *
func_ret_struct(test_st_t *test_st) {         
    return test_st;
} 

test.h:


#ifndef _TEST_H_
#define _TEST_H_

#ifdef  __cplusplus
extern "C" {
#endif

typedef struct test_st_s test_st_t;

extern int a;
extern double b;
extern char c;

int func_ret_int(int val);
double func_ret_double(double val);
char *func_ret_str(char *val);
char func_many_args(int val1, double val2, char val3, short val4);
test_st_t *func_ret_struct(test_st_t *test_st);

struct test_st_s {
    int val1;
    double val2;
    char val3;
};

#ifdef  __cplusplus
}
#endif

#endif  /* _TEST_H_ */

Процесс компиляции претерпел изменения, теперь нам понадобится arm-linux-gnueabi-gcc.


Как компилировать :


linux-gnu-gcc


aarch64-linux-gnu-gcc -c -Wl,-hash-style=sysv -g -O2 -march=armv8-a -fPIC -I./src/c  -o ./objs/test.o ./src/c/test.c
aarch64-linux-gnu-gcc -Wl,-hash-style=sysv -g -O2 -march=armv8-a -shared -o ./objs/libtest.so ./objs/test.o
aarch64-linux-gnu-strip --strip-unneeded ./objs/libtestpp.so

Флаг -march=armv8-a определяет архитектуру. Но особую важность представляют собой два флага -Wl, -hash-style=sysv, дело в том что компилятор по умолчанию компилирует с флагом -hash-style=gnu. Но такой формат hash таблицы не нравится android:


I/python  (13157): dlopen failed: empty/missing DT_HASH in "libtest.so" (built with --hash-style=gnu?)

Без Wl тоже ничего работать не будет, т.к. компоновщик соберет все с -hash-style=gnu. Пишут, что еще подойдет hash-style=both, но я не проверял.


Для архитектуры armv7-a компилятор arm-linux-gnueabi-gcc.


При использовании aarch64-linux-gnu-gcc/arm-linux-gnueabi-gcc нельзя использовать printf, в разделе проблемы написано почему.


clang


$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/clang -c -target aarch64-linux-android21 -fPIC -I./src/c  -o ./objs/test.o ./src/c/test.c
$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/clang -target aarch64-linux-android21 -shared -o ./objs/libtest.so ./objs/test.o
$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --strip-unneeded ./objs/libtest.so

$ANDROID_NDK использую от buildozer-а:


~/.buildozer/android/platform/android-ndk-r19c/

На этом работа с библиотекой на C закончена.


C++


test.cpp:


#include "test.hpp"

/**
 * Методы класса
 **/
std::string test::ret_str(std::string val) {
    std::cout << "C get ret_str: " << val << std::endl;
    return val;
}

int test::ret_int(int val) {
    std::cout << "C get ret_int: " << val << std::endl;
    return val;
}

double test::ret_double(double val) {
    std::cout << "C get ret_double: " << val << std::endl;
    return val;
}

/**
 * Обвязка C для методов класса C++
 **/

// Создаем класс test, и получаем указатель на него.
test *test_new() {
    return new test();
}

// Удаляем класс test.
void test_del(test *test) {
    delete test;
}

/*
 * Вызов методов класса.
 */

// Обертка над методом ret_str
char *test_ret_str(test *test, char *val) {
    // char * к std::string
    std::string str = test->ret_str(std::string(val));

    // std::string к char *
    char *ret = new char[str.length() + 1];
    strcpy(ret, str.c_str());

    return ret;
}

// Обертка над методом ret_int
int test_ret_int(test *test, int val) {
    return test->ret_int(val);
}

// Обертка над методом ret_double
double test_ret_double(test *test, double val) {
    return test->ret_double(val);
}

/*
 * Получение переменных класса.
 */

// Обертка для получения a
int test_get_a(test *test) {
    return test->a;
}

// Обертка для получения b
double test_get_b(test *test) {
    return test->b;
}

// Обертка для получения c
char test_get_c(test *test) {
    return test->c;
}

test.hpp:


#ifndef _TEST_HPP_
#define _TEST_HPP_

#include <iostream>
#include <string>

class test {
public:
    int a = 5;
    double b = 5.12345;
    char c = 'X';

    std::string ret_str(std::string val);
    int ret_int(int val);
    double ret_double(double val);
};

#ifdef __cplusplus
extern "C" {
#endif

    test *test_new();
    void test_del(test *test);
    char *test_ret_str(test *test, char *val);
    int test_ret_int(test *test, int val);
    double test_ret_double(test *test, double val);

    int test_get_a(test *test);
    double test_get_b(test *test);
    char test_get_c(test *test);

#ifdef __cplusplus
}
#endif

#endif

Как компилировать :


clang++


$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/clang++ -c -target aarch64-linux-android21 -std=c++17 -stdlib=libc++ -fPIC -I./src/c  -o ./objs/test.pp.o ./src/c/test.cpp
$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/clang++ -target aarch64-linux-android21 -std=c++17 -stdlib=libc++ -shared -o ./objs/libtestpp.so ./objs/test.pp.o
$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --strip-unneeded ./objs/libtestpp.so

Для С++ понадобится libc++_shared.so, уже скомпилирована для нас. Взять нужную архитектуру из $ANDROID_NDK/sources/cxx-stl/llvm-libc++/libs/ и скопировать в папку к нашим тестовыми библиотеками.


Вот и все, не забудем скопировать .so файлы в папку проекта kivy.


Проблемы С/C++


  • На aarch64-linux-gnu-gcc/arm-linux-gnueabi-gсс ругается на printf, для его работы пересобрал libc.so.6. Но все равно не заработало из-за зависимости на ld-linux-aarch64.so.1, которую ни как не смог подключить к проекту.
  • Ни как не получилось заставить C++ работать на aarch64-linux-gnu-g++/arm-linux-gnueabi-g++, только на clang++ из NDK. Из-за того что:

I/python  (13637):  OSError: dlopen failed: empty/missing DT_HASH in "libstdc++.so.6" (built with --hash-style=gnu?)

libstdc++.so.6 не стал пытаться собирать после неудачи с libc.so.6 и т.к. на clang++ заработало.


GLIBC


Как собрать glibc с помощью aarch64-linux-gnu-gcc/g++ под armv8-a. Мне это ни как не помогло, но опишу процесс на всякий случай т.к. потратил на него много времени.


git clone git://sourceware.org/git/glibc.git
cd glibc
mkdir build
cd build
export glibc_install="$(pwd)/install"
LDFLAGS="-Wl,-hash-style=sysv -march=armv8-a" ../configure --host=aarch64-linux-gnu --prefix "$glibc_install"
make -j 4
make install -j 4

Наши библиотеки в: glibc/build/install/lib


Python


Здесь нам понадобится фреймворк Kivy и buildozer — утилита для создания apk пакетов. Установку и настройку проводил по статье: Kivy. Сборка пакетов под Android и никакой магии


Установка kivy & buildozer


sudo pip3 install kivy
sudo pip3 install buildozer

Установив kivy и buildozer приступим к созданию тестовой программы. Создадим папку под нее:


mkdir android_python
cd android_python

Теперь создадим main.py, это точка запуска нашей программы.


touch main.py

И заполним его:


#!/usr/bin/python3
#-*- coding: utf-8 -*-

import os
import sys
import ctypes

import kivy
kivy.require("1.9.1")
from kivy.app import App
from kivy.uix.button import Button

# class in which we are creating the button
class ButtonApp(App):

    def build(self):
        # use a (r, g, b, a) tuple
        btn = Button(text ="Push Me !",
                   font_size ="20sp",
                   background_color = (1, 1, 1, 1),
                   color = (1, 1, 1, 1),
                   size_hint = (.2, .1),
                   pos_hint = {'x':.4, 'y':.45})

        # bind() use to bind the button to function callback
        btn.bind(on_press = self.callback)
        return btn

    # callback function tells when button pressed
    def callback(self, event):
        exit(0)

##
#  Старт.
##
if __name__ == "__main__":

    test = None
    # Загрузка библиотеки
    try:
        test = ctypes.CDLL('libtest.so')
    except OSError as e:
        print(str(e))
        exit(0)

    ###
    ## C
    ###

    print("ctypes\n")
    print("C\n")

    ##
    # Работа с функциями
    ##

    # Указываем, что функция возвращает int
    test.func_ret_int.restype = ctypes.c_int
    # Указываем, что функция принимает аргумент int
    test.func_ret_int.argtypes = [ctypes.c_int, ]

    # Указываем, что функция возвращает double
    test.func_ret_double.restype = ctypes.c_double
    # Указываем, что функция принимает аргумент double
    test.func_ret_double.argtypes = [ctypes.c_double]

    # Указываем, что функция возвращает char *
    test.func_ret_str.restype = ctypes.c_char_p
    # Указываем, что функция принимает аргумент char *
    test.func_ret_str.argtypes = [ctypes.POINTER(ctypes.c_char), ]

    # Указываем, что функция возвращает char
    test.func_many_args.restype = ctypes.c_char
    # Указываем, что функция принимает аргументы int, double. char, short
    test.func_many_args.argtypes = [ctypes.c_int, ctypes.c_double, ctypes.c_char, ctypes.c_short]

    print('Работа с функциями:')
    print('ret func_ret_int: ', test.func_ret_int(101))
    print('ret func_ret_double: ', test.func_ret_double(12.123456789))
    # Необходимо строку привести к массиву байтов, и массив байтов к строке.
    print('ret func_ret_str: ', test.func_ret_str('Hello!'.encode('utf-8')).decode("utf-8"))
    print('ret func_many_args: ', test.func_many_args(15, 18.1617, 'X'.encode('utf-8'), 32000).decode("utf-8"))

    ##
    # Работа с переменными
    ##

    print('\nРабота с переменными:')
    # Указываем, что переменная типа int
    a = ctypes.c_int.in_dll(test, "a")
    print('ret a: ', a.value)

    # Изменяем значение переменной.
    a.value = 22
    a = ctypes.c_int.in_dll(test, "a")
    print('new a: ', a.value)

    # Указываем, что переменная типа double
    b = ctypes.c_double.in_dll(test, "b")
    print('ret b: ', b.value)

    # Указываем, что переменная типа char
    c = ctypes.c_char.in_dll(test, "c")
    print('ret c: ', c.value.decode("utf-8"))

    ##
    # Работа со структурами
    ##

    print('\nРабота со структурами:')

    # Объявляем структуру в Python аналогичную в C
    class test_st_t(ctypes.Structure):
        _fields_ = [('val1', ctypes.c_int),
                    ('val2', ctypes.c_double),
                    ('val3', ctypes.c_char)]

    # Указываем, что функция возвращает test_st_t *
    test.func_ret_struct.restype = ctypes.POINTER(test_st_t)
    # Указываем, что функция принимает аргумент void *
    test.func_ret_struct.argtypes = [ctypes.c_void_p]

    # Создаем структуру
    test_st = test_st_t(19, 3.5, 'Z'.encode('utf-8'))

    # Python None == Null C
    # ret = test.func_ret_struct(None)
    # print('ret func_ret_struct: ', ret) # Если передали None, то его и получим назад
    ret = test.func_ret_struct(ctypes.byref(test_st))

    # Полученные данные из C
    print('ret val1 = {}\nret val2 = {}\nret val3 = {}'.format(ret.contents.val1, ret.contents.val2,
                                                               ret.contents.val3.decode("utf-8")))

    ###
    ## C++
    ###

    start_time = time.time()

    print("\n\nC++\n")

    # Загрузка библиотеки
    testpp = ctypes.CDLL('libs/libs_arm64_v8a/libtestpp.so')

    # Указываем, что функция возвращает указатель
    testpp.test_new.restype = ctypes.c_void_p
    # Создание класса test
    test = testpp.test_new()

    ##
    # Работа с методами
    ##

    # Указываем, что функция возвращает char *
    testpp.test_ret_str.restype = ctypes.c_char_p
    # Указываем, что функция принимает аргумент void * и char *
    testpp.test_ret_str.argtypes = [ctypes.c_void_p, ctypes.c_char_p]

    # Указываем, что функция возвращает int
    testpp.test_ret_int.restype = ctypes.c_int
    # Указываем, что функция принимает аргумент void * и int
    testpp.test_ret_int.argtypes = [ctypes.c_void_p, ctypes.c_int]

    # Указываем, что функция возвращает double
    testpp.test_ret_double.restype = ctypes.c_double
    # Указываем, что функция принимает аргумент void * и double
    testpp.test_ret_double.argtypes = [ctypes.c_void_p, ctypes.c_double]

    print('Работа с методами:')
    # В качестве 1-ого аргумента передаем указатель на наш класс
    print('ret test_ret_str: ', testpp.test_ret_str(test, 'Hello!'.encode('utf-8')).decode("utf-8"))
    print('ret test_ret_int: ', testpp.test_ret_int(test, 123))
    print('ret test_ret_double: ', testpp.test_ret_double(test, 9.87654321))

    ##
    # Работа с переменными
    ##

    # Указываем, что функция возвращает int
    testpp.test_get_a.restype = ctypes.c_int
    # Указываем, что функция принимает аргумент void *
    testpp.test_get_a.argtypes = [ctypes.c_void_p]
    # Указываем, что функция возвращает double
    testpp.test_get_b.restype = ctypes.c_double
    # Указываем, что функция принимает аргумент void *
    testpp.test_get_b.argtypes = [ctypes.c_void_p]
    # Указываем, что функция возвращает char
    testpp.test_get_c.restype = ctypes.c_char
    # Указываем, что функция принимает аргумент void *
    testpp.test_get_c.argtypes = [ctypes.c_void_p]

    print('\nРабота с переменными:')
    print('ret test_get_a: ', testpp.test_get_a(test))
    print('ret test_get_b: ', testpp.test_get_b(test))
    print('ret test_get_c: ', testpp.test_get_c(test).decode("utf-8"))

    # Указываем, что функция принимает аргумент void *
    testpp.test_del.argtypes = [ctypes.c_void_p]
    # Удаляем класс
    testpp.test_del(test)

    ButtonApp().run()

Здесь создается простая графическая программа с одной кнопкой при нажатии которой произойдет закрытие приложения. Основная задача статьи показать как запускать C библиотеки, результат работы увидим в консоли.


Так же нам понадобится файл спецификации buildozer, описывающий правила сборки пакета apk. Создаем его:


touch buildozer.spec

Заполняем:


[app]

# (str) Title of your application
title = KivyTest

# (str) Package name
package.name = kivy_test

# (str) Package domain (needed for android/ios packaging)
package.domain = com.heattheatr

# (str) Source code where the main.py live
source.dir = .

# (list) Source files to include (let empty to include all the files)
source.include_exts = py,png,jpg,jpeg,ttf,so,6

# (list) Application version
version = 0.0.1

# (list) Application requirements
# comma separated e.g. requirements = sqlite3,kivy
requirements = python3, kivy==2.0.0

# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
# requirements.source.kivy = ../../kivy
#requirements.source.libtest = lib/libtest

# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all)
orientation = portrait

# (bool) Indicate if the application should be fullscreen or not
fullscreen = 1

# (list) Permissions
android.permissions = INTERNET,WRITE_EXTERNAL_STORAGE

# (int) Target Android API, should be as high as possible.
android.api = 28

# (int) Minimum API your APK will support.
android.minapi = 21

# (str) Android NDK version to use
android.ndk = 19c

# (bool) If True, then skip trying to update the Android sdk
# This can be useful to avoid excess Internet downloads or save time
# when an update is due and you just want to test/build your package
android.skip_update = False

# (bool) If True, then automatically accept SDK license
# agreements. This is intended for automation only. If set to False,
# the default, you will be shown the license when first running
# buildozer.
android.accept_sdk_license = True

# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
android.arch = arm64-v8a

# (list) Android additionnal libraries to copy into libs/armeabi
android.add_libs_arm64_v8a = /home/djvu/workspace/c_from_python/ctypes/src/python/android/libs/libs_arm64_v8a/*.*

[buildozer]

# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
log_level = 2

# (int) Display warning if buildozer is run as root (0 = False, 1 = True)
warn_on_root = 0

# (str) Path to build artifact storage, absolute or relative to spec file
build_dir = ./.buildozer

# (str) Path to build output (i.e. .apk, .ipa) storage
bin_dir = ./bin

Здесь выделю следующие поля:


  • source.include_exts — файлы с перечисленными расширениями будут включены в проект.
  • requirements — список python модулей которые будут скачены через pip и включены в проект.
  • android.arch — архитектура под которую производится сборка (через запятую можно указать несколько)
  • android.add_libs_arm64_v8a — папка с библиотеками под конкретную архитектуру, которые будут включены в проект. Под каждую архитектуру свой ключ:
    • android.add_libs_armeabi = libs/android/*.so
    • android.add_libs_armeabi_v7a = libs/android-v7/*.so
    • android.add_libs_arm64_v8a = libs/android-v8/*.so
    • android.add_libs_x86 = libs/android-x86/*.so
    • android.add_libs_mips = libs/android-mips/*.so

Данный buildozer.spec собирает приложения под архитектуру arm64-v8a.


Здесь у меня вопрос к знающим людям, с помощью android.add_libs_arm64_v8a = /home/djvu/workspace/c_from_python/ctypes/src/python/android/libs/libs_arm64_v8a/. я ни как не смог добавить в apk пакет библиотеку libstdc++.so.6. Добавляются только с расширениями so. Подскажите как это сделать?


Теперь соберем apk пакет:


buildozer android debug

Операция очень долгая и растянется на несколько десятков минут. Так же потребуется порядка 1.5 GB свободного места, т.к. buildozer подтянет все необходимые библиотеки для сборки.
После удачного завершения в папке bin соберется пакет kivy_test-0.0.1-arm64-v8a-debug.apk.


Телефон


На телефоне нужно включить режим отладки по USB и разрешить установку через USB.
Можно сбрасывать себе пакеты через telegram (так делал на телефоне со сломанным USB).


main

Установим на телефон через провод:


adb install -r ./bin/kivy_test-*.apk

Убедимся что пакет установился и все наши библиотеки внутри:


adb shell
run-as com.heattheatr.kivy_test
ls files/app/libs


Находим приложение на телефоне:


main

Подключаемся к консоли телефона и мониторим работу приложения:


adb logcat | grep python

Запускаем и получаем следующее — C/C++ отработал без проблем:


I/python  (15497): ctypes
I/python  (15497): C
I/python  (15497): Работа с функциями:
I/python  (15497): ret func_ret_int:  101
I/python  (15497): ret func_ret_double:  12.123456789
I/python  (15497): ret func_ret_str:  Hello!
I/python  (15497): ret func_many_args:  X
I/python  (15497): Работа с переменными:
I/python  (15497): ret a:  5
I/python  (15497): new a:  22
I/python  (15497): ret b:  5.12345
I/python  (15497): ret c:  X
I/python  (15497): Работа со структурами:
I/python  (15497): ret val1 = 19
I/python  (15497): ret val2 = 3.5
I/python  (15497): ret val3 = Z
I/python  (15497): C++
I/python  (15497): Работа с методами:
I/python  (15497): ret test_ret_str:  Hello!
I/python  (15497): ret test_ret_int:  123
I/python  (15497): ret test_ret_double:  9.87654321
I/python  (15497): Работа с переменными:
I/python  (15497): ret test_get_a:  5
I/python  (15497): ret test_get_b:  5.12345
I/python  (15497): ret test_get_c:  X

На экране телефона видим следующую картинку:


main

Все отработало как надо. Нажатие на кнопку закрывает приложение. Больше оно ни чего не умеет делать ;)


Спасибо за внимание.


Ссылки


Tags:
Hubs:
Total votes 3: ↑3 and ↓0+3
Comments2

Articles