Сучасна проблема Java і С++ або чому в Project Panama досі немає С++ інтеграції
Від автора
Всім привіт! Мене звати Денис Макогон. Я є cloud-архітектором і членом команди Java Developer Relations в Oracle. У своїй повсякденній роботі я багато стикаюся з рішеннями, написаними мовами, «не рекомендованими» АНБ для використання, — C, C++. І час від часу я займаюся саме створенням «middleware»-технологій-фреймворків, побудованих на основі Project Panama, які поєднують нативний код і Java, як, наприклад, ось така бібліотека.
У попередніх матеріалах про Project Panama ми вже говорили про Foreign Function & Memory (FFM) API (1, 2, 3, 4). З того часу FFM API встиг перейти з preview у стандартний API разом із релізом Java 22, а питання «чому б просто не зробити так, щоб jextract зчитував усе з C++ header-файлу?!» нікуди не зникло.
Цього разу хочу розібрати, у чому ж складність використання C++ коду в Java-застосунках. Ми розберемо усі аспекти детально, тож беріть каву і погнали.
Project Panama: що саме він вирішує
Project Panama — це великий напрямок в OpenJDK, який робить взаємодію Java з нативним кодом природнішою і більш зручною, аніж JNI. Для цього «deep-dive»-у важливі дві його частини:
- Foreign Function & Memory API: Java API для виклику нативних функцій і безпечнішої роботи з пам’яттю поза Java heap (аніж sun.misc.Unsafe);
jextract: (JDK code tools) генератор Java-коду за C header-файлом, що створює однозначне відображенняC-коду в Java code (враховуючи всі аспекти і експортовані символи: макроси, функції, структури, масиви і так далі, тобто все що експортується).
FFM API дає доступ до наступних класів: Linker, FunctionDescriptor, MemoryLayout, MemorySegment та Arena. Тобто за допомогою цього API на Java можна описати приблизно наступне: у «зовнішній бібліотеці» (.so, .dll, .dylib) знайти функцію з певною назвою, відповідним порядком і кількістю аргументів, певним return type і відповідною calling convention (стосується функцій із варіативною кількістю аргументів як, наприклад, сімейство функцій C fprintf) — і як результат отримати MethodHandle для її виклику.
Наприклад, для класичного C API на кшталт strlen низькорівневий варіант виглядає так:
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
void main(String[] args) {
var linker = Linker.nativeLinker();
var strlen = linker.defaultLookup()
.find("strlen")
.orElseThrow();
var strlenHandle = linker.downcallHandle(
strlen, FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
try (var arena = Arena.ofConfined()) {
var text = arena.allocateFrom("Привіт, Panama!");
long length = (long) strlenHandle.invokeExact(text);
}
}
Це вже не JNI: немає native-методу, окремого JNI glue-коду, JNIEnv* і ручного маркування кожного Java-об’єкта. Але FFM API не скасовує головного правила native interop: Java має точно знати Application Binary Interface (ABI) функції, яку викликає. Для одного strlen це легко. Для кількох сотень функцій, size_t писати FunctionDescriptor ручками — вже не дуже й ефективно. Тут і з’являється jextract.
jextract: автоматичний перекладач C ABI у Java
Як я вже писав у минулій статті, jextract читає C header-файл, пропускає його через Clang (llvm) і генерує Java-обгортки, які всередині використовують FFM API для всього, що можна експортувати з header-файлу. Цей інструмент може побудувати memory layouts для jextract не є C++ binding generator і на те є вагомі причини та перепони. Припустимо, ми маємо такий header:
// math_api.h #include <stdint.h> int32_t sum_i32(int32_t left, int32_t right);
Тоді команда виглядає приблизно так:
jextract \ --target-package dev.silo.math \ --output src/generated/java \ --library :/absolute/path/libmath_api.dylib \ math_api.h
Після цього Java-код не мусить додатково описувати адресу функції, її дескриптор або int32_t. Згенерований клас уже містить потрібні MemoryLayout і MethodHandle. Це не означає, що jextract перетворює C API на ідеальний Java SDK для AutoCloseable для MethodHandle, нормальний обробник помилок, перевірка return value та зручні Java-типи (при переході від згенерованого коду до звичних і зручних класів та record-ів). Але саме прошарок C header --> Java FFM jextract робить чудово, занадто якісно!
І тут постають питання: а що із C++? Чому jextract лається на вміст hpp-файлів? І чому Java-код генерується лише для extern "C"?
Що не так із підтримкою C++
Точніше буде сказати так: FFM уміє викликати функцію за відомим ABI, але ані FFM, ані jextract не надають загального, переносного контракту для C++ ABI. На перший погляд, C++ header дуже схожий на C header-файл:
namespace geometry {
class Point {
public:
Point(double x, double y);
double distance(const Point& other) const;
};
}
Clang чудово розуміє цей код. Проблема не в тому, щоб розпарсити синтаксис, а в тому, щоб безпечно і стабільно викликати скомпільований бінарний код з іншого середовища виконання (JVM, у даному контексті). Із C ситуація значно простіша: є широко підтримувані платформозалежні ABI для scalar types, pointers, C struct та functions. У C++ поверх цього з’являються деталі, для яких немає єдиного стандартного бінарного контракту і ось чому:
- спотворення імен (mangling) і перевантаження функцій;
int sum(int left, int right); double sum(double left, double right);
- layout класу та memory paddings — фактичне розташування полів об’єкта в пам’яті. Для простої
C-структури воно зазвичай очевидне, а для C++ класу компілятор може додати приховані сегменти даних та змінити розташування через успадкування.
struct Example {
char flag; // 1 байт
int value; // часто почнеться з offset 4, а не 1
};
- virtual table — якщо клас має хоча б один віртуальний метод, то об’єкт часто містить прихований покажчик на vtable (
vptr), зазвичай на початку; стандарт C++ не гарантує точне місце чи навіть конкретне представлення.
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
- множинне наслідування. У цьому випадку
Report*не обов’язково має ту саму адресу, що йSerializable*. Для переходу до другого базового класу C++ може змістити покажчик на кілька байтів. Це особливо складно для віртуального наслідування.
class Printable { /* ... */ };
class Serializable { /* ... */ };
class Report : public Printable, public Serializable { /* ... */ };
- правила передачі/повернення класів за значенням. Для умовних
Point make_point()абоvoid process(Image image)компілятор сам вирішує, чи передавати дані через регістри, стек або прихований покажчик на пам’ять для return value. Це залежить від розміру класу, ABI, конструктора копіювання/переміщення та платформи. Java не може надійно вгадати цей calling convention. - exceptions і stack unwinding. Коли C++ код кидає помилку, рантайм шукає відповідний
catchі «розмотує»stack, викликаючи деструктори локальних об’єктів. Це потребує сумісних compiler runtime (частина стандартного середовища C++, яка реалізує винятки, RTTI, виклики деструкторів тощо) та exception metadata (службова інформація в скомпільованому коді, яка описує, як «розмотати» стек для конкретної функції). Ні в якому випадку exception не повинен перетинати C ABI або FFM downcall, розв’язання проблеми повинне відбуватися на тому ж рівні, що й місце виникнення помилки. std::string,std::vector,smart pointersта інші STL-типи. Це C++ класи з прихованимlayout, власними правилами ownership і залежністю від реалізації стандартної бібліотеки темплейтів. Наприклад,std::stringможе використовувати small-string optimization, аstd::vectorмістить специфічні для реалізації покажчики. У той же часstd::unique_ptrмає transfer-of-ownership семантику. Усіх їх не можна змоделювати у Java за допомогоюMemoryLayoutабоC-структур «на око».- templates: один template — це не одна функція, а потенційно багато спеціалізованих аспектів імплементації (ті ж C++ вектори,
std::vector<typename T>, то й реалізація буде типовою). - відмінності між компіляторами, версіями стандартної бібліотеки темплейтів, цільовими платформами та флагами, вказаними при збірці.
Навіть якщо два компілятори нібито використовують один ABI, дрібниці мають значення: libc++ чи libstdc++, debug iterator mode, exception flags, ABI макроси, архітектура, версія бібліотеки темплейтів. Отже, я сподіваюся, вже зрозуміло, що std::vector<int> не є
Окремо варто розвіяти популярне непорозуміння:
extern "C" void process(std::vector<int>& values);
Навіть за наявності конструкції extern "C" аргумент такої функції все ще має тип std::vector<int>&. Його layout, життєвий цикл і семантика лишаються C++ специфічними. На жаль, така декларація не стає C API й не стає коректним входом для jextract.
Отже, повертаючись до теми, правильно ставити питання не «коли Panama підтримає C++?», а «який стабільний ABI я готовий підтримувати між Java і конкретною C++ бібліотекою?». І для більшості застосунків відповідь одна — маленький C ABI, реалізований C++ кодом.
Перехідний адаптер C++ у C: не обхід, а межа відповідальності
Перехідний адаптер від C++ до C на основі opaque handles концептуально є таким собі підходом, який, з одного боку, компілюється як C++, включає оригінальні .hpp-файли та викликає C++ API звичайним способом. З іншого — експортує зрозумілі extern "C" функції, які можна використати в jextract, а також через Linker зможе використати в Java-застосунках. Такий адаптер гарантовано не призведе до спотворення типів C++, а лише впровадить «щось» посередині між двома прямо-несумісними системами. Схема має такий вигляд:
Java application │ │ FFM API / generated jextract bindings ▼ C header: scalars, buffers, C POD, opaque handles │ ▼ C++ adapter (.cpp): ownership, conversions, exception boundary │ ▼ Original C++ library: classes, templates, STL, exceptions
Це важлива архітектурна межа. Java не повинна знати layout типів C++ (системних і користувацьких). Натомість C++ адаптер сам знає, як правильно створити об’єкт, викликати його метод, зловити exception і повернути результат у формі, яку можуть зрозуміти C та Java. Для C++ об’єкта C header зазвичай декларує лише opaque handle та операції над ним:
typedef struct engine_session_handle engine_session_handle; typedef int32_t engine_status; engine_status engine_session_create(int32_t threads, engine_session_handle **out_session); engine_status engine_session_process(engine_session_handle *session, const uint8_t *input, size_t input_size, int32_t *out_result); void engine_session_release(engine_session_handle *session);
Наприклад, умовний об’єкт engine::Session, його керування пам’яттю через std::unique_ptr і обробка винятків лишаються всередині C++ реалізації. C і Java бачать лише сам покажчик, а не тип за ним. Вони можуть передати його назад у той самий адаптер та викликати release на C++ об’єкті, але не можуть самостійно створити цей об’єкт, подивитися на його поля чи трактувати його як
У програмуванні такий покажчик називають opaque handle (або pointer), через нього можна отримати (через відповідний набір методів-інструкцій) доступ до операцій над об’єктом, але внутрішня будова прихована задля відкритого доступу через C ABI (extern "C"). Фактично, перетворюючи складний за будовою об’єкт в поле структури, тип якого є, формально, покажчиком і вдало експортується у зовнішнє середовище. Для встановлення повноцінного доконаного контракту між Java та C++ ще треба чітко визначити, чи може значення покажчика бути null, хто відповідає за його звільнення (release) та чи дозволено використовувати об’єкт із кількох потоків (thread-safety).
Якою може бути базова технологія такого адаптера
Якщо такий адаптер C++ у C потрібен не для однієї функції, а для десятків однотипних класів і методів, у ньому починає повторюватися не бізнес-логіка, а механіка доступу. Саме повторюваність механіки доступу й можна автоматизувати у код-генерації. Не просто «згенерувати C++ binding», а послідовно побудувати C ABI адаптер з обмеженої, перевіреної підмножини C++ API. Концептуально послідовність має такий вигляд:
C++ header + compile_commands.json │ ▼ Clang AST: фактичні C++ типи та overload-и │ ▼ правила lowering: scalar / handle / out parameter / status │ ├── C header для jextract ├── C++ shim, що викликає original API └── звіт: що згенеровано, а що свідомо відхилено
Ключовим тут є реальний build context (набір команд, флагів, header-файлів і зовнішніх бібліотек). Header-файл сам по собі не підказує Clang, з яким стандартом C++, include-ми, макросами, цільовим ABI та стандартною бібліотекою його треба читати. А отже, генератор
// counter.hpp
#include <cstdint>
namespace demo {
class Counter {
public:
explicit Counter(std::int32_t initial);
std::int32_t add(std::int32_t delta);
std::int32_t value() const;
};
}
Для такого класу правила «пониження» (перехід від C++ до C) доволі очевидні:
- C++ клас
Counterстає opaque handle; - конструктором стає метод
createз параметромout; - функції, які будуть репрезентувати методи об’єкта класу, отримують handle (покажчик) першим аргументом;
- C++ return value переходить в
out-параметр, а сама функція повертаєstatus; - деструктор не експортується напряму — натомість створюється
release; - C++ помилки відстежуються всередині згенерованого коду (C++ у C) і перетворюються на інтерпретовані статуси помилок (за прикладом Golang).
Враховуючи все описане вище, потенційний C header-файл може виглядати так:
// counter_capi.h - придатний для jextract #include <stdint.h> typedef struct demo_counter_handle demo_counter_handle; typedef int32_t demo_status; demo_status demo_counter_create(int32_t initial, demo_counter_handle **out_handle); demo_status demo_counter_add(demo_counter_handle *self, int32_t delta, int32_t *out_value); demo_status demo_counter_value(const demo_counter_handle *self, int32_t *out_value); void demo_counter_release(demo_counter_handle *self);
Йому відповідає C++ реалізація, яка має право працювати із C++ класом:
// counter_capi.cpp - компілюється як C++ і лінкується з бібліотекою
#include "counter.hpp"
#include "counter_capi.h"
struct demo_counter_handle {
explicit demo_counter_handle(int32_t initial) : value(initial) {}
demo::Counter value;
};
extern "C" demo_status demo_counter_create(
int32_t initial, demo_counter_handle **out_handle) {
if (out_handle == nullptr) return 1;
try {
*out_handle = new demo_counter_handle(initial);
return 0;
} catch (...) {
return 2;
}
}
extern "C" demo_status demo_counter_add(
demo_counter_handle *self, int32_t delta, int32_t *out_value) {
if (self == nullptr || out_value == nullptr) return 1;
try {
*out_value = self->value.add(delta);
return 0;
} catch (...) {
return 2;
}
}
extern "C" demo_status demo_counter_value(
const demo_counter_handle *self, int32_t *out_value) {
if (self == nullptr || out_value == nullptr) return 1;
try {
*out_value = self->value.value();
return 0;
} catch (...) {
return 2;
}
}
extern "C" void demo_counter_release(demo_counter_handle *self) {
delete self;
}
Як бачите, тут нема спроби прочитати C++ об’єкт із Java. Вищезгаданий
demo::Counterіснує лише на рівні C++ коду;- C ABI бачить opaque handle, прості числові типи з гарантованим розміром у бітах (
int32_tі так далі),out-параметр і статус-код. jextractотримує тількиcounter_capi.h— неcounter.hpp.
Зачекайте, але ж існує reinterpret_cast та void*!
Все вірно, reinterpret_cast часто з’являється в обговореннях C/C++ native interop, тому варто відділити два дуже різні випадки.
Поганий випадок...
...використати reinterpret_cast, щоби переконати компілятор, ніби C++ object має C layout:
// НІ: це не робить std::vector C-структурою. auto *fake = reinterpret_cast<my_c_vector *>(&cpp_vector);
Це не дає std::vector як { pointer, size, capacity }. Такий layout не є API: він суто залежить від реалізації стандартної бібліотеки шаблонів, build-налаштувань та версії toolchain (CMake). Те саме стосується C++ класів, vtable, наслідування й std::string.
Нормальний, спеціалізований випадок...
...передати елементи вже наявного std::vector<T> у C API, який очікує буфер байтів. Тут ми не cast-имо об’єкт vector. Ми беремо data(), тобто pointer на елементи, і на час одного виклику представляємо його як pointer на байти. Розглянемо C++ template-вектор із пікселями:
template <typename Pixel>
using Image = std::vector<Pixel>;
template <typename Pixel>
void apply_filter(Image<Pixel>& image) {
// C API обробляє лише raw bytes і повертає status.
int rc = c_filter_apply(
reinterpret_cast<unsigned char *>(image.data()),
image.size() * sizeof(Pixel));
if (rc != 0) {
throw std::runtime_error("c_filter_apply failed");
}
}
Тут reinterpret_cast<unsigned char*> доречний за конкретних умов:
image.data()вказує на безперервний масивPixel, що гарантуєstd::vector;C-функція справді працює з байтовим представленням, а не очікує інший тип елементів;- кількість байтів передається як
image.size() * sizeof(Pixel); - покажчик не зберігається
C-кодом після завершення виклику; Pixelпридатний для такого byte-level протоколу — наприклад,uint8_t, або інший тип із явно визначеним форматом.
Для std::vector<uint8_t> cast взагалі майже косметичний: data() вже має потрібний тип. Для std::vector<float> так само cast означає, що саме «ця float*, краще не використовувати reinterpret_cast:
void normalize(std::vector<float>& samples) {
c_normalize_f32(samples.data(), samples.size());
}
А якщо на межі interop потрібен об’єкт із фіксованим форматом, цей формат слід описати C POD-типом (проста
Треба обирати щось одне
reinterpret_cast — не allocator, не serializer і не засіб зробити C++ ABI «сумісним із C». Хоч reinterpret_cast і дає змогу приховати тип об’єкта C++, проте такий підхід не прибирає необхідність чітко розуміти, для якого саме типу template-а ми збираємося зробити «dereferencing». Тому й подібний підхід може працювати лише за умов чіткого розуміння, які типи й де будуть застосовуватися. Це означає, що у reinterpret_cast немає жодних переваг перед створенням такого собі opaque handle, який так само буде містити конкретні реалізації конкретних «проблемних типів C++».
Як не крути, але доводиться приймати складні рішення. C++ значно складніший за C, тому й з ним важче працювати з Java рантайму. Але навіть до початку інтеграції C++ коду в Java застосунок, розробник повинен визначитися, як саме буде інтегрований C++ код:
- opaque handle
reinterpret_cast
Або третій варіант — множити й так нескінченну кількість С++ коду і «якось» його переводити в С. Тим самим логіка, розробка і дебаг коду стають значно дорожчими і складнішими для native interop. І, як на мене, кращою опцією є саме opaque handles, бо створення такого коду можна автоматизувати повністю, а бізнес-логіка залишиться на стороні Java застосунку.
Підведемо трохи підсумків.
Project Panama поки що не робить C++ магічно простим для Java. Він дає сучасний і стандартний у Java спосіб викликати добре описаний native ABI та працювати з native memory. Хоч jextract прибирає механічну частину роботи в моделюванні роботи С-коду Java-кодом, але його природний input — C header-файл.
І це не обмеження, яке варто ламати reinterpret_cast-ами. C++ має templates, classes, STL, exceptions і compiler-specific ABI — уся ця зручність зникає при дереференсі. Тому й найпростіший шлях інтеграції Java та C++ — прошарок у вигляді C API: scalars, явно розмірені buffers, C POD і opaque handles.
reinterpret_cast може бути корисним локальним інструментом, проте він не має перетворювати C++ object на C struct, не має приховувати ownership і не має бути контрактом між Java та STL.
Яким би не було рішення для інтеграції С++ в Java, ми вже бачимо властивості потрібного рішення: воно не повинно вдавати, що Java розуміє layout std::vector, не має пропускати C++ exception через C ABI, мусить явно фіксувати ownership, lifetime, shape буферів і версію build-контексту. Перехід від С++ в С через opaque handles — один із базових патернів, який дозволяє сформувати таку межу переходу відповідальності вручну.
Але реалізація підтримки десятків-тисяч С++ бібліотек неодмінно призведе до повторюваного boilerplate: opaque handles, create/release, status code, out parameters, exception translation, перевірки null і довжини буферів. Для однієї-двох функцій, або навіть однієї бібліотеки, це нормальна інженерна робота. Для великої C++ бібліотеки (як OpenCV або Tensor) виникає наступне, вже значно цікавіше питання: як автоматизувати цю механіку, не почавши вгадувати C++ ABI та семантику, яких у header просто немає? Про це ми й поговоримо в одній з наступних статей.
Джерела
- JEP 454: Foreign Function & Memory API
- Foreign Function and Memory API, Java SE documentation
- Jextract — the Native Library Binding Extraction Tool
- Generate Java Bindings with Jextract
Стежте за новинами та оновленнями за посиланнями:
- Dev.java — спеціальний портал Oracle для поглиблення ваших знань з Java та участі у спільноті.
- Inside.java — суто технічний блог, який ведуть архітектори та інженери, що розроблюють Java.
- Inside.java Podcast — подкаст про JDK, JVM, GC, core-libs і так далі.
- Inside.java Newscasts — щотижневе ютуб-шоу винятково про Java.
- Java on YouTube — усе про Java та екосистему.
- OpenJDK mailing lists — місце, де можна дізнатися поточний стан речей у спільноті OpenJDK.
17 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівУдачі з проєктом!
Як в еру AI можна писати не на Java, найкращій мові у світі? 😄
Проблемы нет, просто не пользуйтесь Джавой
Це рівень прийняття рішень, який дуже мало залежить від того хто користується технічним стеком технологій, та програмно-апаратними екосистемами. Скажімо уся Fin Tech індустрія зараз не зміне негайно весь тех стек, вклавши гіркісінький CapEx в переписання усього і уся з Java, максимум частковий перехід на Kotlin, Scala і т.п. РІвно як і усі ШІ-шники негайно не стануть переписувати усе з Python, бо в Java 25 постфіксили проблеми які виникли 1995, яких в Python не було вже в 1993 (зате є інші куди гірші, але вони не є критичними для mashine learning).
Навіть на рівні CTO, теж далеко не усе так просто. Далеко не усі мають ситуацію типу або Ілана Маска чи Сергія Бріна. Навіть Кен Томпсон та Денніс Річчі або Джеймс Гослінг — щоби реалізувати свої екосистемні ідеї, пройшли через дуже складні перемовини з керівництвом, писали заяви на звільнення і т.п.
та ШІ перепише кодобазу за один промт, вжухххх і готово
А потім наздогоне і ще раз перепише. Проходили ми це усе, а особливо коли якомусь черговому делітанту в чергове стукне в голову, що можна не тестувати з QA, деплоїти без DevOps — а професійні BA це лакшері.
І цей весь ШІ слоп потім падає на продакшені та тиждень на вухах усе керівництво, не з ІТ — шукають хто винний.
Те що можна рутину прибирати з діяльності — так можна, це взагалі правильний метод автоматизації.
ти пишеш ніби це щось погане
і С++
Дуже цікава статья, велике дякую!
Трохи сумно що C++ так і не розвинувся у плані більшої сумісності з іншими популярними мовами на рівні стандартів, що цілком логічно приводить до дуже рідкого його використання та інтеграцію з іншими продуктами навіть якщо це і доцільно.
С API вже мабуть більше 60 років буде, а це виходить єдиний працюючий метод поєднання, бо він і є самим простим і надійним.
на рівні яких стандартів?
маю на увазі не фактичний стандарт як С++26, а більш про формування і уявлення типів. Навіть цілочисленні типи мають різне представлення в реалізації.
В сенсі Application Binary Interface — ABI ? uk.wikipedia.org/...овий_програмний_інтерфейс
потрібно як JS все буде флоат
наймманглінгу
Радий, що стаття сподобалася. Продовження 100% буде, бо маю ще багато чого по цій темі розповісти.
Власне, С++26 хоч і оновлює саму мову, проте ніяких змін до ABI не приніс. Все зводиться до того, що чим складніша мова, тим важче її екстраполювати на типи і стандарти інших мов. Наприклад Rust вміє спілкуватися із Java лише через С АВІ, так само і з CPython-ом. Як не крути, але С є базововою технологією для всіх. С++ можна в С, Rust в С, Python в C (CPython), Java в С (JNI). Тому й дивуватися/сумувати, що нема адекватного переходу від С++ до інших рантаймів не варто, бо тоді б довелося спрощувати мову (із чим, доречі, непогано справився Golang).
До чого тут це ? Для початку це навпаки нативна мова яка дозволяє наприклад на пряму використовувати системний API і на сквозну без жодних додаткових механізмів 99.99% хедерів та коду на С. Що лише тільки сниться скажімо Go Lang або Rust. Пів коду того же Docker/Moby для Windows це WinAPI який в Visual C++ в Win SDK, безкоштовно. Аналогічно в Unix часто достаньо додати #inlcude і т.п.
Це іншим мовам треба підтримувати ABI, що добре зроблено наприклад в значно молодшій Node. А з Python С++ друже так не погано, що існує навіть boot python (щоправда ті хто інтегрують професійно його не люблять). В С#/.NET усе зроблено на голову краще. Ще існує мови як Jancy — там взагалі пряма сумісність з С++ ABI github.com/vovkos/jancy
Сама JVM написана на С++.
JNI зроблений відверто не дуже ще початковою командою Гослінга, бо в них було не дуже багато часу. В процессі експлуатації це усе виявилось але було не дуже запотребувано, бо цим займалось не так багато народу в окремих сегментна типу швидкісного пошуку, або web інтерфейсів для медичного обладнання де треба було обробляти вилитих об’єми даних з обладнання типу масс спектрометрів.
Та з приходом ери AI якраз саме через проблеми JNI, Java стала конкретно програвати конкуренцію ровеснику — Python, та Oracle з рештою ввели проект Panama і почали вирішувати проблеми родом з 1995.
У С++ інші фундаментальні проблеми ни які наголошує Пентагон та Кенрнегі-Мелон, не безпідставно — є статистика провалів строків та бюджетів деяких программ як то винищувача F-35,та авіаносця Джеральд Форд. Java по цілому моменту тієї же статистики, в топі дозволених мов. Зараз комітет із стандартизації С++ додає профілі безпеки та рефлексію яка запізнились на 15 років мінімум.
Трохи хронологія невірна. Project Panama зʼявився ще до того як були опубліковані перші наукові статті по llm, але вже існував стек machine learning і всілякі onnx-runtime та opencv із dnn. Ну і слова «програвати» це трохи занадто. А загалом все вірно сказано.
А LLM тут ні до чого. TensorFlow чи PyTorch або Numpy були створені від початку на Python, і зараз як ви підете на вкладку «Робота» то графа Python та AI буде в2-3 рази більша ніж графа Java.
Берете будь яку популярну книгу або курс з AI там буде Python та можливо С/C++, а не Java, це конкретна комерційна загроза технології. Python — індустрійний стандарт в світі ШІ, Jython чи Deeplearning4j — це нішева тема, як і TensorFlow Java інтерфейси і тому подібне.
Разом із тим і FastAPI став популярним тому, що він є частиною екосистеми Python. Коли розробники пишуть AI-сервіси (моделі, ембедінги, інференс), вони природно піднімають навколо них обгортки на FastAPI (або Flask), а не тягнуть туди Spring. Тобто тут простежується прямий каскадний ефект від домінування Python у ШІ.
З часів фреймворків типу Caffe, Java на цей сегмент ринку запізнилась. Академічний сегмент і далі усі інші обрали для AI саме Python.