JDK-8264781 : Implementation of Foreign Function and Memory API (Incubator)
  • Type: CSR
  • Component: core-libs
  • Priority: P4
  • Status: Closed
  • Resolution: Approved
  • Fix Versions: 17
  • Submitted: 2021-04-06
  • Updated: 2021-05-20
  • Resolved: 2021-05-13
Related Reports
CSR :  
Relates :  
Description
Summary
-------

This CSR refers to the latest iteration of the Foreign Memory Access and Foreign Linker API originally targeted for Java 14 and Java 16, respectively, with the goal of refining some of the API rough edges, as well as addressing the feedback received so far from developers.

Problem
-------

Real-world use of the Foreign Memory Access and Foreign Linker APIs revealed some remaining usability issues, listed below:

* Managing the life-cycle of memory segments is not intuitive, and can lead developers to write code that performs less efficiently than expected. For instance, the MemorySegment API heavily relies on dynamic ownership changes (see `MemorySegment::handoff`), where the ownership of a segment is updated in place, by killing the old segment and return a new one with new ownership characteristics. While for confined segment such an operation can be supported at a relatively low cost, the same cannot be said for shared segment (introduced in Java 16); since shared segment feature a relatively expensive `close` operation, it is generally not feasible to dynamically change ownership of a shared segment in critical code paths.

* Shared segments makes it hard to support asynchronous IO operations (e.g. `SocketChannel::read(ByteBuffer)`) on buffer views obtained by such segments; this is due to the fact that a shared segment can be closed by any thread - possibly in the middle of an async IO operation. The API did not provide any workaround for this use case; for this reason, asynchronous operations on byte buffer views obtained from shared segments are not supported (as stated in the javadoc for `MemorySegment::asByteBuffer`), and, as a result, clients are faced with a choice between shared segment and full byte buffer interoperability.

* When linking a foreign function, the address of said function has to be specified ahead of time (e.g. when the native method handle is obtained); while this is good in most cases when the user wants to call a foreign functions, some use cases require more flexibility. Certain libraries in fact, provide extension points by means of function pointers, with a well-defined signature. In such cases, it would be preferable to obtain a native method handle without having to specify an address, and late-bind the address when the foreign call is made.

* Allocating native memory can often be the source of performance bottlenecks in real world applications; while the API allows a more optimized allocation, using the `NativeScope` abstraction, not all the API points which perform allocation can be provided the desired allocation strategy. For instance, it is not possible to pass a `NativeScope` to a native function which returns a struct by value. In addition, `NativeScope` only represent a possible choice in the allocator design space: the allocation scheme it provides is, essentially, a thread-confined arena-based allocation. It is not possible to make `NativeScope` more general and/or to support different allocation schemes.

* Some of the static factory methods in the API are hostile to the use of static imports - for instance, consider `MemoryLayout.ofSequence`.

* Both the Foreign Memory Access API and the Foreign Linker API contained *unsafe* API points, called *restricted methods*; access to restricted methods is controlled by a read-only JDK property (namely, `foreign.restricted`), whose value must be explicitly set to `permit` for access to occur. While this solution is good enough to make restricted methods disabled by default, the use of a JDK property as a means to do so is rather ad-hoc and brittle.

Solution
-------

Here we describe the main ideas behind the API changes brought forward in this CSR:

* The new API models segment life-cycle with an explicit abstraction, named `ResourceScope`. A `ResourceScope` is a stateful abstraction which can be either alive, or closed. Resources managed by a resource scope can only be accessed if the owning resource scope is alive. Resource scopes can be confined (e.g. only thread which created the scope has access to the managed resources) or shared, and can be (optionally) associated with a `Cleaner` object, which is responsible for closing the scope in case the scope becomes unreachable. By making the `ResourceScope` abstraction a first-class citizen in the API, the semantics of other abstractions such as `MemorySegment`, `MemoryAddress`, `VaList` becomes clearer: these abstractions cannot, in fact, be closed in isolation - they can instead be closed, by closing the resource scope they belong to. This allowed us to greatly simplify the `MemorySegment` abstraction, and remove all API points related to dynamic ownership changes. Also, `MemorySegment` is no longer `AutoCloseable` (but `ResourceScope` is), which further clarifies the semantics of the API when multiple segments (e.g. slices) are owned by the same scope.

* As we have seen, there are cases where APIs might want to temporarily inhibit deterministic deallocation. The API now allows clients to *acquire* a `ResourceScope`; this returns a `ResourceScope.Handle` instance. This idiom can be used to perform operations on a segment (or any other resource managed by the scope) while the scope handle is being held by the client. Any attempt to close the resource scope when one or more handles have been acquired will result in an exception. When a client no longer need to work on a resource associated with the acquire scope, the acquire handle can be *released*. This allows to address interoperability issues between memory segments and byte buffer views in the context of asynchronous IO operations.

* The `CLinker` interface now offers an overload of `downcallHandle` which accept no `Addressable` parameter; the native method handle returned by this overload instead accepts an additional prefix argument (of type Addressable) which can be used to specify the foreign function entry point at call-time, rather than at link-time.

* A new abstraction, namely `SegmentAllocator`, replaces `NativeScope`. `SegmentAllocator`  is a functional interface which provides several default methods which help when allocating off-heap memory from Java objects (e.g. on heap arrays). The `SegmentAllocator` interface provides some ready-made allocators, such as an arena allocator, which provides the same optimized allocation scheme previously provided by `NativeScope`. Since now `SegmentAllocator` and `ResourceScope` are independent entities, clients can combine them at will, meaning that it is now possible. for instance, to create an arena allocator out of a shared resource scope. Several API points in `CLinker` have been enhanced to take an extra `SegmentAllocator` parameter, instead of the `NativeScope` parameter accepted previously. Most notably, all native method handles generated by `CLinker::downcallHandle` will now accept an additional `SegmentAllocator` parameter, in case the foreign function returns a `MemorySegment`. (An additional overload of `CLinker::downcallHandle` allows the user to select an allocator at link-time, if desired). To facilitate conversion from `ResourceScope` to `SegmentAllocator`, some API points in `CLinker` (e.g. `CLinker::toCString`) define overloads accepting a `ResourceScope` parameter; this scope is then internally converted into a so called *scoped* allocator (an allocator which allocates segment tied to the provided scope). This makes the API simpler to use in case clients do not require an alternate, more efficient allocation strategy.

* Some of the static factory methods in the API were renamed to be more friendly with respect to static imports. All factories in the layout API have been renamed - e.g. from `ofXYZ` to `XYZlayout`. In the newly added APIs (`ResourceScope` and `SegmentAllocator`) we used the `new` prefix whenever allocating a new instance was an important part of the API contract (this is especially true for resource scope creation). We also used the `of` prefix to denote a loose conversion - e.g. `SegmentAllocator.ofScope` is used to create an allocator from a resource scope.

* To allow access to restricted methods in the API, a new experimental command line option in the Java launcher is added, namely `--enable-native-access=<module list>`. This options accepts a list of modules (separated by commas), where a module name can also be `ALL-UNNAMED` (for the unnamed module). Adding this command line flag to the launcher has the effect of allowing access to restricted methods to a given set of modules (the list of modules specified in the command line option). Access to restricted methods from any other module not in the list is disallowed and will result in an `IllegalCallerException`.

Specification
-------------

A specdiff of the changes as of April 29th, 2021 has been attached to this CSR (v2).

A link of the latest javadoc (as of April 29th, 2021) is included below:

http://cr.openjdk.java.net/~mcimadamore/JEP-412/v2/javadoc/jdk/incubator/foreign/package-summary.html

A link of the latest specdiff (as of April 29th, 2021) is included below:

http://cr.openjdk.java.net/~mcimadamore/JEP-412/v2/specdiff/overview-summary.html

Comments
I've uploaded a new specdiff (v3) which contains the changes to add support for sealing. There's no other change in the API.
20-05-2021

Thanks. For the annotation, we will implement this in 18, as more tooling support will become available (javadoc, and, maybe javac). I'll add address the remaining comments, and coordinate w.r.t. sealing.
13-05-2021

Moving to Approved, with comments. A recommend including a Restricted annotation type with runtime retention to make the restricted methods in addition to the textual note. Besides providing a marker that could be queried programmatically, usages of the Restricted annotation would provide a listing of all the restricted methods. It is recommended practice to have @implSpec tags for the default methods in SegmentAllocator to specify how they call the abstract allocate���(long bytesSize, long bytesAlignment) method. Assuming sealed types go into 17, if the bug corresponding to this CSR hasn't been pushed yet, either this CSR can be updated for the use of sealing or a follow-up bug and CSR can be filed.
13-05-2021

Thanks for the review, as for @Documented, the long term story will be to replicate the same model we have for preview APIs - that is, have a JDK internal annotation, and have javadoc act on it in a special way (by generating extra text).
20-04-2021

Moving to Provisional. Was consideration given to marked restricted methods with, say, a @Documented Restricted annotation? That would facilitate the ability to make static checks for restricted methods.
20-04-2021