JEP 191: Foreign Function Interface
Интерфейс вызова внешних функций
| Автор | Charles Oliver Nutter |
| Ответственный | John Rose |
| Тип | Feature |
| Область | JDK |
| Статус | Closed / Withdrawn |
| Компонент | tools |
| Обсуждение | core dash libs dash dev at openjdk dot java dot net |
| Трудоёмкость | M |
| Длительность | L |
| Создан | 2014/01/28 20:00 |
| Обновлён | 2026/07/29 18:57 |
| Задача | 8046181 |
Аннотация
Определить Foreign Function Interface (интерфейс вызова внешних функций), который может связывать нативные функции, например функции из разделяемых библиотек и ядер операционных систем, с методами Java, а также напрямую управлять блоками нативной памяти.
Цели
-
Предоставить интерфейс вызова внешних функций на уровне Java, похожий на JNA (Java Native Access) или JNR (Java Native Runtime).
-
Оптимизировать на уровне JVM вызовы нативных функций и управление нативной памятью (необязательно).
-
Поддержать будущий JSR о стандартном FFI для Java.
Что не является целью
-
Механизмы явного определения управляемой и неуправляемой структуры стандартных объектов Java.
-
Прочие связанные с процессором улучшения управления объектами Java, например управление строками кэша, GPU/векторизация и т. п.
-
Прямые изменения или улучшения самого JNI. Однако они могут появиться в ходе работы над JSR, а улучшения JVM для FFI могут потребовать внутренних изменений.
Критерии успеха
Успехом будет FFI API на уровне Java, достаточный для реализации крупных возможностей с нативной основой, таких как NIO, расширенные метаданные файловой системы, управление процессами и т. п. В идеале этот FFI API должен стать предпочтительнее, чем написание JNI-реализаций для каждой платформы, когда требуется новая возможность нативного уровня.
Мотивация
Развитию JDK (и Java в целом) за счёт добавления возможностей нативного уровня (новых структур файловой системы, других каналов ввода-вывода, новых криптографических реализаций) мешает необходимость писать код JNI, реализующий эти возможности, для каждой новой платформы. Опыт, необходимый для правильного написания JNI, не связан с опытом, необходимым для написания хорошего кода на Java или вызова нативных библиотек, но JNI был единственным способом соединить эти два мира. Этот API даст инструмент, с помощью которого любой разработчик JDK, понимающий Java и конкретную нативную библиотеку, сможет подключить эту библиотеку как API JDK для развития возможностей JDK и Java.
Этот JEP должен упростить добавление в Java и JDK новых возможностей уровня ОС, оборудования и нативных библиотек, а также предоставить стандартный FFI для Java, которым можно будет пользоваться во всём мире Java.
Описание
Многие годы у разработчиков Java и JDK был только один надёжный способ добавить возможности нативного уровня в приложения Java: Java Native Interface (JNI). Этот интерфейс управляет границей между управляемой средой JVM и неуправляемой средой нативного кода и задаёт явные протоколы для маршалинга данных, управления жизненным циклом объектов, обратных вызовов в Java (upcall), а также для нативных инструментов и API управления JVM. JNI проводит строгую и однозначную границу между управляемым и нативным кодом, но и делает пересечение этой границы невероятно мучительным даже в самых простых случаях.
Больше всего неудобств разработчикам доставляют следующие особенности JNI:
-
Если от разработчиков требуется писать код на C, им нужен опыт в мире, совершенно отличном от Java. Во многих случаях нужная нативная привязка — это простой вызов функции без побочных эффектов и почти без сложностей с маршалингом данных, но JNI в любом случае требует, чтобы разработчики были программистами на C.
-
Для использования JNI нужен опыт, которого обычно нет у типичных разработчиков на C и Java, поскольку разработчик должен хотя бы отчасти понимать, как JVM управляет памятью и кодом (жизненный цикл объектов, сложности, связанные с GC, размещение классов Java, жизненный цикл JVM). JNI позволяет сделать всё правильно, но сделать неправильно намного проще.
-
Помимо написания кода на C, разработчики должны уметь собирать этот код для каждой платформы, которую они хотят поддерживать, или предоставлять конечным пользователям подходящие инструменты, чтобы те могли сделать это сами. Это требуется, несмотря на то что сам JDK поставляется для десятков платформ, а специалисты по конкретным платформам уже проделали значительную работу, чтобы JVM работала на этих платформах и могла обращаться к нативному коду. Из-за этого требования JNI вредит принципу «написано однажды — работает везде» (write once, run anywhere) сильнее, чем FFI API, поскольку последний с гораздо большей вероятностью будет работать на многих платформах с единственным двоичным файлом, а для устранения несовместимостей обычно по-прежнему потребуется только новая сборка и новый выпуск этого одного двоичного файла.
-
Даже если написать идеальный код JNI и предоставить сборки для каждой платформы, производительность библиотек на основе JNI обычно очень низкая по сравнению с той же библиотекой, подключённой к нативному приложению. Причина — неизбежная жёсткость границы JNI между управляемым и неуправляемым кодом и полная неспособность собственных оптимизаций JVM заглянуть за вызовы JNI. Во многих случаях нисходящие вызовы (downcall) JNI простых функций можно было бы выполнять напрямую из кода Java, скомпилированного JIT, поскольку они не требуют манипуляций с памятью и не затрагивают внутреннее устройство JVM. Гарантии JNI не позволяют сделать нативные вызовы настолько лёгкими, насколько это было бы возможно.
-
Наконец, с точки зрения безопасности JNI — непрозрачная граница. JDK знает только о разрешении загрузить определённую библиотеку; он не знает, какие вызовы могут использовать функции этой библиотеки и может ли код библиотеки нарушить стабильность или безопасность JVM. Это провоцирует ошибки разработчиков JNI, которые не являются опытными программистами на C или просто не понимают аспектов безопасности на уровне JVM.
Все трудности JNI были бы решены встроенным FFI API на уровне JDK. Разработчикам Java было бы проще писать код с FFI, он не требовал бы столь глубокого знания внутреннего устройства JVM, отдавал бы предпочтение правильной реализации или быстрому отказу перед скрытыми ошибками и избавил бы от необходимости разбираться в сборке для каждой платформы.
FFI API в JDK предоставит разработчикам JDK следующее:
-
Систему метаданных для описания вызовов нативных библиотек (протокол вызова, структура списка аргументов, типы аргументов, тип возвращаемого значения) и структуры нативной памяти (размер, размещение, типизация, жизненный цикл).
-
Механизмы поиска и загрузки нативных библиотек. Эти возможности может предоставлять существующий
System.loadLibrary, или же они могут включать дополнительные улучшения для поиска двоичных файлов для конкретной платформы или версии, подходящих для хост-системы. -
Механизмы привязки, на основе метаданных, заданной пары «библиотека/функция» к конечной точке в Java, вероятно, через пользовательский интерфейс, за которым стоит связующий код, выполняющий нисходящий вызов в нативный код.
-
Механизмы привязки, на основе метаданных, определённой структуры памяти (размещение, порядок байтов, логические типы) к конечной точке в Java — либо через пользовательский интерфейс, либо через пользовательский класс; в обоих случаях за ними стоит связующий код, управляющий реальным блоком нативной памяти.
-
Необходимый вспомогательный код для маршалинга типов данных Java в нативные типы данных и обратно. В некоторых случаях для этого потребуется создать специфичные для FFI типы, чтобы поддержать разрядности и знаковость чисел, которые Java не может представить.
Кроме того, этот JEP может дополнительно поддержать перечисленные возможности с помощью следующего:
-
Осведомлённость JVM о нисходящих вызовах через FFI. Сюда могут входить: JIT-оптимизация этих вызовов, осведомлённость о нативной памяти на уровне JVM/GC, защита от недопустимых обращений к нативной памяти (ошибок SEGV) и механизмы отказа от защитных мер JNI, заведомо ненужных в конкретных случаях (границы safepoint, гарантии для блокирующих вызовов, управление жизненным циклом объектов и т. п.).
-
Инструменты, работающие во время сборки или во время выполнения, для рефлексивного сбора метаданных функций и памяти из нативных библиотек. Это облегчило бы первоначальную привязку библиотеки: привязку можно было бы генерировать на уровне Java, а не реализовывать вручную и дорабатывать для каждой платформы. Из предшествующих работ здесь можно назвать библиотеку ffi-gen для (J)Ruby, которая использует API метаданных clang (компилятора C из LLVM) для генерации кода Ruby FFI.
-
Подсистема безопасности JVM должна учитывать конкретные пары «библиотека/функция». Должна быть возможность настраивать политики безопасности, разрешающие привязку только определённых функций в определённых библиотеках, а не только грубые разрешения на уровне библиотек, которые существуют сегодня.
Уровень абстракции FFI в JDK пока не определён; как минимум он должен позволять:
-
Загружать библиотеку;
-
Вызывать функцию по некоторому смещению в этой библиотеке;
-
Передавать в библиотеку аргументы с нужным битовым представлением (разрядность, порядок байтов) и получать обратно значения с нужным битовым представлением (типизация на этом уровне не важна, кроме описания разрядности и структуры); и
-
Минимально управлять нативной памятью: выделением, освобождением, доступом, а также передачей в нативные вызовы и получением из них.
Несомненно, будет дальнейшее обсуждение того, насколько далеко следует зайти с этим API.
Альтернативы
Потребность в FFI для Java породила несколько библиотек. Среди них Java Native Access (JNA) используется шире всего, а Java Native Runtime (JNR), пожалуй, наиболее полная и развитая. Вероятно, JNR станет основой этого JEP, поскольку она реализует несколько уровней абстракции, предоставляет метаданные функций и памяти, абстрагирует привязку библиотек и функций и активно используется как минимум проектом JRuby (и его пользователями) по меньшей мере последние пять лет.
Тестирование
Чтобы протестировать этот API, нам понадобится добавить характерные нативные конечные точки (в виде кода на C), обеспечивающие достаточное покрытие всех протоколов вызова, маршалинга типов и возможностей управления памятью. Многие из этих возможностей могут уже присутствовать в существующих наборах тестов JDK и Java. У библиотеки JNR также есть готовый набор тестов, который можно будет (будет) включить в JDK.
Риски и допущения
Первоначальный автор JNR недавно взял перерыв в разработке открытого ПО. Однако он полагает, что сможет помочь нам в этой работе.
Лицензия JNR может быть совместима или несовместима со стандартными лицензионными условиями сообщества OpenJDK, но это обсуждаемо.
Зависимости
Известных зависимостей от других JEP или JSR нет. Неизвестно, чтобы он блокировал какие-либо другие JEP или JSR.
Влияние
-
Совместимость: FFI увеличит объём работы, необходимой для обеспечения кросс-платформенной совместимости JDK. Однако платформы, поддерживаемые JDK, уже поддерживают FFI через JNR.
-
Безопасность: FFI открывает для пользовательского кода возможность обращаться к недоверенным нативным функциям, а также читать и записывать обычно недоступные области нативной памяти. Эта возможность не более опасна, чем то, что дают существующие API (
sun.misc.Unsafeи др.). Можно будет явно задавать средства контроля безопасности на основе пар «библиотека/функция»; эта возможность — улучшение по сравнению с грубым контролемSystem.loadLibrary. -
Производительность/масштабируемость: производительность нативных привязок должна улучшиться, если эти привязки будут написаны с использованием FFI. Остаётся открытым вопрос, следует ли переписать существующие возможности на основе JNI с использованием FFI и в какие сроки это должно произойти.
-
Переносимость: очевидные проблемы с переносимостью есть, но их не больше, чем в текущих сборках JDK. Код JDK, использующий FFI, по-прежнему нужно будет тестировать на поддерживаемых платформах, чтобы убедиться в его работоспособности.
-
Документация: в идеале этот API станет предпочтительным способом привязки нативного кода и памяти, поэтому документация для разработчиков должна содержать всё необходимое, чтобы разработчики JDK могли использовать этот API вместо JNI.
-
TCK: будущий JSR о FFI для Java, очевидно, потребует дополнений к TCK, но в идеале они будут лишь немногим больше, чем тестирование, предусмотренное для FFI на уровне JDK.