گاهی توابع خارجی (Foreign Functions) یک ناحیهی حافظه تخصیص میدهند و سپس اشارهگری به آن ناحیه برمیگردانند. نمونهی بارز آن تابع استاندارد C به نام void *malloc(size_t) است که مقدار درخواستشده حافظه (بر حسب بایت) را تخصیص میدهد و اشارهگری به آن برمیگرداند.
فراخوانی یک تابع استاندارد C مثل malloc مراحل مشابهی با آموزشهای قبلی دارد:
import java.lang.foreign.*;
import java.lang.invoke.*;
String s = "Panama project is cool!";
// Obtain an instance of the native linker
Linker linker = Linker.nativeLinker();
// Locate the address of malloc()
var malloc_addr = linker.defaultLookup().find("malloc").orElseThrow();
// Create a downcall handle for malloc()
MethodHandle malloc = linker.downcallHandle( malloc_addr,
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)
);
long byteSize = ValueLayout.JAVA_CHAR.byteSize() * (s.length() + 1);
// Invoke malloc(), which returns a pointer
MemorySegment segment = (MemorySegment) malloc.invokeExact(byteSize);
// The size of the memory segment created by malloc()
IO.println(
"Size, in bytes, of memory segment created by calling malloc.invokeExact(" +
byteSize + "): " + segment.byteSize());
//...
اگر قطعه کد بالا را در یک نشست jshell اجرا کنید، خروجی شامل مورد زیر خواهد بود:
Size, in bytes, of memory segment created by calling malloc.invokeExact(48): 0
وقتی یک تابع بومی مثل malloc را فراخوانی میکنید که اشارهگری برمیگرداند، Runtime جاوا اطلاعی از اندازه یا عمر Memory Segment مورد نظر اشارهگر ندارد.
در نتیجه، FFM API از یک Memory Segment با طول صفر (Zero-Length) برای نمایش اشارهگر برگشتی malloc استفاده میکند. Memory Segment های با طول صفر معمولاً برای موارد زیر استفاده میشوند:
علاوه بر آن، اگر سعی کنید به محتوای یک Memory Segment با طول صفر دسترسی پیدا کنید، Runtime جاوا IndexOutOfBoundsException پرتاب میکند.
دلیل آن این است که JVM نمیتواند عملیات دسترسی به ناحیهی حافظهای با اندازهی نامشخص را بهصورت ایمن انجام یا اعتبارسنجی کند.
اگرچه Memory Segment های با طول صفر مستقیماً قابل دسترسی نیستند، هدفشان این است که به توابع خارجی دیگری که اشارهگر میپذیرند پاس داده شوند.
از منظر کتابخانهی استاندارد C، میتوانید از free — تابع آزادسازی حافظه — استفاده کنید:
import java.lang.foreign.*;
import java.lang.invoke.*;
String s = "Panama project is cool!";
// Obtain an instance of the native linker
Linker linker = Linker.nativeLinker();
// Locate the address of malloc()
var malloc_addr = linker.defaultLookup().find("malloc").orElseThrow();
// Create a downcall handle for malloc()
MethodHandle malloc = linker.downcallHandle( malloc_addr,
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)
);
long byteSize = ValueLayout.JAVA_CHAR.byteSize() * (s.length() + 1);
// Invoke malloc(), which returns a pointer
MemorySegment segment = (MemorySegment) malloc.invokeExact(byteSize);
// The size of the memory segment created by malloc()
IO.println(
"Size, in bytes, of memory segment created by calling malloc.invokeExact(" +
byteSize + "): " + segment.byteSize());
// Locate the address of free()
var free_addr = linker.defaultLookup().find("free").orElseThrow();
// Create a downcall handle for free()
MethodHandle free = linker.downcallHandle(free_addr, FunctionDescriptor.ofVoid(ValueLayout.ADDRESS));
Consumer<MemorySegment> cleanup = s -> {
try {
free.invokeExact(s);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
در مثال JVM، Memory Segment های با طول صفر با یک Scope جدید مرتبط هستند که همیشه فعال است. پس اگر JVM نمیتواند عمر یک Memory Segment با طول صفر را مدیریت کند، چگونه میتوان با آن کار کرد؟
میتوانید از متدهای MemorySegment.reinterpret برای دسترسی ایمن به Memory Segment های با طول صفر و پیوستن آنها به یک Arena موجود استفاده کنید.
Arena عمر ناحیهی حافظهی پشتیبان Segment را بهصورت خودکار مدیریت میکند. قطعه کد کاملی که حافظهی Off-Heap را با malloc تخصیص میدهد به صورت زیر است:
import java.lang.foreign.*;
import java.lang.invoke.*;
String s = "Panama project is cool!";
try (Arena arena = Arena.ofConfined()) {
// Obtain an instance of the native linker
Linker linker = Linker.nativeLinker();
// Locate the address of malloc()
var malloc_addr = linker.defaultLookup().find("malloc").orElseThrow();
// Create a downcall handle for malloc()
MethodHandle malloc = linker.downcallHandle( malloc_addr,
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)
);
long byteSize = ValueLayout.JAVA_CHAR.byteSize() * (s.length() + 1);
// Invoke malloc(), which returns a pointer
MemorySegment segment = (MemorySegment) malloc.invokeExact(byteSize);
// The size of the memory segment created by malloc()
IO.println(
"Size, in bytes, of memory segment created by calling malloc.invokeExact(" +
byteSize + "): " + segment.byteSize());
// Locate the address of free()
var free_addr = linker.defaultLookup().find("free").orElseThrow();
// Create a downcall handle for free()
MethodHandle free = linker.downcallHandle(free_addr, FunctionDescriptor.ofVoid(ValueLayout.ADDRESS));
Consumer<MemorySegment> cleanup = s -> {
try {
free.invokeExact(s);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
var nativeText = segment.reinterpret(byteSize, arena, cleanup);
}
در این مثال، متد MemorySegment.reinterpret(long,Arena,Consumer) سه آرگومان نیاز دارد:
malloc به آن اشاره میکند.برای بررسی دقیقتر نحوهی عملکرد این مثال، میتوانید آن را با دسترسی به حافظهی Off-Heap nativeText تکمیل کنید و در یک نشست jshell اجرا کنید:
import java.lang.foreign.*;
import java.lang.invoke.*;
String s = "Panama project is cool!";
try (Arena arena = Arena.ofConfined()) {
// Obtain an instance of the native linker
Linker linker = Linker.nativeLinker();
// Locate the address of malloc()
var malloc_addr = linker.defaultLookup().find("malloc").orElseThrow();
// Create a downcall handle for malloc()
MethodHandle malloc = linker.downcallHandle( malloc_addr,
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)
);
long byteSize = ValueLayout.JAVA_CHAR.byteSize() * (s.length() + 1);
// Invoke malloc(), which returns a pointer
MemorySegment segment = (MemorySegment) malloc.invokeExact(byteSize);
// The size of the memory segment created by malloc()
IO.println(
"Size, in bytes, of memory segment created by calling malloc.invokeExact(" +
byteSize + "): " + segment.byteSize());
// Locate the address of free()
var free_addr = linker.defaultLookup().find("free").orElseThrow();
// Create a downcall handle for free()
MethodHandle free = linker.downcallHandle(free_addr, FunctionDescriptor.ofVoid(ValueLayout.ADDRESS));
Consumer<MemorySegment> cleanup = s -> {
try {
free.invokeExact(s);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
var nativeText = segment.reinterpret(byteSize, arena, cleanup);
// Access off-heap memory
for (int i = 0; i < s.length(); i++ ) {
nativeText.setAtIndex(ValueLayout.JAVA_CHAR, i, s.charAt(i));
}
// Add the string terminator at the end
nativeText.setAtIndex(ValueLayout.JAVA_CHAR, s.length(), Character.MIN_VALUE);
// Print the string
for (int i = 0; i < s.length(); i++ ) {
IO.print((char)nativeText.getAtIndex(ValueLayout.JAVA_CHAR, i));
}
}
خروجی در jshell مشابه مورد زیر خواهد بود:
s ==> "Panama project is cool!"
Size, in bytes, of memory segment created by calling malloc.invokeExact(48): 0
Panama project is cool!
---
مثالهای کدی این مجموعه هیچ خطایی پرتاب نمیکنند، اما برخی توابع استاندارد C خطاها را با تنظیم مقدار ماکروی errno نشان میدهند.
مقدار errno از طریق FFM API نیز قابل دسترسی است.
برای دریافت مقدار errno مطابق با تعریف کتابخانهی استاندارد C، میتوانید از Linker.Option.captureCallState(String) استفاده کنید. این گزینه برای ذخیرهسازی برخی متغیرهای Thread-Local استفاده میشود.
Linker.Option.captureCallState(String) بخشهایی از وضعیت اجرا را بلافاصله پس از فراخوانی یک تابع خارجی مرتبط با یک Downcall Method Handle ذخیره میکند.
یک تابع استاندارد C که errno را تنظیم میکند، fopen است. این تابع فایلی را باز میکند که مسیرش رشتهی اشارهشده توسط filename است و یک Stream با آن مرتبط میسازد.
FILE *fopen(const char *filename, const char *mode);
آرگومان mode به رشتهای اشاره میکند که با یکی از مقادیر زیر شروع میشود:
r یا rb — باز کردن فایل برای خواندن.w یا wb — بریدن به طول صفر یا ایجاد فایل برای نوشتن.a یا ab — الحاق؛ باز کردن یا ایجاد فایل برای نوشتن در انتهای فایل.r+ یا rb+ یا r+b — باز کردن فایل برای خواندن و نوشتن.w+ یا wb+ یا w+b — بریدن به طول صفر یا ایجاد فایل برای بروزرسانی.a+ یا ab+ یا a+b — الحاق؛ باز کردن یا ایجاد فایل برای بروزرسانی با نوشتن در انتهای فایل.اگر فایل با موفقیت باز شود، fopen اشارهگری به شیء کنترلکنندهی Stream برمیگرداند.
در غیر این صورت، یک اشارهگر خالی (Null) برگردانده میشود و errno برای نشان دادن خطا تنظیم میشود.
برای دریافت پیام خطا میتوانید تابع استاندارد C به نام strerror را فراخوانی کنید که توصیف متنی مقدار errno را برمیگرداند.
برای نمایش نحوهی عملکرد errno و strerror، تابع fopen را با استفاده از captureCallState("errno") برای دریافت پیامهای خطا فراخوانی میکنیم:
import java.lang.foreign.*;
import java.lang.invoke.*;
import static java.lang.foreign.MemoryLayout.*;
static void invokeFopen(String path, String mode) throws Throwable {
// Setup handles
Linker.Option ccs = Linker.Option.captureCallState("errno");
StructLayout capturedStateLayout = Linker.Option.captureStateLayout();
VarHandle errnoHandle = capturedStateLayout.varHandle(PathElement.groupElement("errno"));
// log C Standard Library function
Linker linker = Linker.nativeLinker();
SymbolLookup stdLib = linker.defaultLookup();
MethodHandle fopen =linker.downcallHandle(stdLib.find("fopen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),
ccs);
// strerror C Standard Library function
MethodHandle strerror = linker.downcallHandle(
stdLib.find("strerror").orElseThrow(),
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT));
// Actual invocation
try (Arena arena = Arena.ofConfined()) {
MemorySegment capturedState = arena.allocate(capturedStateLayout);
MemorySegment location = arena.allocateFrom(path);
MemorySegment openMode = arena.allocateFrom(mode);
var result = (MemorySegment) fopen.invokeExact(capturedState, location, openMode);
if (result.address() == 0) {
// Get more information by consulting the value of errno:
int errno = (int) errnoHandle.get(capturedState, 0);
IO.println("errno: " + errno); // 2
// Convert errno code to a string message:
String errrorString = ((MemorySegment) strerror.invokeExact(errno))
.reinterpret(Long.MAX_VALUE).getString(0);
IO.println("errno string: " + errrorString);
}
}
}
قطعه کد بالا را در یک نشست jshell کپی و پیست کنید و سپس آن را با چند مقدار فراخوانی کنید.
اگر سعی کنید فایلی در مسیر مشخص با حالت r باز کنید و آن فایل وجود نداشته باشد، تابع بومی fopen مقدار errno را برابر 2 تنظیم میکند.
jshell> invokeFopen("duke.txt", "r"); //open a file that doesn't exist
WARNING: A restricted method in java.lang.foreign.Linker has been called
WARNING: java.lang.foreign.Linker::downcallHandle has been called by REPL.$JShell$15 in an unnamed module
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
errno: 2
errno string: No such file or directory
jshell> invokeFopen("duke.txt", "w"); // create a file for writing
jshell> invokeFopen("duke.txt", "r"); // try again to open a file
اگرچه رهگیری errno در درک بازخورد هنگام استفاده از توابع بومی C مفید است، احتمالاً متوجه وجود برخی هشدارها هنگام اجرای قطعه کد بالا شدهاید. بخش بعدی علت این هشدارها و اهمیتشان را بررسی میکند.
در FFM API مجموعهی دیگری از متدها وجود دارد که ناامن هستند و به همین دلیل محدود (Restricted) شدهاند. اگر برنامهای اجرا کنید که نیاز به فراخوانی این متدها دارد، Runtime جاوا یک پیام هشدار چاپ خواهد کرد.
jshell> invokeFopen("duke.txt", "r"); //open a file that doesn't exist
WARNING: A restricted method in java.lang.foreign.Linker has been called
WARNING: java.lang.foreign.Linker::downcallHandle has been called by REPL.$JShell$15 in an unnamed module
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
errno: 2
errno string: No such file or directory
اگر کد در یک Module M نیاز به استفاده از این متدهای محدود یا هر متغیر ناامنی بدون هشدار داشته باشد، باید دسترسی به آنها را با مشخص کردن گزینهی خط فرمان --enable-native-access=M فعال کنید. اگر چند Module نیاز به دسترسی به متدهای محدود دارند، از لیست جداشده با کاما استفاده کنید.
برای فعالسازی استفاده بدون هشدار برای همهی کدهای موجود در Class Path، گزینه --enable-native-access=ALL-UNNAMED را مشخص کنید.
همچنین میتوانید همین کار را برای یک JAR اجرایی با مشخص کردن صفت Enable-Native-Access: ALL-UNNAMED در Manifest آن انجام دهید.
هشدار: نمیتوانید نام Module را بهعنوان مقدار صفت
Enable-Native-Accessمشخص کنید.
| متد | چرا این متد محدود شده |
|---|---|
SymbolLookup.libraryLookup(String, Arena)، SymbolLookup.libraryLookup(Path, Arena) | بارگذاری کتابخانه همیشه باعث اجرای کد بومی میشود. برای مثال، در Linux میتواند از طریق Hook های dlopen اجرا شود. |
MemorySegment.reinterpret(long)، MemorySegment.reinterpret(long, Arena, Consumer)، MemorySegment.reinterpret(Arena, Consumer) | این متدها به شما امکان تغییر اندازه و عمر یک Segment موجود را با ایجاد یک نام مستعار (Alias) جدید به همان ناحیهی حافظه میدهند. برای مثال، ممکن است برنامه اندازهی ناحیه را بیشازحد تخمین بزند و با MemorySegment.reinterpret(long,Arena,Consumer) Segmentای به طول 100 بایت دریافت کند. این موضوع میتواند به تلاش برای دسترسی به حافظه خارج از محدودهی ناحیه منجر شود که ممکن است باعث کرش JVM یا بدتر از آن، فساد خاموش حافظه شود. |
Linker.upcallStub(MethodHandle, FunctionDescriptor, Arena, Linker.Option...) | هنگام ساخت Downcall Handle ها، لینکر نمیتواند بررسی کند آیا Function Pointerای که میسازید، دقیقاً همانی است که برای Downcall مورد نظر نیاز دارید. |
Linker.downcallhandle(FunctionDescriptor, Linker.Option...)، Linker.downcallhandle(MemorySegment, FunctionDescriptor, Linker.Option...) | ساخت یک Downcall Method Handle ذاتاً ناامن است. لینکر هیچ راهی برای تأیید سازگاری Function Descriptor ارائهشده با تابع فراخوانیشده ندارد. |
AddressLayout.withTargetLayout(MemoryLayout) | وقتی یک Address Layout با یک Target Layout مشخص دارید، میتوانید از آن در عملیات Dereference (مثل MemorySegment.get(AddressLayout, long)) برای تغییر اندازهی Segment در حال خواندن استفاده کنید که ناامن است. |
java.lang.ModuleLayer.Controller.enableNativeAccess(Module) | این متد دسترسی بومی را برای Module مشخصشده فعال میکند، اما فقط اگر Module فراخوانیکننده دسترسی بومی داشته باشد. این متد به این دلیل محدود شده که امتیازات فراخوانی متدهای محدود را انتقال میدهد. |
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://dev.java/learn/