Pyjnius: PyJnius против JPype

Созданный на 16 июл. 2020  ·  27Комментарии  ·  Источник: kivy/pyjnius

Я ведущий автор JPype. В рамках обновления документации JPype я добавил PyJnius в список альтернативных кодов JPype. К сожалению, после двух часов игры с PyJnius я не смог придумать ничего, что выглядело бы как преимущество PyJnius над JPype. Каждый аспект, на который я смотрел: прокси, настройщики, обработка многомерных массивов, интеграция с javadoc, обработка сборщика мусора, буферы, интеграция научного кода (numpy, matplotlib и т. Д.), Методы, чувствительные к вызывающим, документация и даже скорость выполнения в большинстве случаев в большинстве случаев покрыты в JPype более полно, чем PyJnius. Однако, будучи автором JPype, возможно, я смотрю на аспекты, которые я ценю, а не на аспекты команды PyJnius. Можете ли вы лучше сформулировать преимущества этого проекта? В чем состоит ценностное предложение этого проекта, какова его целевая аудитория и что он нацелен на то, что альтернативы еще не охватывают?

Самый полезный комментарий

Я связался со всеми кодами мостов Java около 2 лет назад. К сожалению, код PyJnius, по-видимому, был пропущен, так как он никогда не попадал в мои поиски. Я бы пропустил это и в этом раунде, если бы я не искал страницу, на которой в прошлый раз был опубликован красивый технический пресс-релиз, и наткнулся на блог, в котором обсуждались эти два проекта. Не знаю, как я пропустил еще один активный проект в той же области в течение двух лет, но это была явно моя вина.

Похоже, вы перепутали JPype с Py4J, который является другим основным кодом моста. Они делают все, используя сокеты, что влечет за собой как преимущества, так и недостатки. Точно так же я обнаружил, что этот проект не соответствует моим требованиям.

Я не исследовал, что требуется для поддержки Android. Хотя, если у меня есть какие-то технические характеристики, это должно быть возможно. Мы не делаем ничего, кроме собственного JNI и простых старых вызовов C API Python.

Что касается подхода, JPype использует JNI исключительно для связывания JVM с Python с помощью команды «startJVM ()». Хотя следующая версия (2.0) также предоставит возможность делать обратное, когда Python можно запускать из Java. Это достигается за счет многоуровневого подхода. Существует слой Python, который содержит все классы высокого уровня, которые служат в качестве внешнего интерфейса, частный модуль CPython с базовыми классами, содержащими точки входа, слой поддержки C ++, который имеет дело со всеми преобразованиями типов и сопоставлением, а также действует как собственный модуль для библиотеки Java и библиотеки Java, которая выполняет все служебные задачи (сохранение времени жизни объектов, создание вспомогательных классов для срезов и исключений, а также экстракторы / рендеринг документации javadoc).

8 лет назад в JPype царил беспорядок. Он пытался поддерживать как Ruby, так и Python, поэтому слой C ++ представлял собой клубок оболочек, с которыми нужно было работать, а внешний интерфейс был полностью на Python, поэтому он был очень медленным. Он также был раздвоен на возвращаемые типы, поскольку поддержка numpy могла быть скомпилирована, в результате чего возвращались другие объекты. Требовалось множество классов адаптеров, таких как JException, чтобы действовать как прокси там, где собственный объект Python и Java отличался. Но все эти вопросы были решены за 3 года с тех пор, как я присоединился к проекту. Две основные цели (для меня) в JPype - предоставить синтаксис, который достаточно прост, чтобы физики были знакомы с программированием, чтобы иметь возможность использовать Java и иметь высокий уровень интеграции с научным кодом Python. Мы делаем это, заставляя те объекты, которые поддерживаются Java, иметь "все излишества" обертки объектов CPython. Вместо преобразования примитивного массива Java мы делаем примитивный массив Java новым собственным типом Python, который реализует все те же точки входа, что и numpy, и все они поддерживаются передачей буфера памяти. Таким образом, мы получаем быстрые преобразования, вызывая list(jarray) или np.array(jarray) .

Чтобы адаптировать его к Android, последовательность запуска, вероятно, потребуется переработать, а преобразующий код, который он использует для загрузки своей внутренней библиотеки, необходимо будет заменить на более традиционную модель JNI. Я уже удалил код преобразователя в следующей версии, поэтому более поздняя версия уже выполнена. Потребуется только первое.

Ключевые различия в подходе, которые я вижу, заключаются в том, что PyJnius преобразует массивы, идущие в и из. Это может показаться очень непозволительным для научного кодирования, когда передача больших массивов назад и вперед (часто никогда не конвертируемых) является предпочтительным стилем JPype. Решение о необходимости преобразования затем заставляет такие параметры, как передача по значению и передача по ссылке, но это потенциально приводит к большим проблемам, как если бы у вас был вызов с несколькими аргументами, вы выбираете одну политику для всех аргументов. Это также затруднило бы работу с многомерными массивами. (Я думаю, что такой класс адаптера, как obj.method(1, jnius.byref(list1), list2) , обеспечил бы лучший контроль, если бы это было возможно). Кроме того, JPype решил множество проблем, таких как связывание сборщика мусора, методы, чувствительные к вызывающим абонентам, и тому подобное. Если ничего другого, пожалуйста, просмотрите код JPype и посмотрите, есть ли какие-нибудь хорошие идеи, которые вы можете использовать.

Все 27 Комментарий

Спасибо, что обратились к нам!

Я мог бы неправильно вспомнить, потому что уже много лет я не смотрел на jpype, но я использовал другой подход, разговаривая с JVM через сервер (так IPC), а не с общей памятью (но ваш readme намекает на обратное, и я не вижу намеков, опровергающих это при беглом взгляде на код, так что я, вероятно, полностью ошибаюсь), и этот подход сделал его неработающим, например, на Android (что было своего рода основной причиной разработки pyjnius, хотя некоторые люди используют его на настольных платформах). С другой стороны, я не вижу особых намеков на поддержку Android в кодовой базе JPype, если только это не то, о чем идет речь в каталоге native , поскольку его jni.h, похоже, взят из AOSP?

Но, честно говоря, мы вообще не знали о JPype, когда проект был запущен, это был всего лишь шаг вперед от необходимости вручную кодировать jni-код для взаимодействия с конкретными классами Android для поддержки kivy. Я считаю, что @tito немного

Привет. Я помню имя JPype, но во время поиска, что мы могли бы использовать, я не помню, честно говоря, почему он вообще не использовался, 8 лет назад :) Единственной целью в начале было иметь возможность общаться с Android API, но без использования среднего RPC-сервера, как в то время проект P4A.

Я связался со всеми кодами мостов Java около 2 лет назад. К сожалению, код PyJnius, по-видимому, был пропущен, так как он никогда не попадал в мои поиски. Я бы пропустил это и в этом раунде, если бы я не искал страницу, на которой в прошлый раз был опубликован красивый технический пресс-релиз, и наткнулся на блог, в котором обсуждались эти два проекта. Не знаю, как я пропустил еще один активный проект в той же области в течение двух лет, но это была явно моя вина.

Похоже, вы перепутали JPype с Py4J, который является другим основным кодом моста. Они делают все, используя сокеты, что влечет за собой как преимущества, так и недостатки. Точно так же я обнаружил, что этот проект не соответствует моим требованиям.

Я не исследовал, что требуется для поддержки Android. Хотя, если у меня есть какие-то технические характеристики, это должно быть возможно. Мы не делаем ничего, кроме собственного JNI и простых старых вызовов C API Python.

Что касается подхода, JPype использует JNI исключительно для связывания JVM с Python с помощью команды «startJVM ()». Хотя следующая версия (2.0) также предоставит возможность делать обратное, когда Python можно запускать из Java. Это достигается за счет многоуровневого подхода. Существует слой Python, который содержит все классы высокого уровня, которые служат в качестве внешнего интерфейса, частный модуль CPython с базовыми классами, содержащими точки входа, слой поддержки C ++, который имеет дело со всеми преобразованиями типов и сопоставлением, а также действует как собственный модуль для библиотеки Java и библиотеки Java, которая выполняет все служебные задачи (сохранение времени жизни объектов, создание вспомогательных классов для срезов и исключений, а также экстракторы / рендеринг документации javadoc).

8 лет назад в JPype царил беспорядок. Он пытался поддерживать как Ruby, так и Python, поэтому слой C ++ представлял собой клубок оболочек, с которыми нужно было работать, а внешний интерфейс был полностью на Python, поэтому он был очень медленным. Он также был раздвоен на возвращаемые типы, поскольку поддержка numpy могла быть скомпилирована, в результате чего возвращались другие объекты. Требовалось множество классов адаптеров, таких как JException, чтобы действовать как прокси там, где собственный объект Python и Java отличался. Но все эти вопросы были решены за 3 года с тех пор, как я присоединился к проекту. Две основные цели (для меня) в JPype - предоставить синтаксис, который достаточно прост, чтобы физики были знакомы с программированием, чтобы иметь возможность использовать Java и иметь высокий уровень интеграции с научным кодом Python. Мы делаем это, заставляя те объекты, которые поддерживаются Java, иметь "все излишества" обертки объектов CPython. Вместо преобразования примитивного массива Java мы делаем примитивный массив Java новым собственным типом Python, который реализует все те же точки входа, что и numpy, и все они поддерживаются передачей буфера памяти. Таким образом, мы получаем быстрые преобразования, вызывая list(jarray) или np.array(jarray) .

Чтобы адаптировать его к Android, последовательность запуска, вероятно, потребуется переработать, а преобразующий код, который он использует для загрузки своей внутренней библиотеки, необходимо будет заменить на более традиционную модель JNI. Я уже удалил код преобразователя в следующей версии, поэтому более поздняя версия уже выполнена. Потребуется только первое.

Ключевые различия в подходе, которые я вижу, заключаются в том, что PyJnius преобразует массивы, идущие в и из. Это может показаться очень непозволительным для научного кодирования, когда передача больших массивов назад и вперед (часто никогда не конвертируемых) является предпочтительным стилем JPype. Решение о необходимости преобразования затем заставляет такие параметры, как передача по значению и передача по ссылке, но это потенциально приводит к большим проблемам, как если бы у вас был вызов с несколькими аргументами, вы выбираете одну политику для всех аргументов. Это также затруднило бы работу с многомерными массивами. (Я думаю, что такой класс адаптера, как obj.method(1, jnius.byref(list1), list2) , обеспечил бы лучший контроль, если бы это было возможно). Кроме того, JPype решил множество проблем, таких как связывание сборщика мусора, методы, чувствительные к вызывающим абонентам, и тому подобное. Если ничего другого, пожалуйста, просмотрите код JPype и посмотрите, есть ли какие-нибудь хорошие идеи, которые вы можете использовать.

@Thrameos - одна из недавних вещей, которые мы хотим объединить, - это использование лямбда-выражений Python для функциональных интерфейсов Java. См. Https://github.com/kivy/pyjnius/pull/515 ; Я не видел этого в JPype?

JPype поддерживает лямбды из функциональных интерфейсов, начиная с 1.0.0. Это было частью 30 подъемов за 30 дней марта до 1.0.

JPype пережил длительный инкубационный период. Первоначально запускался в 2004 году и продолжался до 2007 года. Затем он получил большой импульс, поскольку группа пользователей воскресила его для переноса на Python 3 примерно в 2015 году. Затем, в 2017 году, он был взят для использования в национальной лаборатории, которая перенесла его с версии 0.6. 3 до 0.7.2. В течение этого периода все усилия были сосредоточены на улучшении и укреплении базовой технологии, обеспечивающей интерфейс. Но это было окончательно завершено в марте после второго переписывания ядра, чтобы мы наконец смогли продвинуться к 1.0.0. С тех пор мы добавляли все, чего «не хватало» во время моей кампании по списку желаний «30 попыток за 30 ночей». Бэклог всего, что я просто не мог реализовать, потому что это было бы слишком много работы, наконец, было устранено (количество проблем уменьшилось с 50 до 20, побуждает пользователей спрашивать, что им нужно, и т. Д.). Таким образом, может быть ряд функций, которых вы не нашли в предыдущих версиях, которые теперь доступны. Я обрабатывал большинство запросов функций менее чем за неделю, так что все, что у меня осталось, это большая тройка (обратный мост, расширение классов в Python, возможность запустить вторую JVM).

Проект вернется в состояние дремоты, поскольку у меня уже 2 месяца из 6 месяцев, чтобы завершить код обратного моста, который позволит Java вызывать Python и генерировать заглушки для библиотек Python, чтобы их можно было использовать в качестве собственных библиотек Java. Он использует ASM для создания классов Java "на лету", так что может быть достигнута встроенная поддержка Python. Все еще не полностью интегрирован, как Jython, но, возможно, достаточно близко, чтобы не было большой разницы.

Большое спасибо за подробные объяснения, и действительно, во всяком случае, безусловно, есть идеи, которые мы могли бы использовать, и стоило бы изучить код, посмотрев на код ранее, то, что я видел, кажется довольно чистым как по качеству кода, так и по структуре. проект, так что поздравляю с проделанной работой. Вы правы насчет моей путаницы с Py4J, я думаю, когда я посмотрел на JPype, он, должно быть, был в том беспорядочном состоянии, которое вы описываете, и его использование должно было быть намного сложнее, чем PyJNIus на тот момент.

Ваши замечания о передаче по значению / преобразованию очень верны и вызвали здесь некоторые недавние обсуждения, поскольку @ hx2A изучил, как улучшить производительность, и сделал преобразование обратно в типы python необязательным (например, передача одноразового списка в java, получение его преобразованный в список java, измененный java или нет и преобразованный обратно в python, просто для сбора мусора, был определенно неоптимальным, теперь мы можем по крайней мере избежать второй части за счет использования аргументов ключевого слова, что безопасно, поскольку java не поддерживает их, поэтому нет конфликта сигнатур, но, безусловно, это немного более шумно с точки зрения синтаксиса).

Что касается возможной разницы между JPype и PyJNIus, мы можем реализовать java-интерфейсы с использованием классов python и передать их java для использования в качестве обратных вызовов, с другой стороны, нам действительно нужно было бы сгенерировать байт-код java, если бы мы хотели расширить классы java. из python, так как это требование использовать некоторый api для Android, который мы не можем охватить прямо сейчас, я не уверен, правильно ли я выхожу из вашего комментария, но, возможно, у вас нет такой возможности, чтобы классы java вызывали ваш такой код Python (с использованием интерфейсов).

JPype может реализовывать интерфейсы на Python. Просто добавьте декораторы к обычным классам Python.

from java.util.function import Consumer

@jpype.JImplements(Consumer)
class MyConsumer:
   @jpype.JOverride
   def apply(self, obj):
       pass

Используйте строку в @JImplements, если класс определен до запуска JVM, несколько интерфейсов могут быть реализованы одновременно, но все методы проверяются, чтобы увидеть, реализованы ли они. Основное отличие состоит в том, что JPype использует методы отправки (все перегрузки относятся к одному и тому же методу), а не перегруженные методы. Это потому, что нам нравится иметь возможность вызывать методы как из Java, так и из Python. Я могу добавить отдельные перегрузки, если это желаемая функция, но никто этого не запрашивал.

(Изменить: причина, по которой мы не использовали наследование Python, заключается в том, что в то время, когда это было добавлено, мы все еще поддерживали Python 2, что вызывало множество проблем с мета-классом, мы очистим его дальше, как только расширения классов будут на месте. Итак, аннотация, которая оценить один раз во время объявления были чище.)

Мы используем ту же систему для реализации настройщиков (черт побери?) Для классов.

@jpype.JImplementationFor("java.util.ArrayList")
class ArrayListImpl:
    def __getitem__(self, i):
        return self.get(i)
    @jpype.JOverride
    def addAll(self, list):
        # Decide if we need to convert or can call directly.
        ...

Мы также используем аннотации для определения неявных преобразователей, таких как «Все объекты Python Path преобразуются в java.io.File»

 @jpype.JConversion("java.io.File", instanceof=pathlib.PurePath)
 def _JFileConvert(jcls, obj):
       Paths = jpype.JClass("java.nio.file.Paths")
       return Paths.get(str(obj))

Очевидно, что использование такой логики немного медленнее, чем специальные методы C, но при этом сохраняется высокая удобочитаемость и гибкость. По мере необходимости я возвращал C критические пути, которые оказались узкими местами.

Мы также предоставляем синтаксический сахар, чтобы все было чисто. MyJavaClass@obj => приведение к MyJavaClass (эквивалент Java (MyJavaClass)obj ) или cls=JInt[:] => создание типа массива ( cls=int[].class ) или a=JDouble[10][5] => создать многомерный массив ( double[][] a = new double[10][5] ).

Я работал над прототипом расширения классов из JPype. У нас та же проблема, что и для некоторых классов Swing и других API, требующих расширения классов. Решение, которое я придумал до сих пор, - это создать расширенный класс с HashMap для каждого из переопределенных методов. Если в хэш-карте для этой точки входа нет ничего, то она передается в super, в противном случае он вызывает обработчик метода прокси. Но я решил, что это будет проще всего реализовать после завершения обратного моста, чтобы Java действительно могла обрабатывать методы Python, а не использовать метод прокси Java. Так что до работы прототипа осталось около 6 месяцев. Вы можете посмотреть на ветвь epypj (мое имя для обратного моста), чтобы увидеть, как работает обратный вызов из Java в Python, а также на шаблоны для создания вызывающего с помощью ASM для создания классов Java на лету.

Что касается управления сборкой мусора, есть несколько частей JPype, которые выполняют эту работу. Во-первых, это JPypeReferenceQueue (native / java / org / jpype / ref / JPypeReferenceQueue), который связывает жизнь объекта Python с объектом Java. Это используется для создания буферов и других вещей, когда Java требуется доступ к концепции Python на время. Второй - использование глобальных ссылок, чтобы Python мог удерживать объект Java в области видимости. Для этого требуется ссылка на сборщик мусора (native / common / jp_gc.cpp), которая прослушивает любую систему, чтобы запустить сборщик мусора, и пингует ее другой при определенных условиях (размер пулов, относительный рост). Последним прокси-серверам необходимо использовать слабые ссылки, потому что в противном случае они образуют циклы (поскольку прокси-сервер содержит ссылку на половину Java, а половина Java указывает на реализацию Python). В конце концов я собираюсь использовать агент, чтобы позволить Python проходить через Java, но это будет в будущем.

Я один из тех, кто использует pyjnius на рабочем столе, а не на android. Я не знал о JPype, когда начинал строить свой проект, но я провел некоторое исследование, чтобы увидеть, в чем различия.

Одна уникальная функция в pyjnius - вызывающая сторона может решить, включать ли защищенные и частные методы и поля. Я предпочитаю только общедоступные, но я понимаю аргумент в пользу того, что создание доступных закрытых полей и методов полезно.

Производительность имеет решающее значение для моего проекта. Я провел несколько тестов с классом ниже:

package org.pkg;

public class MyClass {

  public MyClass() {
  }

  public int number = 42;

  public float add1(float x, float y) {
    return x + y;
  }

  public float add2(float x, float y) {
    return x + y;
  }

  public float add2(int x, int y) {
    return x + y;
  }
}

В JPype:

In [1]: import jpype
   ...: import jpype.imports
   ...: jpype.startJVM()
   ...: from org.pkg import MyClass
   ...: myInstance = MyClass()
   ...:

In [2]: %timeit myInstance.number
640 ns ± 2.65 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [3]: %timeit myInstance.add1(10.3, 20.5)
2.13 µs ± 24.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [4]: %timeit myInstance.add2(10.3, 20.5)
2.19 µs ± 9.41 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

В пижнии:

In [1]: import jnius

In [2]: MyClass = jnius.autoclass('org.pkg.MyClass')

In [3]: myInstance = MyClass()

In [4]: %timeit myInstance.number
161 ns ± 0.104 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

In [5]: %timeit myInstance.add1(10.3, 20.5)
1.04 µs ± 8.16 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [6]: %timeit myInstance.add2(10.3, 20.5)
2.71 µs ± 11.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Pyjnius работает немного быстрее, за исключением перегруженного метода. Мы должны сравнить заметки о том, как решить, какой метод вызывать, когда метод перегружен. У Pyjnius есть этот механизм подсчета очков, который, кажется, добавляет много накладных расходов. JPype принимает это решение намного быстрее.

Наконец, для целей тестирования:

In [9]: def add(x, y):
   ...:     return x + y
   ...:

In [10]: %timeit add(10.3, 20.5)
82.9 ns ± 0.187 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

Конечно, разница в несколько микросекунд незначительна, но она складывается, если очень быстро делать тысячи маленьких вызовов, что мне и нужно делать.

Интеграция JPype с numpy довольно приятна и проста в использовании. Я вижу, как исследователи могут использовать это для написания сценариев, которые передают большие массивы в библиотеки Java без сложного синтаксиса. Мне также нужно передавать большие массивы, и я делаю это с помощью tobytes() и специального кода Java, который может получать байты, но, очевидно, это не так аккуратно и удобно.

К сожалению, скорость JPype - это своего рода перспектива. JPype очень оборонительный, пытаясь защититься от плохих вещей, что означает много нетривиальных накладных расходов. Это означает, например, что всякий раз, когда выполняется вызов Java, он проверяет, подключен ли поток, поэтому мы не выполняем segfault при вызове из внешнего потока, такого как IDE. Моя местная группа пользователей - все ученые, поэтому все точки входа надежно защищены от ужасных перекрестных соединений. Если с ними что-то случится (как бы безумно это ни было), значит, я потерпел неудачу. (Это объясняет 1500 тестов, включая намеренное создание плохих объектов.)

Во-вторых, скорость отдельных действий, вероятно, сильно различается при очень небольших различиях в выполняемой работе. Тривиальный пример, который вы привели, был одним из наихудших случаев. В зависимости от типа поля, к которому осуществляется доступ, в некоторых случаях может действительно измениться скорость доступа.

Для вашего примера скорости вы запрашиваете поле int.

  • В PyJnius он создает дескриптор для поиска объекта, обращается к полю, создает новый Python long и затем возвращает его.
  • В JPype он создает дескриптор для поиска объекта, обращается к полю, создает новый длинный Python, затем создает тип оболочки для Java int, копирует длинную память Python в JInt (поскольку в Python отсутствует способ создания производной целочисленный класс напрямую), затем связывает слот со значением Java и, наконец, возвращает полученный JInt.

Так что даже такая тривиальная задача, как тестирование скорости доступа к полю, на самом деле не так уж и тривиально.
Разница в том, что один возвращал длинный Python, а другой возвращал фактическое целое число Java (которое следует правилам преобразования Java), которое можно было передать другому перегруженному методу и правильно выполнить привязку. Работа по возврату типа оболочки - это намного больше, чем просто возврат типа Python, отсюда огромная разница в скорости.

Я попытался продемонстрировать это, протестировав несколько различных типов полей. К сожалению, когда я тестировал поля объекта, jnius segfaсал код "harness.objectField = harness". Я не уверен, почему именно эта перекрестная разводка вызвала проблему, но мне это не удалось. Меня не особо интересовала скорость JPype, кроме устранения грубых нарушителей, которые давали в 3-5 раз ускорение для вызова и 300 раз для доступа к определенным массивам. Но, возможно, мне следует пересмотреть и посмотреть, какие области можно улучшить. Однако я сомневаюсь, что смогу свести его к минимуму, как PyJnius, не удаляя безопасность или не удаляя контракты на возврат (что я не могу сделать). Максимально возможно увеличение скорости на 10-30%,

Что касается возможности доступа к приватным и защищенным полям, то это, конечно, возможно. Я предпочитаю, чтобы пользователи использовали отражение или другие методы внутреннего доступа, а не открывали объект напрямую. Если бы мне пришлось предоставить что-то подобное, я бы, вероятно, создал поле с именем _private которое бы содержало поля, которые не публикуются. JPype предоставляет только одну оболочку класса для каждого типа, поэтому у меня не так много средств управления мелким зерном. Таким образом, вы не могли выбрать создание класса с частным доступом, а затем создать второй объект того же типа и не получить закрытые права доступа. Я пошел по этому пути с преобразованием строк, и это была катастрофа, когда одни библиотеки выбрали одну политику, а другие - другую, что привело к несовместимости.

Я провел несколько тестов, используя список массивов.

import jpype
import timeit
jpype.startJVM()
ArrayList = jpype.JClass("java.util.ArrayList")

def pack():
    ja = ArrayList()
    for i in range(1000):
        ja.add(i)

def iter(ja):
    u = 0
    for i in ja:
        u+=i

def access(ja):
    u = 0
    for i in range(len(ja)):
        u+=ja.get(i)

def access2(ja):
    u = 0
    for i in range(len(ja)):
        u+=ja[i]


ja = ArrayList()
for i in range(1000):
   ja.add(i)

print("Pack arraylist %e"%( timeit.timeit("pack()", globals=globals(), number=1000)/1e6))
print("Iterate arraylist %e"%(timeit.timeit("iter(ja)", globals=globals(), number=1000)/1e6))
# Get is a direct call
print("Access(get) arraylist %e"%(timeit.timeit("access(ja)", globals=globals(), number=1000)/1e6))
# [] is emulated
print("Access([]) arraylist %e"%(timeit.timeit("access2(ja)", globals=globals(), number=1000)/1e6))

JPype

Пакет Arraylist 2.768904e-06
Итерировать arraylist 5.208071e-06
Доступ (получение) arraylist 4.037985e-06
Доступ ([]) arraylist 4.690264e-06

Джниус

Пакет Arraylist 3.322248e-06
Итерировать arraylist 4.099314e-06
Доступ (получение) arraylist 5.653444e-06
Доступ ([]) arraylist 7.762727e-06

Это не очень последовательная история, кроме как сказать, что они, вероятно, тривиально разные, за исключением того, что они предоставляют очень разные функции. Если все, что вы делаете, - это доступ к методам, то они, скорее всего, очень похожи. Передача массивов, которые предварительно преобразованы в JPype, происходит в 100 раз быстрее, преобразование списков и кортежей в JPype в 2 раза медленнее (в настоящее время мы не используем векторный доступ и не используем специальные обходы для кортежей). Таким образом, суть в том, что в зависимости от вашего стиля кодирования он может быть намного быстрее с тем или другим. Но тогда мои пользователи обычно выбирают JPype для простоты использования и пуленепробиваемой конструкции, а не для скорости. (Ах, кого я шучу! Они точно так же могут наткнуться на Интернет, потому что JPype оказался первой ссылкой, которую они нашли в Google.)

Что касается того, как JPype выполняет привязку методов, эта информация должна быть в руководстве разработчика / пользователя. Связывание метода начинается с предварительной сортировки списка методов для отправки в соответствии с правилами, указанными в спецификации Java, так что если что-то скрывает что-то еще, оно появляется первым в списке (код можно найти в native / java / org / jpype, поскольку мы используйте служебный класс Java для сортировки при первом создании отправки). Кроме того, каждому методу дается список предпочтений, в котором метод скрывает другой. Разрешение начинается с первой проверки каждого аргумента, чтобы увидеть, есть ли для него «слот Java». Слоты Java указывают на существующие объекты, которые не нуждаются в преобразовании, поэтому устранение их перед сопоставлением означает, что мы можем использовать прямые правила, а не неявные. Затем он сопоставляет аргумент на основе типов по 4 уровням (точный, неявный, явный, нет). Он сокращает до явного и не позволяет перейти к следующему методу. Если он когда-либо получает точное значение, он сокращает весь процесс, чтобы перейти к вызову. Если есть совпадение, это скроет методы с менее специфической привязкой. Если обнаружены два неявных совпадения, которые не были скрыты, он переходит к ошибке TypeError. Как только все совпадения исчерпаны, выполняются процедуры преобразования. Затем он освобождает глобальную блокировку Python, выполняет вызов и повторно устанавливает глобальную блокировку. Выполняется поиск возвращаемого типа и создается новая оболочка Python на основе возвращаемого типа (он использует ковариантные возвраты, поэтому возвращаемый тип является наиболее производным, а не типом из метода). Это в основном линейно с количеством перегрузок, хотя есть некоторые сложности с вариативными аргументами, но предварительно построенные таблицы означают, что он попробует foo (long, long), прежде чем он попробует foo (double, double) и попадет в (long , long) предотвратит двойное, двойное совпадение при каждом сопоставлении из-за правил разрешения методов Java. Есть еще несколько ускорений, которые я могу реализовать, но для этого потребуются дополнительные таблицы кеширования.

Я унаследовал систему заказа с сокращениями, когда начал работу над проектом в 2017 году. Я добавил метод, скрывающий кеш-память и слоты Java, чтобы избавиться от большей части наших накладных расходов.

Я оптимизировал путь выполнения для методов. Пересмотренные числа для JPype:

Пак Arraylist 2.226081e-06
Итерировать arraylist 4.082152e-06
Доступ (получение) arraylist 2.962606e-06
Доступ ([]) arraylist 3.644642e-06

Моя местная группа пользователей - все ученые, поэтому все точки входа надежно защищены от ужасных перекрестных соединений. Если с ними что-то случится (как бы безумно это ни было), значит, я потерпел неудачу.

Да, segfaults ужасны, и я получил их сотни, когда начал использовать pyjnius. Я не получал их долгое время, потому что, возможно, я решил проблемы безопасности и встроил их в свой код. Теперь все работает надежно. Я понимаю ваш вариант использования. Если ваши пользователи - ученые, работающие с объектами Java напрямую для анализа данных с помощью различных библиотек Java, segfault может привести к потере ими всей своей работы. JPype, кажется, лучше предназначен для выполнения научной работы, когда конечные пользователи работают напрямую с объектами Java через Python. Однако основной вариант использования pyjnius отличается - он связан с Android. В этом случае проблемы безопасности - это проблема разработчика, поэтому, возможно, уместно будет сделать другой выбор между безопасностью и скоростью.

Признаюсь, я не большой поклонник того, что «безопасно, пока вы наступаете на эти квадраты в таком порядке». Когда я начал работать над JPype, мне потребовалось около года, чтобы полностью проверить все точки входа, чтобы я мог передать код своей местной группе. И с тех пор я добавил еще два года API-брони. За исключением нескольких редких людей, у которых JVM не загружается (что очень трудно решить), остается несколько проблем, поскольку JPype был приведен в соответствие со стандартами производственного кода.

Что касается компромисса со скоростью и безопасностью, скорость - это здорово, но если вы получаете скорость, экономя на безопасных операциях, обычно это плохая сделка для большинства пользователей. Независимо от того, занимаетесь ли вы прототипированием кода или пишете производственную систему, необходимость останавливать работу и пытаться обойти segfault - это отвлечение, с которым пользователи не должны сталкиваться.

Если кто-то захочет дать мне несколько примеров того, как протестировать JPype на эмуляторе Android, я смогу увидеть, как внести необходимые изменения.

Для использования на android мы упаковываем pyjnius как искусство дистрибутива python, созданного python-for-android (часто используя buildozer в качестве более простого интерфейса к нему, но это то же самое), а затем создаем приложение python, которое поставляет этот дистрибутив, тогда ваш код python может импортировать pyjnius или любую другую библиотеку python, которая была построена в дистрибутиве python, когда пользователь запускает приложение.

Таким образом, первым шагом является компиляция jpype в дистрибутив, что выполняется p4a (https://github.com/kivy/python-for-android/), когда вы сообщаете ему, что это часть ваших требований, обычно ( но не всегда) необходим «рецепт», чтобы объяснить p4a, как создавать библиотеки, не являющиеся чистым питоном, а для pyjnius можно найти здесь https://github.com/kivy/python-for-android/blob/ develop / pythonforandroid / recipes / pyjnius / __ init__.py в качестве примера. Если вы используете buildozer, вы можете использовать параметр p4a.local_recipes в buildozer.spec чтобы объявить каталог, в котором можно найти рецепты требований, поэтому вам не нужно разветвлять python-for-android для использовать свой рецепт.

Я бы посоветовал использовать buildozer, поскольку он автоматизирует больше вещей https://buildozer.readthedocs.io/en/latest/installation.html#targeting -android, и вы все равно можете настроить свои локальные рецепты, чтобы попробовать что-то. Первая сборка займет время, так как для нее нужно собрать python и ряд зависимостей для arm, и для этого нужно будет загрузить android ndk и sdk. Вы, вероятно, можете использовать для приложения загрузочную программу kivy по умолчанию и создать приложение вроде «привет, мир», которое будет импортировать jpype и просто отображать результат некоторого кода на этикетке или даже в logcat с помощью print, я не помните, как хорошо kivy работает в эмуляторе Android, я никогда не использовал это, но я думаю, что некоторые пользователи использовали, и с настройкой ускорения он должен работать, afaik, иначе вы могли бы использовать оболочку sdl2 или веб-просмотр, и использовать флягу или Бутылочный сервер для отображения вещей, я бы сначала попробовал с kivy, так как он на сегодняшний день является наиболее протестированным.

Вам понадобится Linux или OSX-машина (виртуальная машина в порядке, а WSL в Windows 10 в порядке) для сборки для Android.

Если вы начнете работать над jpype-рецептом для python-for-android, было бы желательно открыть текущий PR по этому поводу для любого обсуждения, которое может возникнуть. Было бы здорово, если бы это сработало, особенно если оно действительно может устранить некоторые давние ограничения pyjnius. Как обсуждалось ранее в потоке, pyjnius по существу покрывает наши основные требования для использования kivy, но у него недостаточно возможностей для разработки, чтобы значительно выйти за рамки этого.

@inclement Я установил PR для порта Android в jpype-project / jpype # 799. К сожалению, я не совсем уверен, что делать дальше. Кажется, он пытается запустить gradle, что на самом деле не правильный путь сборки.

Действия, которые необходимо выполнить, следующие:

  • [x] Включить все файлы jpype / *. py в сборку (или их предварительно скомпилированные версии).
  • [x] Запустите Apache ant на native / build.xml и поместите полученный файл jar в любое место, где к нему можно будет получить доступ.
  • [x] Включите файл jar (или эквивалент) в сборку.
  • [x] Скомпилируйте код C ++ из native / common и native / python в модуль с именем _jpype, который будет включен в сборку.
  • [x] Включите файл main.py, который просто запускает интерактивную оболочку, чтобы мы могли проверить это вручную.
  • [] В будущем мне нужно будет включить «ASM» или что-то подобное для Android, чтобы я мог загружать динамически созданные классы.
  • [x] Исправьте код C ++, чтобы он использовал настраиваемую загрузочную программу для загрузки JVM и сопутствующего файла jar, а также подключал все собственные методы.
  • [] Исправьте jvmfinder чем-то, что работает на android, и "startJVM" вызывается автоматически, а не в начале main.
  • [] Исправьте org.jpype, чтобы система навигации jar (так работает импорт) могла работать на android.

Я просмотрел некоторые документы, но ничего особенного в том, как это сделать, не было. Макет моего проекта несколько отличается от обычного, поскольку мы не помещаем все в основной модуль (поскольку на самом деле мы создаем 3 модуля, которые составляют систему. Jpype, _jpype и org.jpype.) Мне, вероятно, понадобится специальный рецепт для выполните все эти действия, а также отключите нежелательные шаблоны, такие как запуск gradle (если только он не делает что-то полезное, о чем я не могу сказать).

Кажется, он пытается запустить gradle, что на самом деле не правильный путь сборки.

Gradle - это инструмент сборки, используемый на заключительном этапе упаковки APK, он, вероятно, не связан с вашим включением jpype.

Включите все файлы jpype / *. Py в сборку (или их предварительно скомпилированные версии).

В общем, если jpype по сути работает как обычный модуль Python, то ваша первая попытка рецепта, вероятно, делает большую часть тяжелой работы - CppCompiledComponentsPythonRecipe выполняет что-то вроде запуска python setup.py build_ext и python setup.py install используя среду NDK. Это должно установить пакет jpype python в среде python, которая создается для включения в приложение.

Включите файл jar (или эквивалент) в сборку.

Вероятно, это дополнительный шаг, который потребуется сделать в рецепте, он будет сводиться к копированию вашего jar-файла (или того, что вам нужно) в какое-то подходящее место в проекте Android, который создается python-for-android.

Скомпилируйте код C ++ из native / common и native / python в модуль с именем _jpype, который будет включен в сборку.

Если это обрабатывается файлом setup.py, это уже должно работать, но может потребоваться некоторая настройка. Если это не так, вы можете включить свои команды компиляции в рецепт (и настроить их для среды Android, установив соответствующие переменные env, как вы увидите, используя self.get_env в других рецептах).

Исправьте код C ++, чтобы он использовал настраиваемую загрузочную программу для загрузки JVM и сопутствующего файла jar и подключения всех собственных методов.

Я надеюсь, что эта часть будет довольно простой, просто в зависимости от использования правильной функции интерфейса Android JNI. Мы делаем это несколько хитроумно, исправляя pyjnius, а не выполняя некоторую соответствующую условную компиляцию, поскольку разные вспомогательные библиотеки предоставляют разные обертки, как показано, например, этим патчем . Эта сложность не должна повлиять на вас, вы можете просто вызвать правильную функцию android api.

Подключите jvmfinder к чему-то, что работает на android, и "startJVM" вызывается автоматически, а не в начале main.

Я недостаточно знаком с JVM, чтобы действительно знать, но я думаю, что Android хочет, чтобы вы всегда получали доступ к существующей jvm, и вы не можете запустить новый экземпляр. Будет ли это иметь значение (или это просто неправильно?)?

Запустите Apache ant на native / build.xml и поместите полученный jar-файл где-нибудь, где к нему можно будет получить доступ.

Я также не уверен в этом из-за незнания, это просто шаг внутренней сборки jpype? Я не уверен, как взаимодействуют версии java, но полагаю, что использование системного муравья здесь подойдет.

Он предупредил, что buildozer не уважает setup.py, поэтому он сразу перешел с самого верха на шаг gradle. Следовательно, необходимо добавить эти средние шаги вручную. Наш setup.py выполняет ряд настраиваемых шагов компиляции, таких как создание файла хуков для слияния файла jar с dll, которые, вероятно, здесь не применимы, поэтому было нормально, что он был пропущен, но он также не смог найти файлы cpp и java. которые определены в setup.py. Частично проблема в том, что мой файл setup.py был настолько большим, что мне пришлось разделить его на модуль setupext.

Думаю, сначала мне нужно выяснить, как я могу запустить чистую команду, чтобы я мог запустить процесс один раз и показать журнал. (Я запустил его в первый раз, когда играл. Так что у меня нет чистого журнала.) Также мы хотели бы перенести этот разговор в PR, чтобы мы могли больше оставаться в теме для обсуждения.

FWIW Мне удалось перенести ядро ​​моего проекта, не относящегося к Android, на JPype. Было две незначительных трудности или отличия:

  1. Кажется, что classpath kwarg в jpype.startJVM() игнорируется, что бы я ни делал. Однако функция addClassPath() действительно работала. Разработчикам, которые заботятся о порядке пути к классам, возможно, потребуется внести некоторые изменения по сравнению с тем, как они использовали jnius_config.add_classpath() .

  2. Реализация интерфейсов Java работает отлично, но немного отличается. В моем коде была функция, которая возвращала список строк Python. Оглядываясь назад, я, вероятно, должен был преобразовать каждую строку в строку Java, но я не подумал об этом и заставил определение функции интерфейса возвращать список объектов Java, чтобы заставить это работать. Это отлично работает в pyjnius, что фактически делает строки Python подклассом объектов Java. Это не работает в JPype, но я легко исправил это, преобразовав строки с классом JPype JString .

Написание @JOverride для реализованных методов намного проще, чем код типа @java_method('(Ljava/lang/String;[Ljava/lang/Object;)V') .

Как только я разобрался с этими двумя проблемами, все в основном работало нормально. У меня есть некоторый код, передающий массивы байтов, который придется изменить, но JPype хорошо поддерживает это. Кроме того, неявные преобразования типов JPype очень удобны и могут заменить множество декораторов, которые мне приходилось разбрасывать повсюду.

@Thrameos Я уважаю вашу решимость. Удачи, чтобы JPype работал на Android.

Спасибо за комментарии.

Не уверен, что может быть не так в пути к классам, хотя, если бы я догадывался, это было бы то место, откуда вы начинали Python. Иногда люди запускают JVM из модуля, и поскольку правила для путей Java и путей Python по отношению к начальному каталогу различны, это может привести к пропуску jar-файлов. (В руководстве пользователя есть раздел, посвященный этой проблеме).

Я использую функцию classpath ежедневно, и это часть наших тестовых шаблонов. Так что, если пути к классам не соблюдаются, это приведет к довольно серьезным сбоям. Тем не менее, вы не будете первым, у кого возникнут трудности с поиском волшебного набора местоположений и абсолютных путей, необходимых для работы сложной схемы.

Вот пример, в котором я запускаю свою тестовую систему из каталога Py относительно области разработки.

import jpype
import os

devel = os.path.dirname(__file__)
devel = os.path.join(devel, '..', '..')
devel = os.path.abspath(devel)  # Notice that I converted the path to absolute so that it doesn't matter where the
# PWD of Java will be when this script is called.   Otherwise, if I import this from a different location it will use the
# original PWD and Java will find nothing in the classpath.

classpath = [
    '%s/gov.llnl.math/dist/*' % devel,
    '%s/gov.llnl.rdak/dist/*' % devel,
    '%s/gov.llnl.rnak/dist/*' % devel,
    '%s/gov.llnl.rtk/dist/*' % devel,
    '%s/gov.llnl.rtk.gadras/dist/*' % devel,
    '%s/gov.llnl.rtk.response/dist/*' % devel,
    '%s/gov.llnl.utility/dist/*' % devel,
    ]

jpype.startJVM(classpath=classpath, convertStrings=False)

Относительные и абсолютные пути работают, но это во многом зависит от того, с чего вы начнете. AddClassPath имеет специальную магию, чтобы убедиться, что все пути относятся к местоположению вызывающего. Я предполагаю, что такая же логика необходима в аргументах ключевого слова classpath.

Я вижу проблему с возвратом списка строк. Это будет зависеть от типа возврата в отношении поведения, которое запускается. Если бы метод был объявлен как возвращающий String[] я ожидал бы принудительно включить каждую строку Python в Java при возврате. Если бы метод был объявлен как возвращающий List<String> возникла бы проблема, поскольку дженерики Java отключили его, так что это было бы List<Object> . В зависимости от того, как JPype просматривает список, он может или не может пытаться преобразовать, но это должно быть определено поведением, поэтому я проверю его. Безопасное решение - это список, который должен уметь конвертировать все элементы для возврата.

Не уверен, что может быть не так в пути к классам, хотя, если бы я догадывался, это было бы то место, откуда вы начинали Python. Иногда люди запускают JVM из модуля

Вот что я делал!

Я использую функцию classpath ежедневно, и это часть наших тестовых шаблонов. Так что, если пути к классам не соблюдаются, это приведет к довольно серьезным сбоям. Тем не менее, вы не будете первым, у кого возникнут трудности с поиском волшебного набора местоположений и абсолютных путей, необходимых для работы сложной схемы.

Да, я подумал, что вы сразу заметите это, и что я не был первым, у кого возникла эта проблема. Спасибо за образец кода, он полезен. В результате я внес некоторые улучшения в свой код.

Безопасное решение - это список, который должен уметь конвертировать все элементы для возврата.

Это именно то, что я сделал, и теперь он работает правильно. Спасибо!

Несколько лет назад я сравнил py4j и jnius и обнаружил, что jnius работает намного быстрее.

Когда я когда-либо видел, что jpype упоминается в местах, я всегда предполагал, что это относится к - http://jpype.sourceforge.net/, и держался подальше от него, потому что он казался неподдерживаемым (странным образом не видел новый проект)

Прочитав эту ветку, я снова попробовал свои тесты (мой основной тест на скорость - это чтение и оценка файлов PMML) - и обнаружил, что jpype был довольно производительным.
Некоторые результаты:
https://gist.github.com/AbdealiJK/1dd5b7677435ba22f9ab3e26016bb3e7

# jpype
# createjvm: 0.550s
# loadmodel: tot=1.466451 max=1.064521s avg=0.014665s
# fields   : tot=0.019881 max=0.009795s avg=0.000199s
# score    : tot=0.033356 max=0.023338s avg=0.000334s

# jnius
# createjvm: 0.249s
# loadmodel: tot=1.773011 max=1.385274s avg=0.017730s
# fields   : tot=0.039058 max=0.012234s avg=0.000391s
# score    : tot=0.067590 max=0.031904s avg=0.000676s

# py4j
# createjvm: 0.222s
# loadmodel: tot=0.616913 max=0.027464s avg=0.006169s
# fields   : tot=0.699152 max=0.026426s avg=0.006992s
# score    : tot=0.389583 max=0.017620s avg=0.003896s

Честно говоря, интерфейс API JPype был написан на Python (в отличие от CPython) до марта 2020 года. Таким образом, существует множество старых тестов, показывающих, что он был довольно медленным для того, что предлагал. Просто было так много проблем с серверной частью, которые нужно было решить с помощью управления памятью, удаления целых многослойных оболочек для поддержки Ruby, многопоточности и тому подобного, что скорость была последней, над чем нужно было работать.

На данный момент это должно быть довольно быстро. Но если вы найдете что-то, что требует дополнительной работы по сравнению с другими пакетами, просто отметьте проблемы. Из-за контрактов возврата есть некоторые ограничения, но основные пути для вызова, передачи массивов и буферизации памяти на этом этапе довольно проработаны.

Я также получаю хорошие результаты с JPype. В частности, мне выгодна интеграция с массивами numpy. Мне удалось добавить новые функции, которые я не мог делать раньше, что позволило мне существенно повысить производительность.

Если кто-нибудь может быть настолько любезен, чтобы попытаться обновить инструмент kivy-remote-shell, чтобы получить его для сборки с текущим Python3 и buildozer и улучшить инструкции, чтобы они были более пошаговыми, это помогло бы с усилиями по переносу JPype сильно. Я выполнил все необходимые шаги до этапа загрузки и тестирования. Но без среды для завершения процесса начальной загрузки будет сложно добиться прогресса. Я могу попробовать еще раз обновить инструмент удаленной оболочки в эти выходные (и, возможно, удастся), или заинтересованная сторона, лучше знающая kivy, может выполнить эту предварительную задачу, а затем я могу потратить выходные, выполняя техническую работу, которую я наиболее квалифицирован для выполнения . Хотя я свободно предлагаю свое время, чтобы помогать другим, это ограниченные ресурсы, и любая работа, которую я выполняю по портированию Android, представляет собой задержку моста Python от Java, что также интересует ряд других людей.

Я надеюсь, что усилия по переносу Android помогут избежать попытки переноса PyPy, когда я потратил несколько недель на переработку основного кода, чтобы иметь возможность обрабатывать различия, но затем столкнулся с технической проблемой, когда тривиальная разница в объектной системе вызвала ошибку, и я не смог найти никого, кто мог бы помочь мне отследить, как отлаживать отчет об ошибке, созданный в сгенерированном коде. Хотя я не плачу о пролитом молоке, и все эти усилия были направлены на улучшение кода JPype до других значимых способов, в конце концов те пользователи, которые хотели использовать JPype, остались в покое. Если эта попытка не удастся, не все потеряно, так как я вернусь к ней, но как только что-то окажется в конце очереди, мне трудно вернуться к ней в течение 6 месяцев, если кто-то не сможет найти время, чтобы помочь мне. .

Информация о ходе работ для заинтересованных сторон.

Я успешно загрузил JPype из pythonforandroid и смог протестировать базовую функциональность. Хотя некоторые из расширенных функций могут быть недоступны из-за различий в JVM, я считаю, что подавляющее большинство JPype будет доступно для использования на платформе Android. Перенос занял некоторое время, так как требовал некоторых обновлений для проектов buildozer и pythonforandroid (поэтому много читал исходный код и просил о помощи). Большое спасибо разработчикам за то, что они так отзывчивы, что я смог завершить самую сложную часть процесса за один уик-энд. Без вашего участия это было бы невозможно. Я поместил соответствующие изменения как PR, но, глядя на отставание в PR, может пройти некоторое время, прежде чем они появятся на рассмотрении. Теперь, когда у меня есть ключевые технические характеристики, которые мне нужны, я смогу интегрировать их и иметь рабочий код выпуска где-то около JPype, номинально 1.2 в календаре на позднюю осень. Я могу продвигать его вперед, если есть большой интерес пользователей, но он конкурирует с Python из Java, что является важной функцией для других проектов.

Если кто-то хочет помочь ускорить работу, следующим сложным шагом будет выяснить, как создать образ докера со всем на месте с частично созданной системой, чтобы мы могли выполнить сборку, загрузить эмулятор и запустить тестовый стенд. андроида на лазурных конвейерах (или какой-либо другой системе CI). Как только у нас будет работающий CI, который может определять, что работает, а что нет, мы будем намного ближе к возможности развертывания в качестве стабильного программного обеспечения. Не уверен, что это должно быть размещено в проекте JPype или у нас должен быть отдельный тестовый проект Android.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

stania picture stania  ·  6Комментарии

ghost picture ghost  ·  3Комментарии

cmacdonald picture cmacdonald  ·  20Комментарии

Hukuta picture Hukuta  ·  5Комментарии

cthoyt picture cthoyt  ·  11Комментарии