-
Notifications
You must be signed in to change notification settings - Fork 62
8263018: Improve API for lifecycle of native resources #466
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
8263018: Improve API for lifecycle of native resources #466
Conversation
Only StdLibTest has been ported to new API.
Add overloads to MemoryAddress::asSegmentRestricted
Tweak VaList API to use right abstractions in the right places
Use fork to prevent NativeScope from being closed accidentally by calling scope() on segments.
Fixed exception thrown in checkValidState in case of confined scope
Now these factories return non-closeable, managed segments. Todo: the VaList liveness code seems to be all over the place, I think the impl is in need of a revamp.
…closeable, managed segments.
Remove need to track "attached segments".
Now VaLists are simple views (like segments). They have a scope, and they do not implement AutoCloseable.
Rebase NativeScope on top of arena allocator. Improve efficiency of VaList builder code by using arena allocators.
Fix issues with raw scope exceptions not been caught leading to issues in ResourceScopeTest Fix ResourceScopeTest not calling cleanup manually if addOnClose failed.
Consolidate scope checks
Add benchmark for recycling allocation
|
|
|
A possible, improved naming for the two segment allocators: SegmentAllocator.of(MemorySegment) -> SegmentAllocator.slicing(MemorySegment) What do you think? |
Yes, that's better. I find it hard to come up with a better concise name for the former, allocation returns a sliced prefix of the given segment, thus allocations are aliased, which is fundamentally different to all the other cases. |
One option which I still find kind of appealing, is to add instance methods to connect the various APIs. E.g. instead: SegmentAllocator.of(scope) -> scope.toAllocator(); I think these conversions are great in certain contexts: think of a client slicing a segment and wanting to put the result of a native call in a certain place: Similarly, if you have an resource scope and simply want an allocator that returns segments attached to that scope: I find both more concise that their static variants, but also (crucially) easier to explain. What we lose if we go down that path is "independence" between the various abstractions - in the sense that a ResourceScope is no longer Panama-neutral. |
|
I've been doing some validation, e.g. port jextract to the new API: I think overall the results is neat. I liked how the new API doesn't force me to have a scope for everything (there are many cases where we need to create a segment on the fly, as a step to be able to call a function once) - simple cases can just rely on cleaner and the GC, thus simplifying the code. At the same time, complex cases where lifecycle of multiple resources is linked, can be expressed way more clearly, since the user is now in control of the previously hidden "scope" component. This patch adds overloads to native function wrappers returning structs, so that users can pass a NativeScope if they wish allocation to occur in that scope. To minimize disruption, jextract is generating NativeScope in the target package; I think some abstraction linking together ResourceScope and SegmentAllocator would be nice to have for the native use case, but we're unsure as to what the best way to expose this is. One option would be to have a simple subtype of SegmentAllocator called ScopedSegmentAllocator - which expresses the notion of "I'm allocator that has its own scope". I think this will also be useful for integration with Jim's QBA allocator. Another option would be to associate an optional scope to every SegmentAllocator. That is, add a default method to SegmentAllocator: Honestly, now that I look back at this, it doesn't seem as bad as I originally thought - most of the current allocators actually already have a natural scope - the only exception being malloc() - which doesn't have a scope, since all the returned segments are independent. Well, almost - you would at least still need to have SegmentAllocator <: AutoCloseable: Now, while SegmentAllocator::close can be specified to just do allocatorScope().close() and nothing more, there is a question as to whether an API that has sometimes closeable allocators is a good thing. It seems like, ultimately, having a sub-interface of SegmentAllocator would be cleaner - although I suspect the extra abstraction might look a tad dubious. |
I think that works very well for Less sure about ResourceScope scope = ... There might also be some interconnections with |
Good point - if we had a ScopedSegmentAllocator abstraction, I believe creating an allocator from a scope would not be as important (if clients need both an allocator and a scope, well they can just allocate a ScopedSegmentAllocator). |
I made an attempt at this - captured in the following javadoc: http://cr.openjdk.java.net/~mcimadamore/panama/resourceScope-javadoc_v6_exp/ There is a new ScopedMemoryAllocator - and some of the existing factory now return that. Note that now the difference between SegmentAllocator.malloc and SegmentAllocator.scoped is cleared, as only the latter gives you a ScopedSegmentAllocator. All the arena allocators are also ScopedSegmentAllocators. The recycling allocator has been moved off to the side in MemorySegment::toAllocator. Is this an improvement? While more verbose than the old native scope, I think there's a lot more composition going on here, as now users can select full spectrum of { shared, confined } x { bounded vs. unbounded allocation } x { explicit vs. implicit close } - the former NativeScope used to be a point (although common) in this space: confined x { bounded vs. unbounded allocation } x explicit close. |
|
Small observation: I realized that all segment allocators can be associated with a scope. For allocators in which the scope is not important, we can just return the global scope. In other words, associating a scope to an allocator is mostly harmless - either the allocator does have a lifecycle (and that is managed by the scope), or the allocator does not have one (in which case a neutral scope like global scope can be used). This might address the problem of having a single entity which is connected to both an allocator and a scope - but it doesn't solve the usability problem of the try-with-resources. Unless we make SegmentAlocator AutoCloseable. But, if we make SegmentAllocator auto-closeable on the basis that an allocator has-a scope - then, on that basis it seems that even MemorySegment, VaList, ... should join for the ride? This seems like a slippery slope. |
|
To bring this discussion closer to actual use cases - the thing that worries me is examples like these: (this can be found in the list of jextract samples, in [1]). This code just needs a native string (there is a similar case in the clang implementation used by jextract). Now, there are many ways to rewrite that code (for simplicity I'll just focus on the first two lines):
But there's a sneaky problem here: the function uses the address projection of the string segment - meaning that, at least in principle, the string segment can become unreachable (and hence freed) before the native function is executed. This seems bad - and to avoid that, explicit resource deallocation seems preferrable (unless we want to start playing with reachability fences).
This works as expected. There are two issues here: non-jextracted code cannot do this (as there's no NativeScope in the API); and, arguably, using an unbounded native scope just to allocate a single string seems overkill. That will probably allocate more memory than required, to name one.
This works, and has the same semantics as the code that it's replacing. Now maybe (3) is the best we can do - but I can't help but feeling that code like this will be common (e.g. converting a string on the fly and passing it to some native library) and having to create a scope and an allocator to be able to do that (so that the right semantics occurs) seems unfortunate. We could add an overload in But that also means that each API point accepting a SegmentAllocator will likely need a similar overload. [1] - https://github.com/openjdk/panama-foreign/blob/foreign-jextract/doc/panama_jextract.md |
Rewrite StdLib test not to use NativeScope Tweak names of allocator factories Fix javadoc
|
I've updated the branch. The main change is that now methods in CLinker have an overload accepting a ResourceScope - e.g. whenever we took an allocator, there is also a corresponding overload that takes only a scope. This way the user has a nice and gentle progression through the API complexity:
While (1) and (2) will cover most of the "one-liner" examples, more realistic tests, such as those found in jextract will probably go for (3) [as they do today]. From here, we have a couple of API extensions that we can evaluate and add later (they can be added in a compatible fashion): a. Make all entities that have a scope should also have a close() method- this means that, instead of scope().close(), users can say close(), and use a TWR Note that in the latest change I've converted StdLib test not to use NativeScope. I think the result is uniform and pleasing, and I didn't find myself missing it: I've also added some ResourceScope overloads to jextract: https://github.com/mcimadamore/panama-foreign/tree/jextract%2BresourceScope And tried to convert my pet OpenGL application not to use NativeScope and again, it's all pretty readable. |
PaulSandoz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking very good.
src/jdk.incubator.foreign/share/classes/jdk/incubator/foreign/CLinker.java
Outdated
Show resolved
Hide resolved
src/jdk.incubator.foreign/share/classes/jdk/incubator/foreign/CLinker.java
Outdated
Show resolved
Hide resolved
src/jdk.incubator.foreign/share/classes/jdk/incubator/foreign/CLinker.java
Outdated
Show resolved
Hide resolved
src/jdk.incubator.foreign/share/classes/jdk/incubator/foreign/CLinker.java
Outdated
Show resolved
Hide resolved
| * Return a scoped allocator; the instance returned by this method is lazily created and then shared | ||
| * upon subsequent requests. | ||
| */ | ||
| public SegmentAllocator allocator() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potentially MemoryScope could implement SegmentAllocator then there is no need for this method?
And then on SegmentAllocator:
static SegmentAllocator scoped(ResourceScope scope) {
return (MemoryScope) Objects.requireNonNull(scope);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes - I guess I didn't want to blur the two concepts too much - if we go down that path I think users will be able to cast a ResourceScope into a SegmentAllocator?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They could, but they should not since it's an implementation detail. (FWIW I don't think there is any impact to the integrity of the implementation.)
…CLinker.java Co-authored-by: Paul Sandoz <paul.d.sandoz@googlemail.com>
…CLinker.java Co-authored-by: Paul Sandoz <paul.d.sandoz@googlemail.com>
…CLinker.java Co-authored-by: Paul Sandoz <paul.d.sandoz@googlemail.com>
…CLinker.java Co-authored-by: Paul Sandoz <paul.d.sandoz@googlemail.com>
* Add methods to expose default allocator/scope
|
Addressed latest comments. I also added two new static methods, one to obtain a new "default scope" (that is, the implicit scope that is created when you call e.g. MemorySegment::allocateNative), and also the "default allocator" - e.g. an allocator that returns segments based on their own default scopes. javadoc here: |
JornVernee
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, all the changes still look good to me. I like the new names for the allocator factory methods and that overloads were added to CLinker that take a ResourceScope as well.
|
/integrate |
|
@mcimadamore Since your change was applied there have been 304 commits pushed to the
Your commit was automatically rebased without conflicts. Pushed as commit 4192f7b. 💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored. |
This patch brings the API improvements described in [1]. Note that this is a significant reshuffle of the API, and might have non-trivial source compatibility impact; the main moves are listed below:
MemorySegment is no longer AutoCloseable (ResourceScope is, and a MemorySegment has a ResourceScope)
Resources created without an explicit scope (e.g.
MemorySegment::allocateNative(long)) get a default scope, which is non-closeable and GC managed. In other words, users who do not bother with resource scopes, will get same functionalities (lifecycle-wise) that they get with the ByteBuffer APISupport for custom allocators is added via the
SegmentAllocatorinterface; now all API points which need to perform some allocation will accept an explicitSegmentAllocatorparameter. Where SegmentAllocator-less overloads are provided, the assumed semantics is that ofMemorySegment::allocateNative(long, long)which means the returned segments will be associated with the default scope and will not be closeable.The method handles generated by the linker will accept an additional leading parameter of type
SegmentAllocatorwhenever the native function returns a struct by value. There is an overload which allows to statically bind an allocator at MH creation time.The NativeScope abstraction has been removed. This follows the observation that with
SegmentAllocator::arenaBounded/Unboundedclients can already get pretty close to the functionalities provided by a NativeScope (that said, at least initially, it is likely that jextract will emit a NativeScope abstraction in the generated code, to minimize compatibility impact).As discussed in [1], the new API has a way to prevent a resource scope from being closed when operating on a resource associated with that scope; this is
ResourceScope::acquirewhich returns a so called resource scope handle (an AutoCloseable).Access modes are gone. We only kept read-only views (as they are needed to support mapped memory). Non-closeable segments can be obtained by using the acquire API.
Several methods in MemorySegment are also gone; e.g. all methods related to ownership changes (
withOwnerThread,share) as well as some of the predicate methdos (e.g.ownerThread, which is now available through the segment's scope). The overall philosophy is that a scope is assigned to a segment on creation; the scope contains details about how the segment is to be accessed, and these details cannot be altered after the fact.MemoryAddress::asSegmentRestrictednow takes an optional ResourceScope parameter - the scope to be associated with the returned (unsafe) segment. There is also an overload that doesn't take a ResourceScope, in which case the global scope (singleton, non-closeable scope) will be used. A similar argument applies toVaList.ofAddressRestricted.The linker will now accept a scope for the returned upcall stub segment - if no scope is provided, a default one (GC-managed, non-closeable) will be created instead. Same for
VaList::make.Overall, I think this patch makes the API cleaner by clarifying the role between lifcycles (e.g. resource scopes) and resources (segments, va lists, etc.) , w/o making clients much more verbose. We also did some internal validation (thanks Chris) to make sure that async byte buffer operation could be adjusted using the acquire mechanism. There are some minor outstanding issues which will need to be resolved (as part of this PR, or as a followup) - listed below:
should the default scope accept custom cleanup actions? Since this scope is created internally using our cleaner factory, I think there's a possibility that user-defined cleanup action might stall the cleaner forever.
should ResourceScope have a predicate to see if it's a default scope? How should it be called? In an earlier iteration I had
isCloseablewhich I don't think does a good job (as a scope can also not be closeable for different reasons).Are we ok with the ResourceScope::acquire/ResourceScope.Handle names? Also note that the latter is a simple AutoCloseable, but we need a subclass because the main AutoCloseable::close throws Throwable. Which is unfortunate.
What about NativeScope? Are we ok with not having it? Note that, without NativeScope, code like:
becomes:
E.g. one extra line in the user code. I believe this is not a huge deal, in the sense that in all our example (most of which are jextract based), the body of the try with resources if pretty biggie in comparison. That said, I'm open to reintroduce NativeScope - although probably in a different form - e.g. by having a sub-interface of SegmentAllocator called BoundedAllocator, which is essentially an allocator that has a scope. But we can also add this later depending on our experience with using the API.
That's all for now. Feedback welcome!
[1] - https://inside.java/2021/01/25/memory-access-pulling-all-the-threads/
Progress
Issue
Reviewers
Download
To checkout this PR locally:
$ git fetch https://git.openjdk.java.net/panama-foreign pull/466/head:pull/466$ git checkout pull/466To update a local copy of the PR:
$ git checkout pull/466$ git pull https://git.openjdk.java.net/panama-foreign pull/466/head