هنگام توسعهی برنامههای جاوا ممکن است نیاز به دسترسی به کتابخانهها و حافظهی کدنویسیشده با زبانهای دیگر داشته باشید. پروژهی Panama برای پاسخگویی به نیاز پشتیبانی بهتر از توسعهدهندگان در دسترسی به کتابخانههای بومی (Native)، بهویژه کتابخانههای نوشتهشده با C/C++ طراحی شده است. تعامل بین JVM و رابطهای خارجی (Foreign - غیرجاوا) با Foreign Function and Memory API (FFM API) سادهتر شده است. API مربوط به FFM در JDK 22 بهعنوان ویژگی نهایی (Final) معرفی شد و پشتیبانی از دسترسی به حافظهی خارجی و همچنین فراخوانی توابع خارجی را اضافه کرد.
ابزار jextract فایلهای هدر (.h) کتابخانههای بومی را پردازش میکند و کد جاوا — با نام Bindings — تولید میکند که در داخل خود از Foreign Function and Memory API استفاده میکند.
با خروجی jextract میتوانید مستقیماً از مدل جاوا کتابخانههای بومی مورد نظرتان استفاده کنید.
در این آموزش نحوهی دریافت و اجرای ابزار jextract و همچنین نحوهی استفاده از کد جاوا تولیدشده توسط آن را بررسی میکنیم.
نسخههای ازپیشساختهشدهی jextract بهصورت دورهای منتشر میشوند.
این نسخهها از شاخهی master مخزن jextract ساخته میشوند
و حافظهی خارجی و API توابع در آخرین نسخهی JDK را هدف قرار میدهند.
نسخهی مناسب سیستمعامل خود را دانلود کنید و شروع به استفاده کنید.
همچنین میتوانید jextract را از آخرین منابع با دنبال کردن دستورالعملهای موجود در مخزن آن بسازید.
⚠️ اگر از macOS Catalina یا نسخههای جدیدتر استفاده میکنید، ممکن است قبل از استفاده از فایلهای jextract، لازم باشد صفت قرنطینه (Quarantine) را از آنها حذف کنید. دستور زیر را اجرا کنید:
sudo xattr -r -d com.apple.quarantine path/to/jextract/folder/
-D یا --define-macro <macro>=<value>
یک ماکرو را با مقدار مشخص تعریف میکند (اگر مقدار مشخص نشود، 1 در نظر گرفته میشود).
--header-class-name <name>
نام کلاس هدر تولیدشده را مشخص میکند. اگر این گزینه مشخص نشود، نام کلاس از نام فایل هدر استخراج میشود. برای مثال، کلاس «foo_h» برای هدر «foo.h». اگر چند هدر مشخص شده باشد، این گزینه اجباری است.
-t, --target-package <package>
نام پکیج مقصد برای کلاسهای تولیدشده را مشخص میکند. اگر این گزینه مشخص نشود، از پکیج بدون نام (Unnamed Package) استفاده میشود.
-I, --include-dir <dir>
یک پوشه به مسیرهای جستجوی Include اضافه میکند. پوشهها به ترتیب جستجو میشوند.
برای مثال، اگر -I foo -I bar مشخص شود، ابتدا فایلهای هدر در «foo» و سپس (اگر چیزی یافت نشد) در «bar» جستجو میشوند.
-l, --library <name | path>
یک کتابخانهی مشترک (Shared Library) را مشخص میکند که باید توسط کلاس هدر تولیدشده بارگذاری شود. اگر با : شروع شود، آنچه بعد میآید بهعنوان مسیر کتابخانه تفسیر میشود.
در غیر این صورت، <libspec> نام کتابخانه را نشان میدهد. مثالها:
-l GL-l :libGL.so.1-l :/usr/lib/libGL.so.1--use-system-load-library
کتابخانههای مشخصشده با -l با استفاده از System::loadLibrary یا System::load بارگذاری میشوند.
این گزینه زمانی مفید است که کتابخانهها باید از یکی از مسیرهای موجود در java.library.path بارگذاری شوند.
--output <path>
محل قرارگیری فایلهای تولیدشده را مشخص میکند.
--dump-includes <file>
نمادهای شاملشده را در فایل مشخصشده ذخیره میکند.
برای فیلتر کردن عناصر خاص، jextract میتواند یک خروجی از تمام نمادهای یافتشده در یک فایل هدر تولید کند.
این خروجی قابل ویرایش است و سپس میتواند بهعنوان فایل آرگومان (با سینتکس @argfile) استفاده شود تا فقط برای زیرمجموعهای از نمادها Binding تولید شود.
--include-[function,constant,struct,union,typedef,var]<String>
یک نماد با نام و نوع مشخصشده را در Bindings تولیدشده شامل میکند. وقتی یکی از این گزینهها مشخص شود، هر نمادی که با هیچ فیلتر مشخصشدهای مطابقت نداشته باشد از Bindings تولیدشده حذف میشود.
--version
اطلاعات نسخهی ابزار، نسخهی JDK که برای آن ساخته شده، نسخهی clang و سپس خارج میشود.
بیایید این مثال را در نظر بگیریم: رندر کردن یک کتری چای (Teapot) با استفاده از توابع freeglut. برای ویندوز از بستهی freeglut MSVC استفاده کنید.
freeglut یک جایگزین متنباز برای کتابخانهی OpenGL Utility Toolkit (GLUT) است.
⚠️ اگر از macOS Catalina یا نسخههای جدیدتر استفاده میکنید، باید mesa-glu را نیز نصب کنید. این یک کتابخانهی متنباز است که توابع کمکی اضافی برای تکمیل مشخصات اصلی OpenGL فراهم میکند:
brew install mesa-glu
معمولاً یک کتابخانهی بومی دارای یک پوشهی include است که همهی فایلهای هدر تعریفکنندهی رابط کتابخانه را شامل میشود و یک فایل هدر اصلی دارد.
کتابخانهی freeglut در مسیر /path/to/freeglut/ دارای پوشهای به نام path/to/freeglut/version/include است که فایلهای هدر در آن ذخیره شدهاند.
در ریشهی پروژهی جاوا — که پوشهی src معادل پکیج ریشه است — یک ترمینال باز کنید و jextract را برای تبدیل هدر اصلی freeglut به کد جاوا اجرا کنید:
# macOS and Linux compatible command
jextract --output src \
-l :/opt/homebrew/Cellar/freeglut/3.6.0/lib/libglut.3.dylib \
-I /opt/homebrew/Cellar/freeglut \
-I /opt/homebrew/Cellar/mesa \
-I /opt/homebrew/Cellar/mesa-glu \
-t org.freeglut \
/opt/homebrew/Cellar/freeglut/3.6.0/include/GL/freeglut.h
این دستور فایل هدر را با در نظر گرفتن گزینههای زیر به کلاسهای جاوا تبدیل میکند:
--output src خروجی را در پوشهی ریشهی src ذخیره میکند.-l :/opt/homebrew/Cellar/freeglut/3.6.0/lib/libglut.3.dylib به jextract میگوید Bindings تولیدشده باید از مسیر کتابخانهی مشخصشده بارگذاری شوند.-I /opt/homebrew/Cellar/freeglut، -I /opt/homebrew/Cellar/mesa و -I /opt/homebrew/Cellar/mesa-glu پوشههای جستجوی فایلهای هدر را مشخص میکنند. این مکانها برای یافتن فایلهای هدری که از طریق #include در فایل هدر اصلی گنجانده شدهاند استفاده میشوند.-t org.freeglut پکیج مقصدی را مشخص میکند که کلاسها و Interface های تولیدشده به آن تعلق خواهند داشت. jextract بهصورت خودکار ساختار پکیج را زیر پوشهی src ایجاد میکند./opt/homebrew/Cellar/freeglut/3.6.0/include/GL/freeglut.h فایل هدر اصلی کتابخانهی بومی است که میخواهید Bindings آن تولید شود.دستور معادل در ویندوز مشابه است:
# Windows PowerShell command
jextract --output src `
-I "\path\to\freeglut\include" `
--use-system-load-library `
"-l" opengl32 `
"-l" glu32 `
"-l" freeglut `
"-t" "org.freeglut" `
"\path\to\freeglut\include\GL\glut.h"
برخی کتابخانهها بسیار بزرگ هستند (مثل Windows.h) و ممکن است به همهی کد تولیدشده توسط jextract نیاز نداشته باشید.
برای چنین شرایطی میتوانید از گزینههای --include-XYZ خط فرمان jextract استفاده کنید تا فقط برای عناصر مشخصشده کلاس تولید شود.
برای دانستن اینکه کدام نمادها را میتوان فیلتر کرد، jextract میتواند یک خروجی از تمام نمادهای یافتشده در فایل هدر تولید کند:
# macOS and Linux compatible command
jextract --output src \
-l :/opt/homebrew/Cellar/freeglut/3.6.0/lib/libglut.3.dylib \
-I /opt/homebrew/Cellar/freeglut \
-I /opt/homebrew/Cellar/mesa \
-I /opt/homebrew/Cellar/mesa-glu \
-t org.freeglut \
--dump-includes glut.symbols \
/opt/homebrew/Cellar/freeglut/3.6.0/include/GL/freeglut.h
فایل glut.symbols با نزدیک به 5000 خط مشابه موارد زیر تولید میشود:
## Extracted from: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
--include-function glAccum # header: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
--include-function glActiveTexture # header: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
--include-function glAlphaFunc # header: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
--include-function glAreTexturesResident # header: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
--include-function glArrayElement # header: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
--include-function glAttachShader # header: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
...
فایل را ویرایش کنید تا فقط نمادهای مورد نیازتان را شامل شود و سپس آن را بهعنوان فایل آرگومان (با سینتکس @argfile) استفاده کنید تا فقط برای زیرمجموعهای از نمادها Binding تولید شود:
# macOS and Linux compatible command
jextract --output src \
-l :/opt/homebrew/Cellar/freeglut/3.6.0/lib/libglut.3.dylib \
-I /opt/homebrew/Cellar/freeglut \
-I /opt/homebrew/Cellar/mesa \
-I /opt/homebrew/Cellar/mesa-glu \
-t org.freeglut @glut.symbols \
/opt/homebrew/Cellar/freeglut/3.6.0/include/GL/freeglut.h
⚠️ اگر یک اعلان (Declaration) که توسط ساختار دیگری نیاز است را حذف کنید،
jextractوابستگی ازدسترفته را گزارش میدهد و بدون تولید Binding خاتمه مییابد:$ jextract --include-var aVar test.h ERROR: aVar depends on A which has been excluded
بیشتر متدهایی که jextract تولید میکند استاتیک هستند و برای استفاده با import static طراحی شدهاند.
برای دسترسی به کدی که jextract برای فایل هدر freeglut.h تولید میکند، فقط به دو import wildcard زیر نیاز دارید:
import org.freeglut.*;
import static org.freeglut.freeglut_h.*;
عبارت import static org.freeglut.freeglut_h.*; همهی توابع و فیلدهای استاتیک از کلاسی را که jextract برای فایل هدر اصلی کتابخانه تولید میکند، وارد میکند.
این شامل متدهای دسترسی به توابع، متغیرهای سراسری (Global Variables)، ماکروها، Enum ها، Typedef های اولیه و لایوتهای C Built-in میشود.
عبارت import org.freeglut.*; همهی کلاسهای دیگر تولیدشده توسط jextract را وارد میکند. این کلاسها شامل موارد زیر هستند:
اکنون کد Teapot.java را بنویسید. برای سرعت بخشیدن به کار، از نسخهی بومی Teapot.c الهام میگیریم:
#include <GL/glut.h>
void display(void)
{
//Clear color and depth buffers
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPushMatrix();
// Assign a color to the teapot
glColor3f(0.0, 1.0, 0.0);
// Rotation
glRotatef(10, 0.0, 0.0, 1.0);
glRotatef(10, 0.0, 1.0, 0.0);
//Draw
glutWireTeapot(1);
glPopMatrix();
//Must swap the buffer in double buffer mode
glutSwapBuffers();
}
void init(void)
{
glClearColor(0.0, 0.0, 0.0, 0.0);
//Model(Object coordinates), View (Camera coordinates), Projection (Screen coordinates)
glMatrixMode(GL_PROJECTION);
gluPerspective(40.0, 1.0, 1.0, 10.0);
glMatrixMode(GL_MODELVIEW);
gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.);
}
int main(int argc, char **argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutInitWindowSize(500, 500);
glutCreateWindow("Hello Panama!");
init();
glutDisplayFunc(display);
glutMainLoop();
return 0;
}
متد display را در جاوا نگه میداریم و init را به یک سازنده (Constructor) تبدیل میکنیم:
import java.lang.foreign.Arena;
import java.lang.foreign.SegmentAllocator;
import org.freeglut.*;
import static org.freeglut.freeglut_h.*;
public class Teapot {
Teapot(SegmentAllocator allocator) {
// Reset Background
glClearColor(0f, 0f, 0f, 0f);
//Model(Object coordinates), View (Camera coordinates), Projection (Screen coordinates)
glMatrixMode(GL_PROJECTION());
gluPerspective(40.0, 1.0, 1.0, 10.0);
glMatrixMode(GL_MODELVIEW());
gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.);
}
void display() {
glClear(GL_COLOR_BUFFER_BIT() | GL_DEPTH_BUFFER_BIT());
glPushMatrix();
glColor3f(0.0f, 1.0f, 0.0f);
glRotatef(10, 0.0f, 0.0f, 1.0f);
glRotatef(10, 0.0f, 1.0f, 0.0f);
glutWireTeapot(1);
glPopMatrix();
glutSwapBuffers();
}
public static void main(String[] args) {
try (var arena = Arena.ofConfined()) {
var argc = arena.allocateFrom(C_INT, 0);
glutInit(argc, argc);
glutInitDisplayMode(GLUT_DOUBLE() | GLUT_RGB() | GLUT_DEPTH());
glutInitWindowSize(500, 500);
glutCreateWindow(arena.allocateFrom("Hello Panama!"));
var teapot = new Teapot(arena);
var displayStub = glutDisplayFunc$callback.allocate(teapot::display, arena);
glutDisplayFunc(displayStub);
glutMainLoop();
}
}
}
این مثال از Arena.ofConfined() استفاده میکند. یک Arena محدود (Confined) مناسب است چون برنامه عمر مشخصی دارد.
Scope یک Arena محدود از زمان ساخت تا بسته شدن فعال است. یک Arena محدود یک Thread مالک دارد که همان Thread سازنده است.
فقط Thread مالک میتواند به Memory Segment های تخصیصیافته در Arena محدود دسترسی داشته باشد.
اگر با Thread غیرمالک سعی کنید Arena محدود را ببندید، Exception دریافت خواهید کرد.
Memory Segment argc با Arena با فراخوانی Arena.allocateFrom(OfInt,int) تخصیص داده شده و با عبارت زیر مقداردهی اولیه میشود: var argc = arena.allocateFrom(C_INT, 0);.
C_INT ثابتی است که توسط jextract تولید شده و مقدارش ValueLayout.JAVA_INT است. مقدار 0 برای مقداردهی اولیهی Memory Segment استفاده شده.
در نسخهی C کد، glutInit از متغیرهای اصلی main — یعنی argc و argv — استفاده میکند. نسخهی جاوا قصد پردازش آرگومانهای خط فرمان را ندارد و مقدار argc را صفر قرار میدهد. بنابراین متد glutInit Memory Segment مربوط به argc را دوباره استفاده میکند و بقیهی متد main تا حد امکان مشابه نسخهی بومی باقی میماند:
public static void main(String[] args) {
try (var arena = Arena.ofConfined()) {
var argc = arena.allocateFrom(C_INT, 0);
glutInit(argc, argc);
glutInitDisplayMode(GLUT_DOUBLE() | GLUT_RGB() | GLUT_DEPTH());
glutInitWindowSize(500, 500);
glutCreateWindow(arena.allocateFrom("Hello Panama!"));
var teapot = new Teapot(arena);
var displayStub = glutDisplayFunc$callback.allocate(teapot::display, arena);
glutDisplayFunc(displayStub);
glutMainLoop();
}
}
چون کد تولیدشده برای glutCreateWindow به یک MemorySegment نیاز دارد، آن را با Arena مقداردهیشده تخصیص میدهید و عنوان پنجره را در حافظهی Off-Heap مرتبط با Memory Segment ذخیره میکنید:
glutCreateWindow(arena.allocateFrom("Hello Panama!"));
در نهایت، با فراخوانی سازندهی Teapot.java با Arena یک Teapot ایجاد کنید و سپس Callback نمایش را برای پنجرهی فعلی فراخوانی کنید:
var teapot = new Teapot(arena);
var displayStub = glutDisplayFunc$callback.allocate(teapot::display, arena);
glutDisplayFunc(displayStub);
glutMainLoop();
برای اجرای مثال Teapot.java ابتدا کد تولیدشدهی freeglut را کامپایل کنید:
# macOS and Linux compatible command
javac -d . src/org/freeglut/*.java
# For windows there are too many sources for command line. Put them into separate file
ls -r src/*.java | %{ $_.FullName } | Out-File sources.txt
javac -d classes '@sources.txt'
سپس برنامهی Teapot.java را اجرا کنید:
# macOS and Linux compatible command
java -XstartOnFirstThread --enable-native-access=ALL-UNNAMED src/Teapot.java
# Windows PowerShell command
java -cp classes `
--enable-native-access=ALL-UNNAMED `
-D"java.library.path=C:\Windows\System32`;\path\to\freeglut\bin\x64" `
src\Teapot.java
باید یک کتری چای سبز رنگ ببینید:
هنگام دیباگ کردن برنامه، بررسی آرگومانهای پاسدادهشده به فراخوانیهای بومی مفید است.
کد تولیدشده توسط jextract از Trace کردن فراخوانیهای بومی پشتیبانی میکند. یعنی آرگومانهای پاسدادهشده به فراخوانیهای بومی میتوانند در خروجی استاندارد (Standard Output) چاپ شوند.
برای فعالسازی Trace، فقط پرچم -Djextract.trace.downcalls=true را بهعنوان آرگومان VM هنگام اجرای برنامه پاس دهید:
# macOS and Linux compatible command
java -XstartOnFirstThread -Djextract.trace.downcalls=true --enable-native-access=ALL-UNNAMED src/Teapot.java
# Windows PowerShell command
java -cp classes --enable-native-access=ALL-UNNAMED `
-D"jextract.trace.downcalls=true" `
-D"java.library.path=C:\Windows\System32`;\path\to\freeglut\bin\x64" `
src\Teapot.java
در ادامه بخشی از خروجی دستور بالا آمده است:
glutInit(MemorySegment{ address: 0x600001d9c080, byteSize: 4 }, MemorySegment{ address: 0x600001d9c080, byteSize: 4 })
glutInitDisplayMode(18)
glutInitWindowSize(500, 500)
glutCreateWindow(MemorySegment{ address: 0x600001d99070, byteSize: 14 })
glClearColor(0.0, 0.0, 0.0, 0.0)
glShadeModel(7425)
glLightfv(16384, 4611, MemorySegment{ address: 0x600001da4710, byteSize: 16 })
glLightfv(16384, 4608, MemorySegment{ address: 0x600001da4830, byteSize: 16 })
glLightfv(16384, 4609, MemorySegment{ address: 0x600001da4830, byteSize: 16 })
glLightfv(16384, 4610, MemorySegment{ address: 0x600001da4830, byteSize: 16 })
glMaterialfv(1028, 5633, MemorySegment{ address: 0x13789af10, byteSize: 452 })
glEnable(2896)
glEnable(16384)
glEnable(2929)
glutDisplayFunc(MemorySegment{ address: 0x11456c0c0, byteSize: 0 })
glutIdleFunc(MemorySegment{ address: 0x1145b6ac0, byteSize: 0 })
glutMainLoop()
Foreign Function and Memory API و ابزار jextract فایلهای هدر C را پشتیبانی میکنند. اما زبانهای دیگر نیز قابلیت Interop با C را دارند.
میتوانید همچنان از jextract برای ادغام با کتابخانههای نوشتهشده با آن زبانها از طریق یک لایهی C میانی استفاده کنید.
جدول زیر نشان میدهد کدام زبانها با jextract سازگار هستند و نحوهی استفاده چگونه است:
| زبان | روش دسترسی |
|---|---|
| C++ | C++ امکان اعلان متدهای C با استفاده از سینتکس extern "C" را فراهم میکند. بسیاری از کتابخانههای C++ رابط C نیز دارند. Jextract میتواند چنین رابط C را مصرف کند و از طریق آن به کتابخانه مورد نظر دسترسی پیدا کند. |
| Rust | اکوسیستم Rust ابزاری به نام cbindgen دارد که میتواند یک رابط C برای یک کتابخانهی Rust تولید کند. این رابط C تولیدشده سپس توسط jextract مصرف میشود و برای دسترسی به کتابخانه مورد نظر استفاده میگردد. |
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://dev.java/learn/