Skip to content

Commit 335ffd2

Browse files
committed
[GR-61464] Minor docs updates.
PullRequest: graalpython/3648
2 parents 24f8fbe + 4d81cb4 commit 335ffd2

File tree

12 files changed

+84
-548
lines changed

12 files changed

+84
-548
lines changed

docs/contributor/IMPLEMENTATION_DETAILS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,14 @@ the next run of the cycle GC on the native side.
307307

308308
On Linux, Python native extensions expect to lookup Python C API functions in the global namespace and specify no explicit dependency on any libpython.
309309
To isolate them, we copy them with a new name, change their `SONAME`, add a `DT_NEEDED` dependency on a copy of our libpython shared object, and finally load them with `RTLD_LOCAL`.
310+
The ELF format is not really meant to allow such modifications after the fact.
311+
The most widely used tool to do so, [patchelf](https://github.com/NixOS/patchelf), still sees regular bugfixes and can corrupt ELF files.
310312

311313
On Windows there is no global namespace so native extensions already have a dependency on our libpython DLL.
312314
We copy them and just change the dependency to point to the context-local copy of libpython rather than the global one.
313315

314316
On macOS, while two-level namespaces exist, Python extensions historically use `-undefined dynamic_lookup` where they (just like in Linux) expect to find C API functions in any loaded image.
315317
We have to apply a similar workaround as on Linux, copy to a new name, change the `LC_ID_DYLIB` to that name, and add a `LC_LOAD_DYLIB` section to make the linker load the symbols from our libpython.
318+
This is currently not fully implemented.
316319

317320
Note that any code signatures are invalidated by this process.

docs/user/Native-Extensions.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ You can also manually trigger the detector with the Python `gc.collect()` call.
4949

5050
### Multi-Context and Native Libraries
5151

52+
Using C extensions in multiple contexts is only possible on Linux for now, and many C extensions still have issues in this mode.
53+
You should test your applications thoroughly if you want to use this feature.
54+
There are many possiblities for native code to sidestep the library isolation through other process-wide global state, corrupting the state and leading to incorrect results or crashing.
55+
The implementation also relies on a venv to work, even if you are not using external packages.
56+
5257
To support creating multiple GraalPy contexts that access native modules within the same JVM or Native Image, we need to isolate them from each other.
5358
The current strategy for this is to copy the libraries and modify them such that the dynamic library loader of the operating system will isolate them for us.
5459
To do this, all GraalPy contexts in the same process (not just those in the same engine!) must set the `python.IsolateNativeModules` option to `true`.

docs/user/Python-on-JVM.md

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ This enables you, for example, to use a Pandas frame as `double[][]` or NumPy ar
109109
### Special Jython Module: `jarray`
110110

111111
GraalPy implements the `jarray` module (to create primitive Java arrays) for compatibility.
112+
This module is always available, since we have not found its presence to have a negative impact.
112113
For example:
113114

114115
```python
@@ -142,10 +143,7 @@ However, implicitly, this may produce a copy of the array data, which can be dec
142143

143144
### Exceptions from Java
144145

145-
To catch Java exceptions, use the `--python.EmulateJython` option.
146-
147-
> Note: Catching a Java exception incurs a performance penalty.
148-
146+
You can catch Java exceptions as you would expect.
149147
For example:
150148

151149
```python
@@ -161,8 +159,8 @@ For example:
161159

162160
### Java Collections
163161

164-
* Java arrays and collections that implement the `java.util.Collection` interface can be accessed using the `[]` syntax.
165-
An empty collection is considered `false` in boolean conversions.
162+
* Java arrays and collections that implement the `java.util.Collection` interface can be accessed using the `[]` syntax.
163+
An empty collection is considered `false` in boolean conversions.
166164
The length of a collection is exposed by the `len` built-in function.
167165
For example:
168166

@@ -186,7 +184,7 @@ For example:
186184
False
187185
```
188186

189-
* Java iterables that implement the `java.lang.Iterable` interface can be iterated over using a `for` loop or the `iter` built-in function and are accepted by all built-ins that expect an iterable.
187+
* Java iterables that implement the `java.lang.Iterable` interface can be iterated over using a `for` loop or the `iter` built-in function and are accepted by all built-ins that expect an iterable.
190188
For example:
191189

192190
```python
@@ -242,12 +240,24 @@ For example:
242240

243241
### Inheritance from Java
244242

245-
Inheriting from a Java class (or implementing a Java interface) is supported with some syntactical differences from Jython.
246-
To create a class that inherits from a Java class (or implements a Java interface), use the conventional Python `class` statement: declared methods
247-
override (implement) superclass (interface) methods when their names match.
248-
To call the a superclass method, use the special attribute `self.__super__`.
243+
Inheriting from a Java class (or implementing a Java interface) is supported with some syntactical and significant behavioral differences from Jython.
244+
To create a class that inherits from a Java class (or implements a Java interface), use the conventional Python `class` statement.
245+
Declared methods override (implement) superclass (interface) methods when their names match.
246+
247+
It is important to understand that there is actually delegation happening here - when inheriting from Java, two classes are created, one in Java and one in Python.
248+
These reference each other and any methods that are declared in Python that override or implement a Java method on the superclass are declared on the Java side as delegating to Python.
249249
The created object does not behave like a Python object but instead in the same way as a foreign Java object.
250-
Its Python-level members can be accessed using its `this` attribute. For example:
250+
The reason for this is that when you create an instance of your new class, you get a reference to the *Java* object.
251+
252+
To call Python methods that do *not* override or implement methods that already existed on the superclass, you need to use the special `this` attribute.
253+
Once you are in a Python method, your `self` refers to the Python object, and to get back from a Python method to Java, use the special attribute `__super__`.
254+
And since we do not expose static members on the instance side, if you need to call a static method from an instance on the Java side, use `getClass().static` to get to the meta-object holding the static members.
255+
256+
One important consequence of the two-object-schema here is that the `__init__` method on the Python object is actually called *before* the connection to the Java side is established.
257+
So you cannot currently override construction of the Java object or run code during initialization that would affect the Java half of the combined structure.
258+
You will have to create a factory method if you want to achieve this.
259+
260+
For example:
251261

252262
```python
253263
import atexit
@@ -277,6 +287,8 @@ for record in handler.this.logged:
277287
print(f'Python captured message "{record.getMessage()}" at level {record.getLevel().getName()}')
278288
```
279289

290+
For more information about how the generated Java subclass behaves, see the [Truffle documentation](https://www.graalvm.org/truffle/javadoc/com/oracle/truffle/api/TruffleLanguage.Env.html#createHostAdapter(java.lang.Object%5B%5D)).
291+
280292
## Embedding Python into Java
281293

282294
The other way to use Jython was to embed it into a Java application. There were two options for such an embedding.

graalpython/com.oracle.graal.python.shell/src/com/oracle/graal/python/shell/GraalPythonMain.java

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2017, 2024, Oracle and/or its affiliates.
2+
* Copyright (c) 2017, 2025, Oracle and/or its affiliates.
33
* Copyright (c) 2013, Regents of the University of California
44
*
55
* All rights reserved.
@@ -47,6 +47,7 @@
4747
import java.util.Set;
4848
import java.util.UUID;
4949
import java.util.function.Function;
50+
import java.util.function.Predicate;
5051

5152
import org.graalvm.launcher.AbstractLanguageLauncher;
5253
import org.graalvm.nativeimage.ImageInfo;
@@ -530,11 +531,17 @@ private String getLauncherExecName() {
530531
* {@link #getEnv(String)} is used to retrieve $PATH.
531532
* @return The absolute path to the program or {@code null}.
532533
*/
533-
private String calculateProgramFullPath(String program, Function<Path, Boolean> isExecutable, String envPath) {
534+
private String calculateProgramFullPath(String program, Predicate<Path> isExecutable, String envPath) {
534535
Path programPath = Paths.get(program);
535536

536537
// If this is an absolute path, we are already fine.
537538
if (programPath.isAbsolute()) {
539+
if (IS_WINDOWS) {
540+
String resolvedProgramNameWithExtension = getProgramNameWithExtension(isExecutable, program);
541+
if (resolvedProgramNameWithExtension != null) {
542+
return resolvedProgramNameWithExtension;
543+
}
544+
}
538545
return program;
539546
}
540547

@@ -553,26 +560,15 @@ private String calculateProgramFullPath(String program, Function<Path, Boolean>
553560
i = path.indexOf(File.pathSeparatorChar, previous);
554561
int end = i == -1 ? path.length() : i;
555562
Path resolvedProgramName = Paths.get(path.substring(previous, end)).resolve(programPath);
556-
if (isExecutable.apply(resolvedProgramName)) {
563+
if (isExecutable.test(resolvedProgramName)) {
557564
return resolvedProgramName.toString();
558565
}
559566

560567
// On windows, the program name may be without the extension
561568
if (IS_WINDOWS) {
562-
String pathExtEnvvar = getEnv("PATHEXT");
563-
if (pathExtEnvvar != null) {
564-
// default extensions are defined
565-
String resolvedStr = resolvedProgramName.toString();
566-
if (resolvedStr.length() <= 3 || resolvedStr.charAt(resolvedStr.length() - 4) != '.') {
567-
// program has no file extension
568-
String[] pathExts = pathExtEnvvar.toLowerCase().split(";");
569-
for (String pathExt : pathExts) {
570-
resolvedProgramName = Path.of(resolvedStr + pathExt);
571-
if (isExecutable.apply(resolvedProgramName)) {
572-
return resolvedProgramName.toString();
573-
}
574-
}
575-
}
569+
String resolvedProgramNameWithExtension = getProgramNameWithExtension(isExecutable, resolvedProgramName.toString());
570+
if (resolvedProgramNameWithExtension != null) {
571+
return resolvedProgramNameWithExtension;
576572
}
577573
}
578574

@@ -598,6 +594,28 @@ private String calculateProgramFullPath(String program, Function<Path, Boolean>
598594
return programPath.toAbsolutePath().toString();
599595
}
600596

597+
private static String getProgramNameWithExtension(Predicate<Path> isExecutable, String programStr) {
598+
if (isExecutable.test(Path.of(programStr))) {
599+
return programStr;
600+
}
601+
if (programStr.length() <= 3 || programStr.charAt(programStr.length() - 4) != '.') {
602+
// program has no file extension
603+
Path programNameWithExtension = null;
604+
String pathExtEnvvar = getEnv("PATHEXT");
605+
if (pathExtEnvvar != null) {
606+
// default extensions are defined
607+
String[] pathExts = pathExtEnvvar.toLowerCase().split(";");
608+
for (String pathExt : pathExts) {
609+
programNameWithExtension = Path.of(programStr + pathExt);
610+
if (isExecutable.test(programNameWithExtension)) {
611+
return programNameWithExtension.toString();
612+
}
613+
}
614+
}
615+
}
616+
return null;
617+
}
618+
601619
private String[] getExecutableList() {
602620
String launcherExecName = getLauncherExecName();
603621
if (launcherExecName != null) {

graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/shell/TestPathResolution.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -43,7 +43,7 @@
4343
import java.lang.reflect.Method;
4444
import java.nio.file.Path;
4545
import java.nio.file.Paths;
46-
import java.util.function.Function;
46+
import java.util.function.Predicate;
4747

4848
import org.junit.Assert;
4949
import org.junit.Before;
@@ -57,11 +57,11 @@ public class TestPathResolution {
5757
@Before
5858
public void setup() throws NoSuchMethodException {
5959
// Use reflection to avoid exposing the method in public API
60-
calculateProgramFullPathMethod = GraalPythonMain.class.getDeclaredMethod("calculateProgramFullPath", String.class, Function.class, String.class);
60+
calculateProgramFullPathMethod = GraalPythonMain.class.getDeclaredMethod("calculateProgramFullPath", String.class, Predicate.class, String.class);
6161
calculateProgramFullPathMethod.setAccessible(true);
6262
}
6363

64-
public String calculateProgramFullPath(String executable, Function<Path, Boolean> isExecutable, String path) {
64+
public String calculateProgramFullPath(String executable, Predicate<Path> isExecutable, String path) {
6565
try {
6666
return (String) calculateProgramFullPathMethod.invoke(new GraalPythonMain(), executable, isExecutable, path);
6767
} catch (Throwable e) {

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/ElfFile.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -60,12 +60,20 @@ private String getPatchelf() {
6060
var executable = env.getPublicTruffleFile(context.getOption(PythonOptions.Executable).toJavaStringUncached());
6161
var patchelf = executable.resolveSibling("patchelf");
6262
var i = 0;
63-
while (!patchelf.isExecutable() && i < path.length) {
63+
while (!isExecutable(patchelf) && i < path.length) {
6464
patchelf = env.getPublicTruffleFile(path[i++]).resolve("patchelf");
6565
}
6666
return patchelf.toString();
6767
}
6868

69+
private static boolean isExecutable(TruffleFile patchelf) {
70+
try {
71+
return patchelf.isExecutable();
72+
} catch (SecurityException e) {
73+
return false;
74+
}
75+
}
76+
6977
ElfFile(byte[] b, PythonContext context) throws IOException {
7078
this.context = context;
7179
this.tempfile = context.getEnv().createTempFile(null, null, ".so");

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/copying/MachODylibCommand.java

Lines changed: 0 additions & 135 deletions
This file was deleted.

0 commit comments

Comments
 (0)