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

Introduce the Hybrid ABI #371

Merged
merged 45 commits into from
Nov 24, 2022
Merged

Introduce the Hybrid ABI #371

merged 45 commits into from
Nov 24, 2022

Conversation

antocuni
Copy link
Collaborator

@antocuni antocuni commented Oct 16, 2022

This PR introduces the hybrid ABI:

  • extensions which targets universal cannot #include <Python.h>, cannot use legacy features and produce a file called e.g. simple.hpy0.so.
  • extensions which targets hybrid can use Python.h and produce a file called e.g. simply.hpy0-cpy38.so.

See the updated docs for more info.

Filename
For universal, I choose to use .hpy0.so because we should use a versioned scheme. I didn't want to use .hpy1.so for now, because we might still want to change the ABI before becoming "official"

For hybrid, we want to include both the hpy version and the cpython version. I chose hpy0-cp38.so, which is the result of many sub-choices.
To start with, let's see what is the filename that you get on "normal" extensions:

  • linux: foo.cpython-38-x86_64-linux-gnu.so.
  • linux stable ABI: foo.abi3.so
  • mac: foo.cpython-310-darwin.so (or .dylib? I think I've seen both)
  • win: foo.cp38-win_amd64.pyd
  • pypy: foo.pypy38-pp73-x86_64-linux-gnu.so
  • graalpy: foo.graalpy-38-native-x86_64-darwin.dylib

It varies wildly:

  • cpython uses both cpython-38 and cp38.
  • the platform tag sometimes includes the CPU, sometimes not
  • the stable ABI extensions doesn't include the platform tag at all

So for hpy hybrid I chose to:

  • use cp38 instead of cpython-38: this is shorter, and most importantly it's the same tag which goes in the wheel names
  • don't include the platform tag: if it's not needed for the stable ABI, it means it's not needed for us as well

Compile time errors
In universal mode, you are not allowed to use Python.h: we try hard to display a nice compile time error by putting our own Python.h in the include dirs. This might not work in all setups, but it should be good enough for most cases.

In universal mode, you cannot call HPy_AsPyObject and HPy_FromPyObject: I managed to get a compile-time error on gcc, but not on old clangs nor in MSVC. Again, too bad but it's better than nothing:

// declare the following API functions as _HPY_LEGACY, which triggers an
// #error if they are used
HPyAPI_FUNC _HPY_LEGACY cpy_PyObject *HPy_AsPyObject(HPyContext *ctx, HPy h);
HPyAPI_FUNC _HPY_LEGACY HPy HPy_FromPyObject(HPyContext *ctx, cpy_PyObject *obj);

// clang and gcc supports __has_attribute, MSVC doesn't. This should be enough
// to be able to use it portably
#ifdef __has_attribute
# define _HPY_compiler_has_attribute(x) __has_attribute(x)
#else
# define _HPY_compiler_has_attribute(x) 0
#endif
#ifdef HPY_ABI_UNIVERSAL
# if _HPY_compiler_has_attribute(error)
// gcc, clang>=14
# define _HPY_LEGACY __attribute__((error("Cannot use legacy functions when targeting the HPy Universal ABI")))
# else
// we don't have any diagnostic feature, too bad
# define _HPY_LEGACY
# endif
#else
// in non-universal modes, we don't attach any attribute
# define _HPY_LEGACY
#endif

The other legacy features that you cannot use are .legacy_methods and .legacy_slots: but these are now impossible to use, because in order to pass anything meaningful you have to instantiate e.g. a PyMethodDef struct, which you can't because you cannot include Python.h

We still need to reference some cpy_* types, e.g. to declare the HPyModule_Spec.legacy_methods field. This is done here:

/* ~~~~~~~~~~~~~~~~ CPython legacy types ~~~~~~~~~~~~~~~~ */
/* These are the types which are needed to implement legacy features such as
.legacy_slots, .legacy_methods, HPy_FromPyObject, HPy_AsPyObject, etc.
In cpython and hybrid ABI mode we can #include Python.h and use the "real"
types.
In universal ABI mode, legacy features cannot be used, but we still need
the corresponding C types to use in the HPy declarations. Note that we use
only forward declarations, meaning that it will actually be impossible to
instantiate any of these struct.
*/
#ifdef HPY_ABI_UNIVERSAL
typedef struct FORBIDDEN_cpy_PyObject cpy_PyObject;
typedef struct FORBIDDEN_PyMethodDef cpy_PyMethodDef;
typedef struct FORBIDDEN_bufferinfo cpy_Py_buffer;
// declare the following API functions as _HPY_LEGACY, which triggers an
// #error if they are used
HPyAPI_FUNC _HPY_LEGACY cpy_PyObject *HPy_AsPyObject(HPyContext *ctx, HPy h);
HPyAPI_FUNC _HPY_LEGACY HPy HPy_FromPyObject(HPyContext *ctx, cpy_PyObject *obj);
#else
// Python.h has already been included by the main hpy.h
typedef PyObject cpy_PyObject;
typedef PyMethodDef cpy_PyMethodDef;
typedef Py_buffer cpy_Py_buffer;
#endif /* HPY_ABI_UNIVERSAL */

in cpython and hybrid modes, we use the real types from Python.h. In universal mode, we use "fake" types, but note that they are only forward declarations and they cannot be instantiated.

This should also fix #288.

Future improvements
There are many things which we can to to improve the situation, but I think they can be done in future PRs:

  • post-compilation sanity check: after compiling to universal, we could run a sanity check to actually ensure that we don't link against any Py* symbol
  • runtime check: it would be nice to have two different context for universal and hybrid ABI: the universal ctx would fail cleanly if you try to call HPy_{As,From}PyObject or use .legacy_methods, etc.
  • better compile time errors on different compilers

…Instead

of having cpython-ABI by default, we require it to be set explicitly. Also
start to introduce supprt for the upcoming hybrid ABI
…distutils. Currently it's exactly the same as the universal ABI
Before this commit, we always used .hpy.so, even on windows. Now do a much
better job:

1. we use .hpy0.so instead of .hpy.so. We will migrate to .hpy1.so as soon as
   the ABI is stable

2. we use the proper extension on non-linux systems

3. for the hybrid ABI, we use a different scheme, which generates
   e.g. .hpy0-cp38.so on CPython (but with lots of different cases for all
   possible systems, see abitag.py and its test)
@antocuni antocuni changed the title WIP: introduce the Hybrid ABI Introduce the Hybrid ABI Oct 20, 2022
@antocuni antocuni marked this pull request as ready for review October 20, 2022 14:46
Copy link
Contributor

@fangerer fangerer left a comment

Choose a reason for hiding this comment

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

Great work, thank you!
I added some comments but I don't see blockers.

hpy/devel/abitag.py Show resolved Hide resolved
hpy/devel/include/hpy.h Show resolved Hide resolved
hpy/devel/include/hpy/cpy_types.h Show resolved Hide resolved
hpy/devel/include/hpy/hpymodule.h Outdated Show resolved Hide resolved
@@ -155,7 +155,7 @@ static PyObject *get_version(PyObject *self, PyObject *ignored)
}

static PyMethodDef HPyMethods[] = {
{"load", (PyCFunction)load, METH_VARARGS | METH_KEYWORDS, "Load a .hpy.so"},
{"load", (PyCFunction)load, METH_VARARGS | METH_KEYWORDS, "Load a .hpy0.so"},
Copy link
Contributor

Choose a reason for hiding this comment

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

We are also using this in hpy/devel/abitag.py. I think these two usages should have a strong connection. Can we do what we do with the version? Define it in a C header and expose it in Python.

Copy link
Contributor

Choose a reason for hiding this comment

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

One little more thing: there is an int ctx_version field in HPyContext. Matti brought to my attention, that this field basically is the ABI version, right? So, I think we should set ctx_version appropriately.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes, good point. It turns out that sharing the same definition in C and Python is a mess as usual, so I opted for defining it in two places (hpy.h and abitag.py) and add a test which checks that they are on sync.
Also, I renamed ctx->ctx_version into ctx->abi_version and set its value to HPY_ABI_TAG, see commits 71d2a7d and 1f70e65.

test/conftest.py Show resolved Hide resolved
# We need this because some of the nice compilation errors (such as the ones
# causes by _HPY_LEGACY) are triggered only by gcc. Would be nice to have them
# also for other compilers
ONLY_GCC = pytest.mark.skipif(sys.platform!='linux', reason='GCC only')
Copy link
Contributor

Choose a reason for hiding this comment

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

Should that maybe go into support.py ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed by 4907ee8

@steve-s
Copy link
Contributor

steve-s commented Nov 3, 2022

If we include the ABI version in the tag, I am wondering if we need #356. At the same time I like the approach in #356, because it will be "fool-proof" -- even if you have wrong tag for whatever reason it'll still check and choose the right ABI for your extension. So right now I am in favor of keeping both the ABI tag and #356 even though they are redundant.

@fangerer
Copy link
Contributor

fangerer commented Nov 3, 2022

@steve-s You are right, I forgot about that effort. Would it make sense to introduce some generic ABI tag?

@steve-s
Copy link
Contributor

steve-s commented Nov 3, 2022

Would it make sense to introduce some generic ABI tag?

What exactly do you mean by that? That we'd drop hpyX tag as suggested in this PR and use just generic hpy? I don't know. I am not opposed to having both the tag with a major ABI version (in the sense of #356) and the mechanism introduced in #356.

@fangerer
Copy link
Contributor

fangerer commented Nov 3, 2022

What exactly do you mean by that? That we'd drop hpyX tag as suggested in this PR and use just generic hpy?

Yes, that's what I meant.

I do also think that a generic ABI tag does not make a lot of sense. I agree that we can have both.

@mattip
Copy link
Contributor

mattip commented Nov 10, 2022

Is there more to do here or should we merge this as-is and incrementally improve things toward a release?

@mattip
Copy link
Contributor

mattip commented Nov 10, 2022

Closes #366

@steve-s
Copy link
Contributor

steve-s commented Nov 10, 2022

I also had a cursory look over the changes and I vote for merging it and incrementally improve things toward a release

@mattip
Copy link
Contributor

mattip commented Nov 16, 2022

@antocuni would you like to make one last pass over the comments and clear the merge conflict?

@mattip
Copy link
Contributor

mattip commented Nov 23, 2022

after the capsule changes, this PR has conflicts

@antocuni
Copy link
Collaborator Author

If we include the ABI version in the tag, I am wondering if we need #356. At the same time I like the approach in #356, because it will be "fool-proof" -- even if you have wrong tag for whatever reason it'll still check and choose the right ABI for your extension. So right now I am in favor of keeping both the ABI tag and #356 even though they are redundant.

if think we can have both: basically, the logic of this PR tries to ensure that you cannot use CPython symbols by mistake, and #356 would become a sanity check to ensure that it works well.
Not sure if it's high-priority, but it surely doesn't hurt.

@antocuni
Copy link
Collaborator Author

@mattip @fangerer @steve-s I think I addressed all the comments and fixed the conflicts.
If you don't have anything against, I'll merge the PR as soon as the tests are green

@fangerer
Copy link
Contributor

LGTM. Please go ahead and merge.
That's great work. Thank you!

@mattip
Copy link
Contributor

mattip commented Nov 24, 2022

Yay!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Unable to compile on Python 3.11 alpha 5
4 participants