C из Python (ctypes) на Android

от автора

main

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

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

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

Что это за ошибка будет описано ниже. Ее решение пересобрать libstdc++.so.6 компилятором arm-linux-gnueabi-gcc с ключом -Wl,-hash-style=sysv. И установить библиотеку на телефон вместе с проектом.

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.

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

arm-linux-gnueabi-gcc -c -Wl,-hash-style=sysv -g -O2 -march=armv7-a -fPIC -I./src/c  -o ./objs/test.o ./src/c/test.c arm-linux-gnueabi-gcc -Wl,-hash-style=sysv -g -O2 -march=armv7-a -shared -o ./objs/libtest.so ./objs/test.o

Флаг -march=armv7-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, но я не проверял. На этом работа с библиотекой на C закончена. Перейдем к python-у.

Python

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

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

sudo pip3 install kivy git clone https://github.com/kivy/buildozer.git cd buildozer sudo python3 setup.py install

Установив 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('./lib/libtest/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")))      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 = armeabi-v7a  # (list) Android additionnal libraries to copy into libs/armeabi android.add_libs_armeabi_v7a = /home/djvu/workspace/c_from_python/ctypes/src/python/android/lib/libtest/*.*  [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_armeabi_v7a — папка с библиотеками под конкретную архитектуру, которые будут включены в проект. Под каждую архитектуру свой ключ:
    • 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

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

Не забудем скопировать в lib/libtest/ скомпилированную C библиотеку libtest.so.
Теперь соберем apk пакет:

buildozer android debug

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

Телефон

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

main

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

adb install -r ./bin/kivy_test-0.0.1-armeabi-v7a-debug.apk

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

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

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

main

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

adb logcat | grep python

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

I/python  (23580): C I/python  (23580): Работа с функциями: I/python  (23580): ret func_ret_int:  101 I/python  (23580): ret func_ret_double:  12.123456789 I/python  (23580): ret func_ret_str:  Hello! I/python  (23580): ret func_many_args:  X I/python  (23580): Работа с переменными: I/python  (23580): ret a:  5 I/python  (23580): new a:  22 I/python  (23580): ret b:  5.12345 I/python  (23580): ret c:  X I/python  (23580): Работа со структурами: I/python  (23580): ret val1 = 19 I/python  (23580): ret val2 = 3.5 I/python  (23580): ret val3 = Z

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

main

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

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

Ссылки


ссылка на оригинал статьи https://habr.com/ru/post/656453/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *