Skip to content
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

Basic GraalPy Support #3247

Merged
merged 25 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
449c27b
graalpy: recognize graalpy implementation when building
timfel Mar 16, 2023
9c35db2
graalpy: global Ellipse, None, NotImplemented, True, and False are on…
timfel Jun 8, 2023
2fe0059
graalpy: PyObject struct is opaque, use functions for everything
timfel Jun 8, 2023
7cca2e3
graalpy: missing many of the same functions as pypy
timfel Jun 8, 2023
e1f8df1
graalpy: do not have 128bit conversion functions
timfel Jun 8, 2023
7acf947
graalpy: add functions for datetime accessor macros
timfel Jun 7, 2023
007d4de
graalpy: add implementations for list macro functions
timfel Jun 7, 2023
ab05a9d
graalpy: skip tuple macros
timfel Jun 7, 2023
04337a8
graalpy: always use extern Py_CompileString function
timfel Jun 9, 2023
47d0abf
graalpy: disable assertion that does not apply to graalpy
timfel Jun 9, 2023
a0d5c9c
graalpy: floatobject structure is opaque on graalpy
timfel Sep 15, 2023
ce6e84a
graalpy: ignore gc dependent test
timfel Sep 15, 2023
6c68cd4
graalpy: add CI config
timfel Oct 11, 2023
e42f1a4
graalpy: run rust fmt
timfel Oct 12, 2023
21da565
graalpy: add changelog entry
timfel Oct 12, 2023
ed84116
graalpy: discover interpreter on PATH
timfel Oct 12, 2023
86617f0
graalpy: interpreter id is not applicable to graalpy (just like pypy)
timfel Oct 12, 2023
26a0521
graalpy: skip tests that cannot work on GraalPy
timfel Oct 12, 2023
1584ec3
graalpy: fix constructing normalized Err instances
timfel Oct 13, 2023
e56e4d3
graalpy: correct capi library name, but skip rust tests due to missin…
timfel Oct 13, 2023
04619be
graalpy: no support for C extensions on windows in latest release
timfel Oct 13, 2023
d6b2fe8
graalpy: declare support versions
timfel Mar 18, 2024
1f505e1
graalpy: frame, code, method, and function objects access from C API …
timfel Mar 22, 2024
366a3b2
graalpy: take care only to expose C structure that GraalPy allocates
timfel Mar 22, 2024
c5ae4c7
graalpy: Bail out if graalpy version is less than what we support
timfel Mar 25, 2024
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
15 changes: 9 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
build:
continue-on-error: ${{ endsWith(inputs.python-version, '-dev') || contains(fromJSON('["3.7", "pypy3.7"]'), inputs.python-version) || inputs.rust == 'beta' || inputs.rust == 'nightly' }}
runs-on: ${{ inputs.os }}
if: ${{ !(startsWith(inputs.python-version, 'graalpy') && startsWith(inputs.os, 'windows')) }}
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Windows support now available, or is this left for a follow-up?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is available, but I would leave it as a follow up since this is our first release with C API support on Windows and I haven't managed to set up a dev environment with PyO3 on Windows so far :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep makes sense 👍

steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -65,6 +66,7 @@ jobs:
run: nox -s docs

- name: Build (no features)
if: ${{ !startsWith(inputs.python-version, 'graalpy') }}
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
run: cargo build --lib --tests --no-default-features

# --no-default-features when used with `cargo build/test -p` doesn't seem to work!
Expand All @@ -74,7 +76,7 @@ jobs:
cargo build --no-default-features

# Run tests (except on PyPy, because no embedding API).
- if: ${{ !startsWith(inputs.python-version, 'pypy') }}
- if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }}
name: Test (no features)
run: cargo test --no-default-features --lib --tests

Expand All @@ -85,24 +87,25 @@ jobs:
cargo test --no-default-features

- name: Build (all additive features)
if: ${{ !startsWith(inputs.python-version, 'graalpy') }}
run: cargo build --lib --tests --no-default-features --features "full ${{ inputs.extra-features }}"

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

# Run tests (except on PyPy, because no embedding API).
- if: ${{ !startsWith(inputs.python-version, 'pypy') }}
- if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }}
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
name: Test
run: cargo test --no-default-features --features "full ${{ inputs.extra-features }}"

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

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

Expand All @@ -120,7 +123,7 @@ jobs:

- uses: dorny/paths-filter@v3
# pypy 3.7 and 3.8 are not PEP 3123 compliant so fail checks here
if: ${{ inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' }}
if: ${{ inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !startsWith(inputs.python-version, 'graalpy') }}
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
id: ffi-changes
with:
base: ${{ github.event.pull_request.base.ref || github.event.merge_group.base_ref }}
Expand All @@ -135,7 +138,7 @@ jobs:
- name: Run pyo3-ffi-check
# pypy 3.7 and 3.8 are not PEP 3123 compliant so fail checks here, nor
# is pypy 3.9 on windows
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'))) }}
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'))) }}
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved
run: nox -s ffi-check

env:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ jobs:
"pypy3.8",
"pypy3.9",
"pypy3.10",
"graalpy24.0",
]
platform:
[
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
## Usage

PyO3 supports the following software versions:
- Python 3.7 and up (CPython and PyPy)
- Python 3.7 and up (CPython, PyPy, and GraalPy)
timfel marked this conversation as resolved.
Show resolved Hide resolved
- Rust 1.56 and up

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.
Expand Down
1 change: 1 addition & 0 deletions newsfragments/3247.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for running PyO3 extensions on GraalPy.
69 changes: 63 additions & 6 deletions pyo3-build-config/src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ use crate::{
/// Minimum Python version PyO3 supports.
const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 };

/// GraalPy may implement the same CPython version over multiple releases.
const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion {
major: 24,
minor: 0,
};

/// Maximum Python version that can be used as minimum required Python version with abi3.
const ABI3_MAX_MINOR: u8 = 12;

Expand Down Expand Up @@ -173,6 +179,11 @@ impl InterpreterConfig {
See https://foss.heptapod.net/pypy/pypy/-/issues/3397 for more information."
));
}
} else if self.implementation.is_graalpy() {
println!("cargo:rustc-cfg=GraalPy");
if self.abi3 {
warn!("GraalPy does not support abi3 so the build artifacts will be version-specific.");
timfel marked this conversation as resolved.
Show resolved Hide resolved
}
} else if self.abi3 {
out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned());
}
Expand All @@ -197,6 +208,12 @@ import sys
from sysconfig import get_config_var, get_platform

PYPY = platform.python_implementation() == "PyPy"
GRAALPY = platform.python_implementation() == "GraalVM"

if GRAALPY:
graalpy_ver = map(int, __graalpython__.get_graalvm_version().split('.'));
print("graalpy_major", next(graalpy_ver))
print("graalpy_minor", next(graalpy_ver))

# sys.base_prefix is missing on Python versions older than 3.3; this allows the script to continue
# so that the version mismatch can be reported in a nicer way later.
Expand Down Expand Up @@ -226,7 +243,7 @@ SHARED = bool(get_config_var("Py_ENABLE_SHARED"))
print("implementation", platform.python_implementation())
print("version_major", sys.version_info[0])
print("version_minor", sys.version_info[1])
print("shared", PYPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED)
print("shared", PYPY or GRAALPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, should we be checking that the version is 24.0 or above, and bailing out if that's not the case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good idea. Like this? bca1c5a

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks like a suitable implementation to me 👍

print_if_set("ld_version", get_config_var("LDVERSION"))
print_if_set("libdir", get_config_var("LIBDIR"))
print_if_set("base_prefix", base_prefix)
Expand All @@ -244,6 +261,23 @@ print("ext_suffix", get_config_var("EXT_SUFFIX"))
interpreter.as_ref().display()
);

if let Some(value) = map.get("graalpy_major") {
let graalpy_version = PythonVersion {
major: value
.parse()
.context("failed to parse GraalPy major version")?,
minor: map["graalpy_minor"]
.parse()
.context("failed to parse GraalPy minor version")?,
};
ensure!(
graalpy_version >= MINIMUM_SUPPORTED_VERSION_GRAALPY,
"At least GraalPy version {} needed, got {}",
MINIMUM_SUPPORTED_VERSION_GRAALPY,
graalpy_version
);
};

let shared = map["shared"].as_str() == "True";

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

Expand Down Expand Up @@ -647,6 +681,7 @@ impl FromStr for PythonVersion {
pub enum PythonImplementation {
CPython,
PyPy,
GraalPy,
}

impl PythonImplementation {
Expand All @@ -655,12 +690,19 @@ impl PythonImplementation {
self == PythonImplementation::PyPy
}

#[doc(hidden)]
pub fn is_graalpy(self) -> bool {
self == PythonImplementation::GraalPy
}

#[doc(hidden)]
pub fn from_soabi(soabi: &str) -> Result<Self> {
if soabi.starts_with("pypy") {
Ok(PythonImplementation::PyPy)
} else if soabi.starts_with("cpython") {
Ok(PythonImplementation::CPython)
} else if soabi.starts_with("graalpy") {
Ok(PythonImplementation::GraalPy)
} else {
bail!("unsupported Python interpreter");
}
Expand All @@ -672,6 +714,7 @@ impl Display for PythonImplementation {
match self {
PythonImplementation::CPython => write!(f, "CPython"),
PythonImplementation::PyPy => write!(f, "PyPy"),
PythonImplementation::GraalPy => write!(f, "GraalVM"),
timfel marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand All @@ -682,6 +725,7 @@ impl FromStr for PythonImplementation {
match s {
"CPython" => Ok(PythonImplementation::CPython),
"PyPy" => Ok(PythonImplementation::PyPy),
"GraalVM" => Ok(PythonImplementation::GraalPy),
_ => bail!("unknown interpreter: {}", s),
}
}
Expand Down Expand Up @@ -760,7 +804,7 @@ pub struct CrossCompileConfig {
/// The version of the Python library to link against.
version: Option<PythonVersion>,

/// The target Python implementation hint (CPython or PyPy)
/// The target Python implementation hint (CPython, PyPy, GraalPy, ...)
implementation: Option<PythonImplementation>,

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

fn is_graalpy_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool {
let graalpy_version_pat = if let Some(v) = v {
format!("graalpy{}", v)
} else {
"graalpy2".into()
};
path == "lib_graalpython" || path.starts_with(&graalpy_version_pat)
}

fn is_cpython_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool {
let cpython_version_pat = if let Some(v) = v {
format!("python{}", v)
Expand Down Expand Up @@ -1297,6 +1350,7 @@ fn search_lib_dir(path: impl AsRef<Path>, cross: &CrossCompileConfig) -> Vec<Pat
search_lib_dir(f.path(), cross)
} else if is_cpython_lib_dir(&file_name, &cross.version)
|| is_pypy_lib_dir(&file_name, &cross.version)
|| is_graalpy_lib_dir(&file_name, &cross.version)
{
search_lib_dir(f.path(), cross)
} else {
Expand Down Expand Up @@ -1418,7 +1472,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result<In
///
/// Must be called from a PyO3 crate build script.
fn default_abi3_config(host: &Triple, version: PythonVersion) -> InterpreterConfig {
// FIXME: PyPy does not support the Stable ABI yet.
// FIXME: PyPy & GraalPy do not support the Stable ABI.
let implementation = PythonImplementation::CPython;
let abi3 = true;

Expand Down Expand Up @@ -1524,7 +1578,7 @@ fn default_lib_name_windows(
// CPython bug: linking against python3_d.dll raises error
// https://github.com/python/cpython/issues/101614
format!("python{}{}_d", version.major, version.minor)
} else if abi3 && !implementation.is_pypy() {
} else if abi3 && !(implementation.is_pypy() || implementation.is_graalpy()) {
WINDOWS_ABI3_LIB_NAME.to_owned()
} else if mingw {
// https://packages.msys2.org/base/mingw-w64-python
Expand Down Expand Up @@ -1562,6 +1616,7 @@ fn default_lib_name_unix(
format!("pypy{}-c", version.major)
}
}
PythonImplementation::GraalPy => "python-native".to_string(),
}
}

Expand Down Expand Up @@ -1662,7 +1717,9 @@ pub fn find_interpreter() -> Result<PathBuf> {
.find(|bin| {
if let Ok(out) = Command::new(bin).arg("--version").output() {
// begin with `Python 3.X.X :: additional info`
out.stdout.starts_with(b"Python 3") || out.stderr.starts_with(b"Python 3")
out.stdout.starts_with(b"Python 3")
|| out.stderr.starts_with(b"Python 3")
|| out.stdout.starts_with(b"GraalPy 3")
} else {
false
}
Expand Down
5 changes: 4 additions & 1 deletion pyo3-build-config/src/import_lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use python3_dll_a::ImportLibraryGenerator;
use target_lexicon::{Architecture, OperatingSystem, Triple};

use super::{PythonImplementation, PythonVersion};
use crate::errors::{Context, Result};
use crate::errors::{Context, Error, Result};

/// Generates the `python3.dll` or `pythonXY.dll` import library for Windows targets.
///
Expand Down Expand Up @@ -42,6 +42,9 @@ pub(super) fn generate_import_lib(
let implementation = match py_impl {
PythonImplementation::CPython => python3_dll_a::PythonImplementation::CPython,
PythonImplementation::PyPy => python3_dll_a::PythonImplementation::PyPy,
PythonImplementation::GraalPy => {
return Err(Error::from("No support for GraalPy on Windows"))
}
};

ImportLibraryGenerator::new(&arch, &env)
Expand Down
1 change: 1 addition & 0 deletions pyo3-build-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use target_lexicon::OperatingSystem;
/// | `#[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**. |
/// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` feature enabled. |
/// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. |
/// | `#[cfg(GraalPy)]` | This marks code which is run when compiling for GraalPy. |
///
/// For examples of how to use these attributes, [see PyO3's guide](https://pyo3.rs/latest/building-and-distribution/multiple_python_versions.html).
#[cfg(feature = "resolve-config")]
Expand Down
29 changes: 29 additions & 0 deletions pyo3-ffi/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ const SUPPORTED_VERSIONS_PYPY: SupportedVersions = SupportedVersions {
},
};

const SUPPORTED_VERSIONS_GRAALPY: SupportedVersions = SupportedVersions {
min: PythonVersion {
major: 3,
minor: 10,
},
max: PythonVersion {
major: 3,
minor: 11,
},
};

fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> {
// This is an undocumented env var which is only really intended to be used in CI / for testing
// and development.
Expand Down Expand Up @@ -73,6 +84,24 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> {
std::env::var("CARGO_PKG_VERSION").unwrap()
);
}
PythonImplementation::GraalPy => {
let versions = SUPPORTED_VERSIONS_GRAALPY;
ensure!(
interpreter_config.version >= versions.min,
"the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})",
interpreter_config.version,
versions.min,
);
// GraalPy does not support abi3, so we cannot offer forward compatibility
ensure!(
interpreter_config.version <= versions.max,
"the configured GraalPy interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\
= help: please check if an updated version of PyO3 is available. Current version: {}",
interpreter_config.version,
versions.max,
std::env::var("CARGO_PKG_VERSION").unwrap()
);
}
}

Ok(())
Expand Down
1 change: 1 addition & 0 deletions pyo3-ffi/src/abstract_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub unsafe fn PyObject_DelAttr(o: *mut PyObject, attr_name: *mut PyObject) -> c_
extern "C" {
#[cfg(all(
not(PyPy),
not(GraalPy),
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
))]
#[cfg_attr(PyPy, link_name = "PyPyObject_CallNoArgs")]
Expand Down
Loading
Loading