blob: f9addfffaee36dee3572113e907e852dd9858f31 [file] [log] [blame]
/*
* Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*
*/
package jdk.internal.foreign;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SegmentScope;
import java.lang.foreign.SegmentAllocator;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.ref.Cleaner;
import java.util.Objects;
import jdk.internal.misc.ScopedMemoryAccess;
import jdk.internal.vm.annotation.ForceInline;
/**
* This class manages the temporal bounds associated with a memory segment as well
* as thread confinement. A session has a liveness bit, which is updated when the session is closed
* (this operation is triggered by {@link MemorySessionImpl#close()}). This bit is consulted prior
* to memory access (see {@link #checkValidStateRaw()}).
* There are two kinds of memory session: confined memory session and shared memory session.
* A confined memory session has an associated owner thread that confines some operations to
* associated owner thread such as {@link #close()} or {@link #checkValidStateRaw()}.
* Shared sessions do not feature an owner thread - meaning their operations can be called, in a racy
* manner, by multiple threads. To guarantee temporal safety in the presence of concurrent thread,
* shared sessions use a more sophisticated synchronization mechanism, which guarantees that no concurrent
* access is possible when a session is being closed (see {@link jdk.internal.misc.ScopedMemoryAccess}).
*/
public abstract sealed class MemorySessionImpl
implements SegmentScope, SegmentAllocator
permits ConfinedSession, GlobalSession, SharedSession {
static final int OPEN = 0;
static final int CLOSING = -1;
static final int CLOSED = -2;
static final VarHandle STATE;
static final int MAX_FORKS = Integer.MAX_VALUE;
public static final MemorySessionImpl GLOBAL = new GlobalSession(null);
static final ScopedMemoryAccess.ScopedAccessError ALREADY_CLOSED = new ScopedMemoryAccess.ScopedAccessError(MemorySessionImpl::alreadyClosed);
static final ScopedMemoryAccess.ScopedAccessError WRONG_THREAD = new ScopedMemoryAccess.ScopedAccessError(MemorySessionImpl::wrongThread);
final ResourceList resourceList;
final Thread owner;
int state = OPEN;
static {
try {
STATE = MethodHandles.lookup().findVarHandle(MemorySessionImpl.class, "state", int.class);
} catch (Exception ex) {
throw new ExceptionInInitializerError(ex);
}
}
public Arena asArena() {
return new Arena() {
@Override
public SegmentScope scope() {
return MemorySessionImpl.this;
}
@Override
public void close() {
MemorySessionImpl.this.close();
}
@Override
public boolean isCloseableBy(Thread thread) {
Objects.requireNonNull(thread);
return ownerThread() == null || // shared
ownerThread() == thread;
}
};
}
public void addCloseAction(Runnable runnable) {
Objects.requireNonNull(runnable);
addInternal(ResourceList.ResourceCleanup.ofRunnable(runnable));
}
/**
* Add a cleanup action. If a failure occurred (because of a add vs. close race), call the cleanup action.
* This semantics is useful when allocating new memory segments, since we first do a malloc/mmap and _then_
* we register the cleanup (free/munmap) against the session; so, if registration fails, we still have to
* cleanup memory. From the perspective of the client, such a failure would manifest as a factory
* returning a segment that is already "closed" - which is always possible anyway (e.g. if the session
* is closed _after_ the cleanup for the segment is registered but _before_ the factory returns the
* new segment to the client). For this reason, it's not worth adding extra complexity to the segment
* initialization logic here - and using an optimistic logic works well in practice.
*/
public void addOrCleanupIfFail(ResourceList.ResourceCleanup resource) {
try {
addInternal(resource);
} catch (Throwable ex) {
resource.cleanup();
throw ex;
}
}
void addInternal(ResourceList.ResourceCleanup resource) {
checkValidState();
// Note: from here on we no longer check the session state. Two cases are possible: either the resource cleanup
// is added to the list when the session is still open, in which case everything works ok; or the resource
// cleanup is added while the session is being closed. In this latter case, what matters is whether we have already
// called `ResourceList::cleanup` to run all the cleanup actions. If not, we can still add this resource
// to the list (and, in case of an add vs. close race, it might happen that the cleanup action will be
// called immediately after).
resourceList.add(resource);
}
protected MemorySessionImpl(Thread owner, ResourceList resourceList) {
this.owner = owner;
this.resourceList = resourceList;
}
public static MemorySessionImpl createConfined(Thread thread) {
return new ConfinedSession(thread);
}
public static MemorySessionImpl createShared() {
return new SharedSession();
}
public static MemorySessionImpl createImplicit(Cleaner cleaner) {
return new ImplicitSession(cleaner);
}
@Override
public MemorySegment allocate(long byteSize, long byteAlignment) {
Utils.checkAllocationSizeAndAlign(byteSize, byteAlignment);
return NativeMemorySegmentImpl.makeNativeSegment(byteSize, byteAlignment, this);
}
public abstract void release0();
public abstract void acquire0();
@Override
public void whileAlive(Runnable action) {
Objects.requireNonNull(action);
acquire0();
try {
action.run();
} finally {
release0();
}
}
public final Thread ownerThread() {
return owner;
}
public static boolean sameOwnerThread(SegmentScope scope1, SegmentScope scope2) {
return ((MemorySessionImpl) scope1).ownerThread() ==
((MemorySessionImpl) scope2).ownerThread();
}
@Override
public final boolean isAccessibleBy(Thread thread) {
Objects.requireNonNull(thread);
return owner == null || owner == thread;
}
/**
* Returns true, if this session is still open. This method may be called in any thread.
* @return {@code true} if this session is not closed yet.
*/
public boolean isAlive() {
return state >= OPEN;
}
/**
* This is a faster version of {@link #checkValidState()}, which is called upon memory access, and which
* relies on invariants associated with the memory session implementations (volatile access
* to the closed state bit is replaced with plain access). This method should be monomorphic,
* to avoid virtual calls in the memory access hot path. This method is not intended as general purpose method
* and should only be used in the memory access handle hot path; for liveness checks triggered by other API methods,
* please use {@link #checkValidState()}.
*/
@ForceInline
public void checkValidStateRaw() {
if (owner != null && owner != Thread.currentThread()) {
throw WRONG_THREAD;
}
if (state < OPEN) {
throw ALREADY_CLOSED;
}
}
/**
* Checks that this session is still alive (see {@link #isAlive()}).
* @throws IllegalStateException if this session is already closed or if this is
* a confined session and this method is called outside of the owner thread.
*/
public void checkValidState() {
try {
checkValidStateRaw();
} catch (ScopedMemoryAccess.ScopedAccessError error) {
throw error.newRuntimeException();
}
}
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
public boolean isCloseable() {
return true;
}
/**
* Closes this session, executing any cleanup action (where provided).
* @throws IllegalStateException if this session is already closed or if this is
* a confined session and this method is called outside of the owner thread.
*/
public void close() {
justClose();
resourceList.cleanup();
}
abstract void justClose();
public static MemorySessionImpl heapSession(Object ref) {
return new GlobalSession(ref);
}
/**
* A list of all cleanup actions associated with a memory session. Cleanup actions are modelled as instances
* of the {@link ResourceCleanup} class, and, together, form a linked list. Depending on whether a session
* is shared or confined, different implementations of this class will be used, see {@link ConfinedSession.ConfinedResourceList}
* and {@link SharedSession.SharedResourceList}.
*/
public abstract static class ResourceList implements Runnable {
ResourceCleanup fst;
abstract void add(ResourceCleanup cleanup);
abstract void cleanup();
public final void run() {
cleanup(); // cleaner interop
}
static void cleanup(ResourceCleanup first) {
ResourceCleanup current = first;
while (current != null) {
current.cleanup();
current = current.next;
}
}
public abstract static class ResourceCleanup {
ResourceCleanup next;
public abstract void cleanup();
static final ResourceCleanup CLOSED_LIST = new ResourceCleanup() {
@Override
public void cleanup() {
throw new IllegalStateException("This resource list has already been closed!");
}
};
static ResourceCleanup ofRunnable(Runnable cleanupAction) {
return new ResourceCleanup() {
@Override
public void cleanup() {
cleanupAction.run();
}
};
}
}
}
// helper functions to centralize error handling
static IllegalStateException tooManyAcquires() {
return new IllegalStateException("Session acquire limit exceeded");
}
static IllegalStateException alreadyAcquired(int acquires) {
return new IllegalStateException(String.format("Session is acquired by %d clients", acquires));
}
static IllegalStateException alreadyClosed() {
return new IllegalStateException("Already closed");
}
static WrongThreadException wrongThread() {
return new WrongThreadException("Attempted access outside owning thread");
}
static UnsupportedOperationException nonCloseable() {
return new UnsupportedOperationException("Attempted to close a non-closeable session");
}
}