Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/user/Embedding-Native-Extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ The Context API allows you to set options such as `allowIO`, `allowHostAccess`,
To use Python native extensions on GraalPy, the `allowNativeAccess` option must be set to `true`, but this opens the door to full native access.
This means that while Python code may be denied access to the host file system, thread or subprocess creation, and more, the native extension is under no such restriction.

## Java Virtual Threads

Python native extensions are not compatible with Java virtual threads.
Native extensions commonly use native thread-local state, including CPython and extension-runtime state that cannot track Java virtual-thread scheduling.
If an embedded application may load or call native extensions, run that Python code on platform threads.
For example, server applications that use virtual threads for request handling should dispatch GraalPy calls that may use native extensions to a platform-thread executor.

## Memory Management

Python C extensions, like the CPython reference implementation, use reference counting for memory management.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,11 @@ private void generateNativeAccessSupport(Element[] origins) throws IOException {
lines.add(" return Linker.nativeLinker().upcallStub(staticMethodHandle, functionDescriptor, (Arena) arena).address();");
lines.add(" }");
lines.add("");
lines.add(" @Override");
lines.add(" protected boolean isCurrentThreadVirtualImpl() {");
lines.add(" return Thread.currentThread().isVirtual();");
lines.add(" }");
lines.add("");
lines.add(" private static FunctionDescriptor createFunctionDescriptor(NativeSimpleType resType, NativeSimpleType[] argTypes) {");
lines.add(" MemoryLayout[] argLayouts = new MemoryLayout[argTypes.length];");
lines.add(" for (int i = 0; i < argTypes.length; i++) {");
Expand Down Expand Up @@ -1598,6 +1603,11 @@ private void generateDummyNativeAccessSupport(Element[] origins) throws IOExcept
lines.add(" protected long createClosureImpl(MethodHandle staticMethodHandle, NativeSimpleType resType, NativeSimpleType[] argTypes, Object arena) {");
lines.add(" throw unsupported();");
lines.add(" }");
lines.add("");
lines.add(" @Override");
lines.add(" protected boolean isCurrentThreadVirtualImpl() {");
lines.add(" return false;");
lines.add(" }");
lines.add("}");

var file = processingEnv.getFiler().createSourceFile(NATIVE_ACCESS_PACKAGE + "." + NATIVE_ACCESS_SUPPORT_IMPL_CLASS_NAME, origins);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ private BackgroundGCTask(PythonContext context) {
super("Python GC", LOGGER);
this.ctx = new WeakReference<>(context);
this.rssInterval = context.getOption(PythonOptions.BackgroundGCTaskInterval);
this.gcRSSThreshold = context.getOption(PythonOptions.BackgroundGCTaskThreshold) / (double) 100;
this.gcGrowthThreshold = context.getOption(PythonOptions.BackgroundGCTaskThreshold) / (double) 100;
this.gcRSSMinimum = context.getOption(PythonOptions.BackgroundGCTaskMinimum);
}

Expand All @@ -624,7 +624,7 @@ private BackgroundGCTask(PythonContext context) {
// RSS monitor interval in ms
final int rssInterval;
/**
* RSS percentage increase between System.gc() calls. Low percentage will trigger
* Resources percentage increase between System.gc() calls. Low percentage will trigger
* System.gc() more often which can cause unnecessary overhead.
*
* <ul>
Expand All @@ -636,7 +636,7 @@ private BackgroundGCTask(PythonContext context) {
*
* <pre>
*/
final double gcRSSThreshold;
final double gcGrowthThreshold;

/**
* RSS minimum memory (in megabytes) start calling System.gc(). Default is 4GB.
Expand Down Expand Up @@ -695,19 +695,20 @@ private void perform() {
return;
}

if (rss < gcRSSMinimum) {
return;
}

// skip GC if no new native weakrefs have been created.
int currentWeakrefCount = context.handleContext.nativeLookup.size();
if (currentWeakrefCount < this.previousWeakrefCount || this.previousWeakrefCount == -1) {
this.previousWeakrefCount = currentWeakrefCount;
return;
}

double ratio = ((rss - this.previousRSS) / (double) this.previousRSS);
if (ratio >= gcRSSThreshold) {
double ratio = ((currentWeakrefCount - this.previousWeakrefCount) / (double) Math.max(1, this.previousWeakrefCount));
if (rss < gcRSSMinimum && ratio < gcGrowthThreshold) {
return;
}

ratio = ((rss - this.previousRSS) / (double) this.previousRSS);
if (ratio >= gcGrowthThreshold) {
this.previousWeakrefCount = currentWeakrefCount;

long start = System.nanoTime();
Expand All @@ -729,7 +730,7 @@ private void perform() {
* mappings (RssFile) and shmem memory (RssShmem). GC can only reduce RssAnon while
* RssFile is managed by the operating system which doesn't go down easily.
*/
this.previousRSS += (long) (this.previousRSS * gcRSSThreshold);
this.previousRSS += (long) (this.previousRSS * gcGrowthThreshold);
}
}
}
Expand Down Expand Up @@ -890,6 +891,9 @@ private static CApiContext loadCApi(Node node, PythonContext context, TruffleStr
String libName = PythonContext.getSupportLibName("python-native");
final TruffleFile capiFile = homePath.resolve(libName).getCanonicalFile();
try {
if (PythonContext.isCurrentThreadVirtual()) {
throw new ApiInitException(ErrorMessages.NATIVE_EXTENSIONS_VIRTUAL_THREAD);
}
boolean useNative = true;
boolean isolateNative = PythonOptions.IsolateNativeModules.getValue(env.getOptions());
final NativeLibraryLocator loc;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,8 @@ public abstract class ErrorMessages {
public static final TruffleString NFI_NOT_AVAILABLE = tsLiteral("GraalPy option '%s' is set to '%s, but the 'nfi' language, which is required for this feature, is not available. " +
"If this is a GraalPy standalone distribution: this indicates internal error. If GraalPy was used as a Maven dependency: " +
"are you missing a runtime dependency 'org.graalvm.truffle:truffle-nfi-libffi', which should be a dependency of 'org.graalvm.polyglot:python{-community}'?");
public static final TruffleString NATIVE_EXTENSIONS_VIRTUAL_THREAD = tsLiteral("Python native extensions cannot be used from Java virtual threads. " +
"Run Python code that may load or call native extensions on a platform thread.");

// AST Validator
public static final TruffleString ANN_ASSIGN_WITH_SIMPLE_NON_NAME_TARGET = tsLiteral("AnnAssign with simple non-Name target");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@
import com.oracle.graal.python.runtime.exception.PythonExitException;
import com.oracle.graal.python.runtime.exception.PythonThreadKillException;
import com.oracle.graal.python.runtime.locale.PythonLocale;
import com.oracle.graal.python.runtime.nativeaccess.NativeAccessSupport;
import com.oracle.graal.python.runtime.nativeaccess.NativeContext;
import com.oracle.graal.python.runtime.nativeaccess.NativeMemory;
import com.oracle.graal.python.runtime.object.IDUtils;
Expand Down Expand Up @@ -247,6 +248,10 @@ public final class PythonContext extends Python3Core {
// Used for testing only.
public boolean wasStackWalk;

public static boolean isCurrentThreadVirtual() {
return NativeAccessSupport.isCurrentThreadVirtual();
}

@TruffleBoundary
public static String getSupportLibName(PythonOS os, String libName) {
// note: this should be aligned with MX's "lib" substitution
Expand Down Expand Up @@ -2722,7 +2727,7 @@ public void initializeMultiThreading() {
public void attachThread(Thread thread, ContextThreadLocal<PythonThreadState> threadState) {
CompilerAsserts.neverPartOfCompilation();
PythonThreadState pythonThreadState = threadState.get(thread);
threadStateMapping.put(thread, pythonThreadState);
PythonThreadState previousThreadState = threadStateMapping.put(thread, pythonThreadState);
ReentrantLock initLock = getcApiInitializationLock();
/*
* Synchronize with C API initialization so that we do not miss eager initialization of this
Expand All @@ -2734,9 +2739,19 @@ public void attachThread(Thread thread, ContextThreadLocal<PythonThreadState> th
initLock.lock();
try {
if (getCApiState() == CApiState.INITIALIZED) {
if (isCurrentThreadVirtual()) {
throw PRaiseNode.raiseStatic(getLanguage().unavailableSafepointLocation, SystemError, ErrorMessages.NATIVE_EXTENSIONS_VIRTUAL_THREAD);
}
// initialize this thread's native TLS slot eagerly instead of on first use
initializeNativeThreadState(pythonThreadState);
}
} catch (PException e) {
if (previousThreadState == null) {
threadStateMapping.remove(thread);
} else {
threadStateMapping.put(thread, previousThreadState);
}
throw e;
} finally {
initLock.unlock();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,19 @@ public static void checkBytecodeDSLEnv() {
@Option(category = OptionCategory.INTERNAL, usageSyntax = "<time>", help = "Specifies the interval (ms) for the background GC task to monitor the resident set size (RSS)") //
public static final OptionKey<Integer> BackgroundGCTaskInterval = new OptionKey<>(1000);

@Option(category = OptionCategory.INTERNAL, usageSyntax = "<limit>", help = "The percentage increase in RSS memory between System.gc() calls. Low percentage will trigger System.gc() more often. (default: 30).") //
public static final OptionKey<Integer> BackgroundGCTaskThreshold = new OptionKey<>(30);
@Option(category = OptionCategory.INTERNAL, usageSyntax = "[1,100]", help = "The percentage increase in RSS memory between System.gc() calls. Low percentage will trigger System.gc() more often. (default: 30).") //
public static final OptionKey<Integer> BackgroundGCTaskThreshold = new OptionKey<>(30,
new OptionType<>("BackgroundGCTaskThreshold", input -> {
try {
int value = Integer.parseInt(input);
if (value >= 1 && value <= 100) {
return value;
}
} catch (NumberFormatException e) {
// fallthrough
}
throw new IllegalArgumentException("BackgroundGCTaskThreshold must be an integer in range [1, 100]");
}));

@Option(category = OptionCategory.INTERNAL, usageSyntax = "<megabytes>", help = "The minimum RSS memory (in megabytes) to start calling System.gc(). (default: 4 GB).") //
public static final OptionKey<Integer> BackgroundGCTaskMinimum = new OptionKey<>(4096);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ static long createClosure(MethodHandle staticMethodHandle, NativeSimpleType resT
return INSTANCE.createClosureImpl(staticMethodHandle, resType, argTypes, arena);
}

public static boolean isCurrentThreadVirtual() {
return INSTANCE.isCurrentThreadVirtualImpl();
}

private MethodHandle createTypedDowncallHandle(NativeSimpleType resType, NativeSimpleType... argTypes) {
Class<?>[] parameterTypes = new Class<?>[argTypes.length + 1];
parameterTypes[0] = long.class;
Expand All @@ -134,4 +138,6 @@ private MethodHandle createTypedDowncallHandle(NativeSimpleType resType, NativeS
protected abstract MethodHandle createDowncallHandleImpl(MethodType methodType, boolean critical);

protected abstract long createClosureImpl(MethodHandle staticMethodHandle, NativeSimpleType resType, NativeSimpleType[] argTypes, Object arena);

protected abstract boolean isCurrentThreadVirtualImpl();
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,9 @@ protected MethodHandle createDowncallHandleImpl(MethodType methodType, boolean c
protected long createClosureImpl(MethodHandle staticMethodHandle, NativeSimpleType resType, NativeSimpleType[] argTypes, Object arena) {
throw unsupported();
}

@Override
protected boolean isCurrentThreadVirtualImpl() {
return false;
}
}
Loading