Skip to content

Commit 9a38e70

Browse files
timfeldavidhewitt
andauthored
Basic GraalPy Support (#3247)
* graalpy: recognize graalpy implementation when building * graalpy: global Ellipse, None, NotImplemented, True, and False are only available as pointers * graalpy: PyObject struct is opaque, use functions for everything * graalpy: missing many of the same functions as pypy * graalpy: do not have 128bit conversion functions * graalpy: add functions for datetime accessor macros * graalpy: add implementations for list macro functions * graalpy: skip tuple macros * graalpy: always use extern Py_CompileString function * graalpy: disable assertion that does not apply to graalpy * graalpy: floatobject structure is opaque on graalpy * graalpy: ignore gc dependent test * graalpy: add CI config * graalpy: run rust fmt * graalpy: add changelog entry * graalpy: discover interpreter on PATH * graalpy: interpreter id is not applicable to graalpy (just like pypy) * graalpy: skip tests that cannot work on GraalPy * graalpy: fix constructing normalized Err instances Co-authored-by: David Hewitt <[email protected]> * graalpy: correct capi library name, but skip rust tests due to missing symbols * graalpy: no support for C extensions on windows in latest release * graalpy: declare support versions * graalpy: frame, code, method, and function objects access from C API is mostly missing * graalpy: take care only to expose C structure that GraalPy allocates * graalpy: Bail out if graalpy version is less than what we support --------- Co-authored-by: David Hewitt <[email protected]>
1 parent 54ffaec commit 9a38e70

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+731
-308
lines changed

.github/workflows/build.yml

+9-6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
build:
2525
continue-on-error: ${{ endsWith(inputs.python-version, '-dev') || contains(fromJSON('["3.7", "pypy3.7"]'), inputs.python-version) || inputs.rust == 'beta' || inputs.rust == 'nightly' }}
2626
runs-on: ${{ inputs.os }}
27+
if: ${{ !(startsWith(inputs.python-version, 'graalpy') && startsWith(inputs.os, 'windows')) }}
2728
steps:
2829
- uses: actions/checkout@v4
2930

@@ -65,6 +66,7 @@ jobs:
6566
run: nox -s docs
6667

6768
- name: Build (no features)
69+
if: ${{ !startsWith(inputs.python-version, 'graalpy') }}
6870
run: cargo build --lib --tests --no-default-features
6971

7072
# --no-default-features when used with `cargo build/test -p` doesn't seem to work!
@@ -74,7 +76,7 @@ jobs:
7476
cargo build --no-default-features
7577
7678
# Run tests (except on PyPy, because no embedding API).
77-
- if: ${{ !startsWith(inputs.python-version, 'pypy') }}
79+
- if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }}
7880
name: Test (no features)
7981
run: cargo test --no-default-features --lib --tests
8082

@@ -85,24 +87,25 @@ jobs:
8587
cargo test --no-default-features
8688
8789
- name: Build (all additive features)
90+
if: ${{ !startsWith(inputs.python-version, 'graalpy') }}
8891
run: cargo build --lib --tests --no-default-features --features "full ${{ inputs.extra-features }}"
8992

9093
- if: ${{ startsWith(inputs.python-version, 'pypy') }}
9194
name: Build PyPy (abi3-py37)
9295
run: cargo build --lib --tests --no-default-features --features "abi3-py37 full ${{ inputs.extra-features }}"
9396

9497
# Run tests (except on PyPy, because no embedding API).
95-
- if: ${{ !startsWith(inputs.python-version, 'pypy') }}
98+
- if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }}
9699
name: Test
97100
run: cargo test --no-default-features --features "full ${{ inputs.extra-features }}"
98101

99102
# Run tests again, but in abi3 mode
100-
- if: ${{ !startsWith(inputs.python-version, 'pypy') }}
103+
- if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }}
101104
name: Test (abi3)
102105
run: cargo test --no-default-features --features "abi3 full ${{ inputs.extra-features }}"
103106

104107
# Run tests again, for abi3-py37 (the minimal Python version)
105-
- if: ${{ (!startsWith(inputs.python-version, 'pypy')) && (inputs.python-version != '3.7') }}
108+
- if: ${{ (!startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy')) && (inputs.python-version != '3.7') }}
106109
name: Test (abi3-py37)
107110
run: cargo test --no-default-features --features "abi3-py37 full ${{ inputs.extra-features }}"
108111

@@ -120,7 +123,7 @@ jobs:
120123

121124
- uses: dorny/paths-filter@v3
122125
# pypy 3.7 and 3.8 are not PEP 3123 compliant so fail checks here
123-
if: ${{ inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' }}
126+
if: ${{ inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !startsWith(inputs.python-version, 'graalpy') }}
124127
id: ffi-changes
125128
with:
126129
base: ${{ github.event.pull_request.base.ref || github.event.merge_group.base_ref }}
@@ -135,7 +138,7 @@ jobs:
135138
- name: Run pyo3-ffi-check
136139
# pypy 3.7 and 3.8 are not PEP 3123 compliant so fail checks here, nor
137140
# is pypy 3.9 on windows
138-
if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }}
141+
if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !startsWith(inputs.python-version, 'graalpy') && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }}
139142
run: nox -s ffi-check
140143

141144
env:

.github/workflows/ci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ jobs:
228228
"pypy3.8",
229229
"pypy3.9",
230230
"pypy3.10",
231+
"graalpy24.0",
231232
]
232233
platform:
233234
[

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
## Usage
1818

1919
PyO3 supports the following software versions:
20-
- Python 3.7 and up (CPython and PyPy)
20+
- Python 3.7 and up (CPython, PyPy, and GraalPy)
2121
- Rust 1.56 and up
2222

2323
You can use PyO3 to write a native Python module in Rust, or to embed Python in a Rust binary. The following sections explain each of these in turn.

newsfragments/3247.added.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added support for running PyO3 extensions on GraalPy.

pyo3-build-config/src/impl_.rs

+63-6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ use crate::{
3232
/// Minimum Python version PyO3 supports.
3333
const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 };
3434

35+
/// GraalPy may implement the same CPython version over multiple releases.
36+
const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion {
37+
major: 24,
38+
minor: 0,
39+
};
40+
3541
/// Maximum Python version that can be used as minimum required Python version with abi3.
3642
const ABI3_MAX_MINOR: u8 = 12;
3743

@@ -173,6 +179,11 @@ impl InterpreterConfig {
173179
See https://foss.heptapod.net/pypy/pypy/-/issues/3397 for more information."
174180
));
175181
}
182+
} else if self.implementation.is_graalpy() {
183+
println!("cargo:rustc-cfg=GraalPy");
184+
if self.abi3 {
185+
warn!("GraalPy does not support abi3 so the build artifacts will be version-specific.");
186+
}
176187
} else if self.abi3 {
177188
out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned());
178189
}
@@ -197,6 +208,12 @@ import sys
197208
from sysconfig import get_config_var, get_platform
198209
199210
PYPY = platform.python_implementation() == "PyPy"
211+
GRAALPY = platform.python_implementation() == "GraalVM"
212+
213+
if GRAALPY:
214+
graalpy_ver = map(int, __graalpython__.get_graalvm_version().split('.'));
215+
print("graalpy_major", next(graalpy_ver))
216+
print("graalpy_minor", next(graalpy_ver))
200217
201218
# sys.base_prefix is missing on Python versions older than 3.3; this allows the script to continue
202219
# so that the version mismatch can be reported in a nicer way later.
@@ -226,7 +243,7 @@ SHARED = bool(get_config_var("Py_ENABLE_SHARED"))
226243
print("implementation", platform.python_implementation())
227244
print("version_major", sys.version_info[0])
228245
print("version_minor", sys.version_info[1])
229-
print("shared", PYPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED)
246+
print("shared", PYPY or GRAALPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED)
230247
print_if_set("ld_version", get_config_var("LDVERSION"))
231248
print_if_set("libdir", get_config_var("LIBDIR"))
232249
print_if_set("base_prefix", base_prefix)
@@ -244,6 +261,23 @@ print("ext_suffix", get_config_var("EXT_SUFFIX"))
244261
interpreter.as_ref().display()
245262
);
246263

264+
if let Some(value) = map.get("graalpy_major") {
265+
let graalpy_version = PythonVersion {
266+
major: value
267+
.parse()
268+
.context("failed to parse GraalPy major version")?,
269+
minor: map["graalpy_minor"]
270+
.parse()
271+
.context("failed to parse GraalPy minor version")?,
272+
};
273+
ensure!(
274+
graalpy_version >= MINIMUM_SUPPORTED_VERSION_GRAALPY,
275+
"At least GraalPy version {} needed, got {}",
276+
MINIMUM_SUPPORTED_VERSION_GRAALPY,
277+
graalpy_version
278+
);
279+
};
280+
247281
let shared = map["shared"].as_str() == "True";
248282

249283
let version = PythonVersion {
@@ -588,7 +622,7 @@ print("ext_suffix", get_config_var("EXT_SUFFIX"))
588622
/// Lowers the configured version to the abi3 version, if set.
589623
fn fixup_for_abi3_version(&mut self, abi3_version: Option<PythonVersion>) -> Result<()> {
590624
// PyPy doesn't support abi3; don't adjust the version
591-
if self.implementation.is_pypy() {
625+
if self.implementation.is_pypy() || self.implementation.is_graalpy() {
592626
return Ok(());
593627
}
594628

@@ -647,6 +681,7 @@ impl FromStr for PythonVersion {
647681
pub enum PythonImplementation {
648682
CPython,
649683
PyPy,
684+
GraalPy,
650685
}
651686

652687
impl PythonImplementation {
@@ -655,12 +690,19 @@ impl PythonImplementation {
655690
self == PythonImplementation::PyPy
656691
}
657692

693+
#[doc(hidden)]
694+
pub fn is_graalpy(self) -> bool {
695+
self == PythonImplementation::GraalPy
696+
}
697+
658698
#[doc(hidden)]
659699
pub fn from_soabi(soabi: &str) -> Result<Self> {
660700
if soabi.starts_with("pypy") {
661701
Ok(PythonImplementation::PyPy)
662702
} else if soabi.starts_with("cpython") {
663703
Ok(PythonImplementation::CPython)
704+
} else if soabi.starts_with("graalpy") {
705+
Ok(PythonImplementation::GraalPy)
664706
} else {
665707
bail!("unsupported Python interpreter");
666708
}
@@ -672,6 +714,7 @@ impl Display for PythonImplementation {
672714
match self {
673715
PythonImplementation::CPython => write!(f, "CPython"),
674716
PythonImplementation::PyPy => write!(f, "PyPy"),
717+
PythonImplementation::GraalPy => write!(f, "GraalVM"),
675718
}
676719
}
677720
}
@@ -682,6 +725,7 @@ impl FromStr for PythonImplementation {
682725
match s {
683726
"CPython" => Ok(PythonImplementation::CPython),
684727
"PyPy" => Ok(PythonImplementation::PyPy),
728+
"GraalVM" => Ok(PythonImplementation::GraalPy),
685729
_ => bail!("unknown interpreter: {}", s),
686730
}
687731
}
@@ -760,7 +804,7 @@ pub struct CrossCompileConfig {
760804
/// The version of the Python library to link against.
761805
version: Option<PythonVersion>,
762806

763-
/// The target Python implementation hint (CPython or PyPy)
807+
/// The target Python implementation hint (CPython, PyPy, GraalPy, ...)
764808
implementation: Option<PythonImplementation>,
765809

766810
/// The compile target triple (e.g. aarch64-unknown-linux-gnu)
@@ -1264,6 +1308,15 @@ fn is_pypy_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool {
12641308
path == "lib_pypy" || path.starts_with(&pypy_version_pat)
12651309
}
12661310

1311+
fn is_graalpy_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool {
1312+
let graalpy_version_pat = if let Some(v) = v {
1313+
format!("graalpy{}", v)
1314+
} else {
1315+
"graalpy2".into()
1316+
};
1317+
path == "lib_graalpython" || path.starts_with(&graalpy_version_pat)
1318+
}
1319+
12671320
fn is_cpython_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool {
12681321
let cpython_version_pat = if let Some(v) = v {
12691322
format!("python{}", v)
@@ -1297,6 +1350,7 @@ fn search_lib_dir(path: impl AsRef<Path>, cross: &CrossCompileConfig) -> Vec<Pat
12971350
search_lib_dir(f.path(), cross)
12981351
} else if is_cpython_lib_dir(&file_name, &cross.version)
12991352
|| is_pypy_lib_dir(&file_name, &cross.version)
1353+
|| is_graalpy_lib_dir(&file_name, &cross.version)
13001354
{
13011355
search_lib_dir(f.path(), cross)
13021356
} else {
@@ -1418,7 +1472,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result<In
14181472
///
14191473
/// Must be called from a PyO3 crate build script.
14201474
fn default_abi3_config(host: &Triple, version: PythonVersion) -> InterpreterConfig {
1421-
// FIXME: PyPy does not support the Stable ABI yet.
1475+
// FIXME: PyPy & GraalPy do not support the Stable ABI.
14221476
let implementation = PythonImplementation::CPython;
14231477
let abi3 = true;
14241478

@@ -1524,7 +1578,7 @@ fn default_lib_name_windows(
15241578
// CPython bug: linking against python3_d.dll raises error
15251579
// https://github.com/python/cpython/issues/101614
15261580
format!("python{}{}_d", version.major, version.minor)
1527-
} else if abi3 && !implementation.is_pypy() {
1581+
} else if abi3 && !(implementation.is_pypy() || implementation.is_graalpy()) {
15281582
WINDOWS_ABI3_LIB_NAME.to_owned()
15291583
} else if mingw {
15301584
// https://packages.msys2.org/base/mingw-w64-python
@@ -1562,6 +1616,7 @@ fn default_lib_name_unix(
15621616
format!("pypy{}-c", version.major)
15631617
}
15641618
}
1619+
PythonImplementation::GraalPy => "python-native".to_string(),
15651620
}
15661621
}
15671622

@@ -1662,7 +1717,9 @@ pub fn find_interpreter() -> Result<PathBuf> {
16621717
.find(|bin| {
16631718
if let Ok(out) = Command::new(bin).arg("--version").output() {
16641719
// begin with `Python 3.X.X :: additional info`
1665-
out.stdout.starts_with(b"Python 3") || out.stderr.starts_with(b"Python 3")
1720+
out.stdout.starts_with(b"Python 3")
1721+
|| out.stderr.starts_with(b"Python 3")
1722+
|| out.stdout.starts_with(b"GraalPy 3")
16661723
} else {
16671724
false
16681725
}

pyo3-build-config/src/import_lib.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use python3_dll_a::ImportLibraryGenerator;
77
use target_lexicon::{Architecture, OperatingSystem, Triple};
88

99
use super::{PythonImplementation, PythonVersion};
10-
use crate::errors::{Context, Result};
10+
use crate::errors::{Context, Error, Result};
1111

1212
/// Generates the `python3.dll` or `pythonXY.dll` import library for Windows targets.
1313
///
@@ -42,6 +42,9 @@ pub(super) fn generate_import_lib(
4242
let implementation = match py_impl {
4343
PythonImplementation::CPython => python3_dll_a::PythonImplementation::CPython,
4444
PythonImplementation::PyPy => python3_dll_a::PythonImplementation::PyPy,
45+
PythonImplementation::GraalPy => {
46+
return Err(Error::from("No support for GraalPy on Windows"))
47+
}
4548
};
4649

4750
ImportLibraryGenerator::new(&arch, &env)

pyo3-build-config/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use target_lexicon::OperatingSystem;
3838
/// | `#[cfg(Py_3_7)]`, `#[cfg(Py_3_8)]`, `#[cfg(Py_3_9)]`, `#[cfg(Py_3_10)]` | These attributes mark code only for a given Python version and up. For example, `#[cfg(Py_3_7)]` marks code which can run on Python 3.7 **and newer**. |
3939
/// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` feature enabled. |
4040
/// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. |
41+
/// | `#[cfg(GraalPy)]` | This marks code which is run when compiling for GraalPy. |
4142
///
4243
/// For examples of how to use these attributes, [see PyO3's guide](https://pyo3.rs/latest/building-and-distribution/multiple_python_versions.html).
4344
#[cfg(feature = "resolve-config")]

pyo3-ffi/build.rs

+29
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ const SUPPORTED_VERSIONS_PYPY: SupportedVersions = SupportedVersions {
2929
},
3030
};
3131

32+
const SUPPORTED_VERSIONS_GRAALPY: SupportedVersions = SupportedVersions {
33+
min: PythonVersion {
34+
major: 3,
35+
minor: 10,
36+
},
37+
max: PythonVersion {
38+
major: 3,
39+
minor: 11,
40+
},
41+
};
42+
3243
fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> {
3344
// This is an undocumented env var which is only really intended to be used in CI / for testing
3445
// and development.
@@ -73,6 +84,24 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> {
7384
std::env::var("CARGO_PKG_VERSION").unwrap()
7485
);
7586
}
87+
PythonImplementation::GraalPy => {
88+
let versions = SUPPORTED_VERSIONS_GRAALPY;
89+
ensure!(
90+
interpreter_config.version >= versions.min,
91+
"the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})",
92+
interpreter_config.version,
93+
versions.min,
94+
);
95+
// GraalPy does not support abi3, so we cannot offer forward compatibility
96+
ensure!(
97+
interpreter_config.version <= versions.max,
98+
"the configured GraalPy interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\
99+
= help: please check if an updated version of PyO3 is available. Current version: {}",
100+
interpreter_config.version,
101+
versions.max,
102+
std::env::var("CARGO_PKG_VERSION").unwrap()
103+
);
104+
}
76105
}
77106

78107
Ok(())

pyo3-ffi/src/abstract_.rs

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub unsafe fn PyObject_DelAttr(o: *mut PyObject, attr_name: *mut PyObject) -> c_
2323
extern "C" {
2424
#[cfg(all(
2525
not(PyPy),
26+
not(GraalPy),
2627
any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // Added to python in 3.9 but to limited API in 3.10
2728
))]
2829
#[cfg_attr(PyPy, link_name = "PyPyObject_CallNoArgs")]

0 commit comments

Comments
 (0)