Вступ до Project Panama. Частина 3. JEXTRACT
Перша та друга частини показують, що Project Panama пропонує чудову структуру (Foreign Function & Memory API), призначену для створення та виклику нативного коду C. У той час як частина перша була здебільшого вступом для нового API, необхідного для роботи із нативним кодом у Java, метою другої частини було заглибитися в аспекти та проблематику представлення нативних варіативних функцій в Java. У цій статті ми ще глибше розглянемо реалізацію варіативної функції, наведеної в Частині 2, а також аспекти реалізації, яку пропонує інструмент генерації коду Project Panama — jextract.
Компіляція
Компілятори Java і C діють по-різному, що обумовлено природою мов програмування (лише компіляція проти компіляції та інтерпретування). Компілятор javac перетворює текст код на байт-код, що виконується JVM, тоді як компілятор C перетворює код на машинну мову. Враховуючи специфіку компіляції нативного коду, зустрічаючи варіативні функції С, компілятор створює новий варіант цієї функції, який повністю відповідає сигнатурі виклику. Це робиться задля того, щоб середовище виконання застосунку розуміло, як саме викликати певну варіативну функцію. Наприклад, для варіативних функцій типу C printf, компілятор створить окрему варіативність функції C printf, яка відповідатиме сигнатурі виклику:
#include <stdio.h>
// printf(const char* s, …);
int main() { printf("Hi! My name is %d\n", "Denis"); printf("Hi! My name is %d. I'm %d years old\n", "Denis", 31);}
Кожен із цих викликів матиме відповідно власний тип функції:
int(const char*, const char*);
int(const char*, const char*, const int);
У Java ситуація зовсім інша. Vararg-и мають іншу природу походження: вони завжди є масивом певної довжини, де кожен елемент має тип (ширше або вужче визначення):
public int sum(int ... nums) {…
}
Ключовою відмінністю від С компіляції варіативних аргументів є те, що компілятор javac створює масив для зберігання даних параметрів. У цьому випадку компілятор створює масив з компонентами загального типу для зберігання аргументів (найзагальніше визначення може бути типу Object[]). Отже, обробка варіативних параметрів у Java — це суто рантайм-функціонал, а в С — лише на етапі компіляції.
С код у Java
Якщо подивитися на реалізацію нативних викликів, наведених у минулих статтях, стає зрозуміло, що процедура написання інфраструктурного коду нативного виклику є тим самим процесом, що робить С компілятор відносно С коду: для кожної комбінації варіативних аргументів необхідно створити спеціалізований варіант застосунку методу (MethodHandle), а Linker, у цьому контексті, діє як компілятор C у Java, у той же час FunctionDescriptor є носієм як типу методу (MethodType), так і очікуваної сигнатури виконання застосунку методу:
static final Linker linker = Linker.nativeLinker();
static final FunctionDescriptor puts$fd = FunctionDescriptor.of(JAVA_INT, ADDRESS);
linker.downcallType(puts$fd); // int(Addressable)
Схоже, що місія Project Panama (Foreign Function & Memory API, зокрема) полягає в тому, щоб дозволити розробникам писати С програми на Java, зберігаючи той самий рівень досвіду програмування, але з необхідністю реалізації інфраструктурного коду нативного виклику, подібно до того, що C компілятор робить проти коду C:
class PrintfImpls {static final Linker LINKER = Linker.nativeLinker();
static final Addressable PRINTF_ADDR = LINKER.defaultLookup().lookup("printf").orElseThrow(); static MethodHandle specializedPrintf(MemoryLayout... varargLayouts) {FunctionDescriptor specialized = PRINTF_BASE_TYPE.asVariadic(varargLayouts);
return LINKER.downcallHandle(PRINTF_ADDR, specialized);
}
static final FunctionDescriptor PRINTF_BASE_TYPE = FunctionDescriptor.of(JAVA_INT, ADDRESS);
public static final MethodHandle WithInt = specializedPrintf(JAVA_INT);
public static final MethodHandle WithString = specializedPrintf(ADDRESS);
public static final MethodHandle WithIntAndString = specializedPrintf(JAVA_INT, ADDRESS);
}
Підхід, описаний у Частині 1 і Частині 2, базується на тому факті, що розробник повинен створити екземпляр Linker, дескрипторів функцій та застосунків методів, які відповідають певній функції C (наприклад, C printf у stdio.h). Найбільше занепокоєння викликає те, що розробник зосереджений не на створенні програми, яка використовує нативну функцію, а на тому, щоб витратити деякий час на написання інфраструктурного коду нативних функцій.
Це одна з проблем, яку прагне вирішити Project Panama, впроваджуючи новий інструмент (jextract) для генерації інфраструктурного коду навколо нативних функцій, для того, щоб сфокусуватися на головному обов’язку — написанні програм, які вже використовують нативний код.
jextract
Цікавий факт: новий інструмент генерації коду є першим інструментом коду, який є частиною OpenJDK, але не частиною дистрибутиву. Є кілька причин, чому цей інструмент не є частиною дистрибутива JDK. По-перше, не всі Java розробники працюють з нативним кодом напряму, тому інструмент jextract не для всіх. Друга (і, напевно, найголовніша) причина — це великий розмір самого інструменту. Зазвичай розмір дистрибутиву JDK становить менше 400 Мб, а інструмент jextract сягає майже 180 Мб.
Треба розуміти, що саме jextract є нетиповим рішенням, бо частіше за все сама JDK залежить від інструментів типу javac, javap, jlink і так далі. Але у випадку з jextract ситуація зовсім інша, бо залежність зворотня — jextract залежить від самої JDK, бо використовує Foreign Function & Memory API. А в якості бекенду для обробки С коду використовується libclang (llvm).
libclang
Призначення інструменту jextract — створювати інфраструктурний код Java для нативних символів мови C на основі файлу libclang відповідає за:
- переклад вихідного коду C у деякий проміжний стан (також званий «інтерфейсом»),
- перевести проміжний стан вихідного коду C у машинний код (також називається «back-end», Clang використовує для цього LLVM).
Найголовніше те, що Clang — це не просто компілятор, це також бібліотека, або так званий «С інтерфейс для Clang». Цей функціонал інструмент jextract й використовує, а саме: аналіз
JDK 19
Інструмент jextact написаний на двох мовах: С та Java. Якщо із С кодом все зрозуміло, то із Java питання відкрите. jextract використовує пакет java.lang.foreign, який містить Foreign Function & Memory API. Але найцікавішим є те, що jextract використовує Foreign Function & Memory API-класи для двох цілей: як для роботи з libclang, так і для моделювання інфраструктурного коду для нативних бібліотек. jextract як уроборос — цей інстурмент використовує сам себе для створення коду для роботи із libclang, який використовується для генерації інфраструктурного коду бібліотек, відносно котрих цей інструмент був застосований. Отже, jextract сам використовує себе, і що це, як не доказ самоконтролю якості коду та функціоналу?
Використання jextract
Найкращий спосіб зрозуміти, що робить інструмент jextract, це подивитися, що він генерує для бібліотеки C stdio.
Інсталяція
Як згадувалося раніше, jextract є першим самостійним інструментом кодогенерації у OpenJDK, який не є частиною дистрибутиву. Це означає, що для початку роботи з інструментом jextract необхідно встановити самі інструменти, дистибутив, доступний за адресою jdk.java.net/jextract. Процес інсталяції схожий на інсталяцію JDK, оскільки дистрибутивний пакет jextract — це не просто один бінарний файл, а JRE, зібране за допомогою jlink з додатковим модулем функціоналу.
Отже, завантажте, розпакуйте та додайте до $PATH. Переконайтеся, що інструмент jextract доступний:
$ jextract --version
jextract 19
JDK version 19-ea+23-1706
clang version 13.0.0
Функціонал
Настав час розібрати можливості jextract. Щоб сгенерувати вихідні Java класи для бібліотеки С, необхідно надати інструменту jextract наступні інструкції:
jextract --source -t com.clang.stdlib.stdio -I /usr/include --output src/main/java /usr/include/stdio.h
Примітка. У macOS C stdlib містить папку (шлях під параметром -I), розташовану тут:
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include
Наведена вище команда наказує jextract сгенерувати Java класи для /usr/include/stdio.h, базовою папкою інклудів є /usr/include, отримані класи мають бути розміщені в src/main/java в межах пакету com.clang.stdlib.stdio. Зверніть увагу, що такі параметри, як -I (папка з інклудами), є тими самими параметрами, представленими в компіляторі gcc/clang. Параметр -l, не згаданий у команді вище, є комбінацією двох прапорців gcc: -L <каталог> і -l <назва-бібліотеки>, значенням для цього параметра може бути ім’я бібліотеки або її абсолютний шлях.
Вивчення вмісту пакета
У результаті інструмент jextract створить новий пакет Java, що містить класи Java, що охоплюють API бібліотеки C stdio, згаданий у файлі заголовка stdio.h:
src/main/java/com/clang/stdlib/stdio
├── Constants$root.java
├── FILE.java
├── RuntimeHelper.java
├── __darwin_mbstate_t.java
├── __darwin_pthread_attr_t.java
├── __darwin_pthread_cond_t.java
├── __darwin_pthread_condattr_t.java
├── __darwin_pthread_handler_rec.java
├── __darwin_pthread_mutex_t.java
├── __darwin_pthread_mutexattr_t.java
├── __darwin_pthread_once_t.java
├── __darwin_pthread_rwlock_t.java
├── __darwin_pthread_rwlockattr_t.java
├── __mbstate_t.java
├── __sFILE.java
├── __sbuf.java
├── _opaque_pthread_attr_t.java
├── _opaque_pthread_cond_t.java
├── _opaque_pthread_condattr_t.java
├── _opaque_pthread_mutex_t.java
├── _opaque_pthread_mutexattr_t.java
├── _opaque_pthread_once_t.java
├── _opaque_pthread_rwlock_t.java
├── _opaque_pthread_rwlockattr_t.java
├── _opaque_pthread_t.java
├── constants$0.java
...
├── constants$17.java
├── funopen$x0.java
├── funopen$x1.java
├── funopen$x2.java
├── funopen$x3.java
└── stdio_h.java
0 directories, 48 files
Java класи, що містяться у цьому пакеті, відповідатимуть контекту С-хедера stdio.h. Слід врахувати, що за замовчанням цей контент є платформено-залежним, бо місить нативні символи, притаманні платформі macOS, наприклад. Водночас, визначення функцій бібліотеки C stdio є платформено-незалежним.
Характерні властивості пакету:
- є лише один публічний API класс, який містить Java методи, що є фасадами до нативних викликів
- кожен нативний символ матиме публічний доступ (структура, функція, макрос і так далі)
Розширені можливості
Як бачите, інструмент jextract генерує багато Java коду для файлу заголовка C stdio.h. Не всі ці файли заголовків можуть знадобитися для виконання конкретної функції C, або взагалі є платформено-специфічними. Щоб перевірити, що саме jextract згенерує для файлу заголовка, він має такий параметр:
--dump-includes <file> dump included symbols into specified file
Цей параметр накаже jextract створити файл, що містить усі нативні символи, які він потенційно може отримати з файлу заголовка. Щойно створений файл міститиме комбінацію таких значень параметрів:
--header-class-name <name>
--include-function <name>
--include-enum <name>
--include-macro <name>
--include-struct <name>
--include-typedef <name>
--include-union <name>
--include-var <name>
Використовуючи комбінацію цих параметрів, можна обмежити кількість коду, який може створити jextract. Наприклад, наступна команда створить інфраструктурний код лише для нативної функції C printf:
jextract --source -t com.clang.stdlib.stdio -I /usr/include --output src/main/java --include-function printf /usr/include/stdio.h
Фільтрування допомагає уникнути створення непотрібних Java класів, а також зайвих відображень нативних символів типу як макросів, структур, змінних і типів.
Отже, файл дампа — це файлове сховище для параметрів jextract (типовий концепт для shell), цей файл можна надати за допомогою простого трюку shell — @<file>:
jextract --source -t com.clang.stdlib.stdio -I /usr/include --output src/main/java @dump.txt /usr/include/stdio.h
Примітка. Дуже важливо знати, що ви робите під час фільтрації, може існувати залежність між компонентами файлу заголовка, як-от типи stdio.h FILE і __sFILE:
typedef struct __sFILE {….
} FILE;
де нативний символ FILE є аліасом для типу структури sFILE. Виключення одного може призвести до проблем з іншими.
Згенеровані Java класи міститимуть мінімальний код, необхідний для виклику функції C printf, дескриптор функції та застосунок методу:
class constants$0 {static final FunctionDescriptor printf$FUNC = FunctionDescriptor.of(Constants$root.C_INT$LAYOUT,
Constants$root.C_POINTER$LAYOUT
);
static final MethodHandle printf$MH = RuntimeHelper.downcallHandleVariadic(
"printf",
constants$0.printf$FUNC
);
}
а також публічні методи:
public class stdio_h { /* package-private */ stdio_h() {} public static MethodHandle printf$MH() {return RuntimeHelper.requireNonNull(constants$0.printf$MH,"printf");
}
public static int printf ( Addressable x0, Object... x1) {var mh$ = printf$MH();
try {return (int)mh$.invokeExact(x0, x1);
} catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$);}
}
}
Трохи висновків
З інструментом jextract шлях від визначення необхідної нативної бібліотеки C до її фактичної інтеграції у Java застосунку набагато коротший, порівняно з підходом, описаним у Частинах 1 і Частина 2. Отже, єдине, за що відповідає розробник, — це нативний розподіл пам’яті та інтеграція функцій.
Обсяг роботи, яку необхідно виконати перед викликом нативної функції, вимагає набагато менше часу та зусиль:
import java.lang.foreign.MemorySession;
import java.lang.foreign.SegmentAllocator;
import com.java_devrel.samples.stdlib.stdio.stdio_h;
public class Printf { public static void main(String[] args) { try (var memorySession = MemorySession.openConfined()) {stdio_h.printf(memorySession.allocateUtf8String(
"Welcome from the other side!\n"));
}
}
}
Можливість фільтрації допомагає точніше визначити, що саме має бути витягнуто з файлу заголовка. Враховуйте, що інструмент jextract не відстежує залежності поміж нативними символами, наприклад, як функція
int vfscanf(FILE * __restrict __stream, const char * __restrict __format, va_list)
залежить від структури FILE. Але створення інфраструктурного коду для vfscanf не призведе до створення JAva представлення структури FILE. Отже, дуже важливо чітко визначити, що саме ви хотіли б отримати в результаті, інструмент jextract — це повторюваний процес кодогенерації, де не відбувається ніякої магії. Іншими словами, або генеруйте усе, або витратьте трохи часу на те, щоб визначитися, які саме нативні символи вам вкрай необхідні, бо jextract працює по моделі «або все, або те, що замовляли».
Як бачите, jextract якісно змінює підхід до кодогенерації Java представлень для нативного коду, тим самим зберігаючи час на відповідне написання цільового коду замість інфраструктурного.
Код
Код для цієї статті доступний тут.
P.S.
Пам’ятайте про те, що Foreign Function & Memory API доступно у режимі Preview. Для того, щоб працювати з цим API:
- Скомпілюйте програму за допомогою
javac --release 19 --enable-preview Main.javaта запустіть її за допомогоюjava --enable-preview Main. - Використовуючи програму запуску вихідного коду, запустіть програму з
java --source 19 --enable-preview Main.java. - Використовуючи
jshell, почніть його зjshell --enable-preview.
----
dev.java | inside.java | Java on YouTube | Java on Twitter | Me on Twitter
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

5 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів