Ранее я писал статью C/C++ из Python (ctypes), в ней описывается процесс запуска на Linux. На этот раз мне понадобилось повторить это уже на Android. В этой статье речь пойдет о сборке, необходимых инструментах, механизмах отладки и установки.
Код на C ни каких изменений не претерпел. Подробнее ознакомиться с описанием кода можно по ссылке на статью приведенной в начале данного материала. С кодом на C++ возникли некоторые трудности:
I/python (13637): OSError: dlopen failed: empty/missing DT_HASH in "libstdc++.so.6" (built with --hash-style=gnu?)
Что это за ошибка будет описано ниже.
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 закончена.
По аналогии с С код на C++ так же можно будет запустить, нужно собрать все зависимые библиотеки для Android компилятором arm-linux-gnueabi-g++ с ключом -Wl,-hash-style=sysv. И установить библиотеки на телефон вместе с проектом. В данном случае для запуска библиотеки C++ необходимо собрать libstdc++.so.6.
Перейдем к 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
Данный buildozer.spec собирает приложения под архитектуру armeabi-v7a.
Здесь у меня вопрос к знающим людям, с помощью 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).
Установим на телефон через провод:
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
Находим приложение на телефоне:
Подключаемся к консоли телефона и мониторим работу приложения:
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
На экране телефона видим следующую картинку:
Все отработало как надо. Нажатие на кнопку закрывает приложение. Больше оно ни чего не умеет делать
Спасибо за внимание.
C/C++ из Python (Kivy, ctypes) на Android
Ранее я писал статью C/C++ из Python (ctypes) , в ней описывается процесс запуска на Linux . На этот раз мне понадобилось повторить это уже на Android . В этой статье речь пойдет о сборке, необходимых...
habr.com