Adventures in compatibility: emulating CPython's C API in PyPy
CC Attribution - NonCommercial - ShareAlike 3.0 Unported:
You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal and non-commercial purpose as long as the work is attributed to the author in the manner specified by the author or licensor and the work or content is shared also in adapted form only under the conditions of this
Lamy, Ronan
PyPy is a fast and compliant implementation of Python. In other words, it's an interpreter for the Python language that can act as a full replacement for the reference interpreter, CPython. It's optimised to enable efficient just-in-time compilation of Python code to machine code, and has releases matching versions 2.7, 3.5 and soon(ish) 3.6. The PyPy project also developed cffi as a clean and efficient way of interfacing with C code. However, many libraries in the Python ecosystem are implemented as C extensions, which target CPython's C API. Many others use Cython, which builds C extensions under the hood. Therefore, PyPy needs an emulation layer for the C API. This emulation needs to bridge the differences between the implementation languages and the object models of CPython and PyPy. The solution is called cpyext. It's implemented in a mixture of RPython and C, with most of the API functions and macros implemented in RPython. cpyext exposes PyObjects to the extension code that appear similar to CPython objects (as long as extension writers stay within the fuzzily defined boundaries of the public API) but are merely 'shadows' of the real PyPy objects. After a brief presentation of PyPy, its goals, and its current statuts and roadmap, this talk will dive into the vexed topic of its handling of C extensions. By the end of it, the audience should understand the operating principles of cpyext and have a clearer understanding of what happens when you install and run numpy, for instance, on top of PyPy. Some basic familiarity with CPython internals and how C extensions are made will be assumed.
 So, first few words about me. I have been a PyPyCore developers for about six years now. And well, it's basically my job to work on open source and do consulting related to it. Well, I have a mail and a Twitter original. So this talk, so before diving into C extensions and how we support them in PyPy, which is what's called CpyX, just a quick introduction to PyPy and a few words about what we are doing at the moment, especially because this is, I think, the only PyPy talk at the conference. So PyPy, that's our elevator pitch. PyPy is a fast, compliant alternative implementation of the Python language. So what it means, well, first it means that we run Python programs. But more than that, we try to be compliant with, well, the specification of the Python language, which, as you probably know, is just the source code of Cpython. So that causes a few issues. Nevertheless, we manage to do that and to provide very decent speed for Python programs. So to have an idea of the architecture of PyPy, I like to start from the architecture of Cpython, which is a bit simpler. So, well, PyPy is written in C as you know. And in the Python executable, you have two main parts. You have the compiler that takes the source code, turns it into byte code, and you have the byte code interpreter that takes each byte code and runs a specific operation corresponding to the byte code. PyPy, it's quite similar, except that, well, in the interpreter, on top of the byte code interpreter, we have just in time compiler that works by observing hot loops in the code or any sort of code that runs often. And then this code is traced, analyzed, and so that it gets runtime information on the types. And with this, it emits optimized machine code. You know, the magic in PyPy comes mostly from the Python tool chain up there, which generates more or less automatically the runtime compiler and also adds other features that work with the JIT, like the GC. And the other good thing about this tool chain is that the language we use is our Python, which is a subset of Python, so we can easily test the interpreter. Now, another difference between C Python and PyPy, it's to do with the memory management. So in C Python, it's just a standard C application, so it uses just malloc and free and all that, which is not ideal for an interpreter that tends to allocate small objects in random places all the time. And also, due to this simplistic model, C Python needs to update reference counts all the time. And in contrast, PyPy has quite a performance garbage collector, which is specially tuned for Python. And that way, and also, it works well with the JIT. And well, and there's an observable user difference, which is when at which time does the location happen. In PyPy, since we have a garbage collector, it's the allocation happens when the object is collected by the garbage collector, whereas in C Python, it's when the reference counts hit zero, which happens because you've told the interpreter to determine the reference count. So in C Python, you know when the allocation happens, usually. And another, well, the most interesting difference between PyPy and C Python, it's speed. So people often say that PyPy provides magic speedups, and it often does. But it can do more than that. It's true that, well, if you're lucky, you can have your program that used to run on C Python, try PyPy, and cool, it's twice faster. That sounds great. But actually, if you, in such a case, if you try to optimize for PyPy, you can get your code to be four times faster than on C Python. So why stop at just running PyPy if you want to read the benefits? Optimize. And well, as usual, if you want to optimize, you first need to have good benchmarks. And so a good benchmark is something you really care about. So it's not a micro benchmark. It's not a test. It's something that matches the way you really use your code. And once you have a benchmark, well, you should use a profiler to find the whole parts of the code. And on PyPy, there's basically only one profiler that understands PyPy. It's called VMProf. And once you've done that, then you can actually modify the code and a few pointers for doing that. First, you need to avoid dynamic features at run times, or at least in a loops. So it's fine to use metaprogramming as much as you want before you run your main computation. But in the inner loop, there should be none of that. But what a side benefit of having a JIT is that you can use function calls because function calls are usually inline by the JIT. So well, so you're free to structure your code. And there are a few optimizations that you need to be aware of in order to hit them and or to avoid disabling them by accident. So the one is that attribute access on instances is well optimized. Actually it's as if every class had a slots declaration and you don't need to look up an attribute name in a dict. And the other thing is that we have what we call list strategies. So when you have a list that contains a homogeneous list containing built-in types like ints or floats, then access will be very fast. That's as if you're indexing a C array. So let's have a few words about the current status. I'll start by Python 3. So we had our first free supported release of any Python 3 last Christmas. The latest version was released in April. So what we support is 3.5 at the moment. And it's, well, there are still things missing on Windows, but it's fully supported on Linux, Mac, BSD. And so what we're working on, it's 3.6. We are about halfway, which is always a hard thing to say. But I hope there's not that much left. At least the main syntax features are there. Now it's only the details. And for Python 3, we need to work on more optimizations for things that didn't exist in Python 2. The fact that we have a Unicode everywhere, async stuff is, we need to find ways to make it a bit better. It's decent, but it could be better. And well, that's it for Python 3. Python 2 had a release at the same time as Python 3. And so in this release, apart from the, well, yeah, in this release, we mainly had a few incremental improvements. So I remind you that CFFI is still the best way to talk to C code, and it keeps being updated with every release of PyPy. But the main highlight of the last release was the improvements to extension compatibility and speed. So now you can pip install the main scientific libraries. And we have a few wheels that you can check because they are not, you have no PyPy wheels on PyPI because of issues with many Linux. So if you use PyPy, check out this website, and you'll get binary wheels. So with this, I start the main part of the talk, which is CpyX. So CpyX, well, what is CpyX, really? As a user, you could just think of it as Python.h for PyPy. When you need to compile a C extension, you link it against Python.h. Well, you do that on Cpy and on PyPy, you do exactly the same. And with this Python.h, the compiler will know where to find the actual C function you want to call from the extension. So what's in CpyX? Well this Python.h is mostly generated during the PyPy build, and we have a bit of C code, that sort of unwritten C code, which is mostly a copy from Cpython. But most of the implementation comes from our Python implementation. And well, what does it look like? Looks like this. So this is our Python code. This is the code that implements PyList setItem. And if you've already looked at the Cpython source, you'll notice that it's mostly the same logic. Well, we have some error checking, and then we find where the... Well we fetch the place where we store some item in the list, and we update. And we do a decref because the old object is not referenced anymore. So this is... This looks quite simple, and this is... The good thing is that it is debuggable and testable, because it is using our Python. However, this hides a lot of complexity, which is in this decorator at the top. And this decorator at the top does all the magic of moving between the C world and the our Python world. And there's quite a lot of things to do, like all that and that. So what do we need to do to make Cpy's work? So we have two very different worlds. When we have a C extension that tries to talk to the PyPy interpreter, the implementation languages are different, but this means that the internal object structures are different. We have a completely different memory model inside PyPy. We have managed memory, we have garbage collector that can move the objects around. But that's quite incompatible with the C model, where you refer to things using pointers, and pointers are basically locations in memory. So if things move around, C code gets confused. And in PyPy, we have also a lot more control on what's going on. We have exceptions that can propagate. And so we have internal exception inside the interpreter that used, for instance, to implement the user level exceptions. And but inside the C extension, there's, well, there are no exceptions in C. So extensions need to set the thing called an error indicator that the interpreter needs to check from time to time. Also, C extensions need to deal with the ref counts because you, well, you need to know when an object can be deallocated. But in PyPy, we have garbage collector, so we don't worry about that at all. And so how do we bridge this? And the main idea is to have these C objects linked to Python objects. So they thought that they are basically just proxies for the actual object. And the specific mechanism is just this line here. So if you know Cpython, you'll notice that this up PyPy link is the only difference with this what Cpython does. And this up PyPy link allows the Cpy X layer to query the memory manager in order to find the PyPy object. And well, and then it's just a matter of, well, it's a small matter of programming to implement all the C semantics. So you need to remember your in-crafs and decrafs when you are implementing API functions in Python. And but there are more problems like what do you do when, if you want to call a C function from Python, which is the whole point of having a C extension. And so that's why we have these magic decorators that handle the transformation magically. And basically, yeah, we had levels of indirection. Any time we need to cross a boundary, any time there's something to check, because we are back from the C world where anything can happen. So that's the general idea. But well, you probably know the rest of this quote or actually folk saying, which is that all problems in computer science can be solved by another level of indirection, except the problem of too many layers of indirection. And we've recently noticed that we had too many layers of indirection. So this was last year in Cape Town. And we've started to improve the performance of CpyX. So as I said, crossing this boundary is expensive. Because we have this magic going on, we do it even without realizing, because we don't need to explicitly convert between the C world and the PyPy world. It happens behind the scenes. So we need to be more careful about that. And by being more careful, we can remove a lot of this back and forth. Because in many cases, it turned out to be completely useless. And well, it's an extension of this idea, which is when you have an operation that can happen completely on the C side, then we should just implement it in C and never cross the boundary. So this requires some care as well because of the different underlying assumptions of PyPy and of the C world. But it's possible. And while we've started doing it, it's not complete. But the results are here. So in blue, it's just Cpython as a reference. Everything is PyPy before these optimizations. And red is after. And this is a bunch of rather silly microbenchmarks so that we just check the overhead of each specific operation. So we used to be between four and six times slower than Cpython, and now we are between the same speed and 50% faster. So there's hope for the speed of CpyHackst. And the future, while it's to continue working on this, we can talk. There were a few red bars that were still a lot slower than Cpython. It's probably doable to be faster than Cpython in all cases. Just requires work. And while we have PyPy OpenSpace this afternoon at 2, so if you want to come discuss with your own PyPy problems or your ideas, and well, that's it. I'm waiting for your questions. Any one questions? Were there any C extensions that this PyHackst was written in mind for? Or is it just for the general case? Does that make sense? No, it's for the general case. Well there are extensions we've been using as tests, and also because there are extensions we would really like to make compatible with PyPy. And well, it was mainly NumPy, and it's now fully supports PyPy now I think. Can you say which rule? No, because I don't know. Sorry? Harris one. Harris one. When will PyPy drop support for Python 2? The current official answer is never. Any reasons for that? Well a lot of our users are still on Python 2. And our tool chain is implemented in Python 2, so that's not a motivation. And also as language implementers we are quite happy to have something that doesn't change. So well, these are all the reasons why PyPy likes Python 2. And well, we see no real reason to drop Python 2. We have two branches for Python 2 and Python 3 that share a lot of code, so it's not a major burden to keep supporting Python 2 either. I have a question about Air Python. How is it related to C Python? Is it a subset or something like that? Well it is a subset. So you can run our Python code as Python code. And that's what we do a lot for testing. And that's quite convenient. But the exact definition of our Python is just the subset of Python that can be understood by the tool chain. So what about Cytan? Does this extension, the compatibility layer make PyPy compatible with Cytan? Yes, yes. Cytan is fully... Well I won't say fully because there's one piece of code in Cytan that's C Python specific. But apart from that, Cytan is fully supported and extensions that use Cytan now work on PyPy. Scikit-learn, which uses a lot of Cytan, now has experimental support for PyPy, for instance. And the LXML has supported PyPy for years. Thank you very much.
 So, first few words about me.  I have been a PyPyCore developers for about six years now.  And well, it's basically my job to work on open source and do consulting related to it.  Well, I have a mail and a Twitter original.  So this talk, so before diving into C extensions and how we support them in PyPy, which is  what's called CpyX, just a quick introduction to PyPy and a few words about what we are  doing at the moment, especially because this is, I think, the only PyPy talk at the conference.  So PyPy, that's our elevator pitch.  PyPy is a fast, compliant alternative implementation of the Python language.  So what it means, well, first it means that we run Python programs.  But more than that, we try to be compliant with, well, the specification of the Python  language, which, as you probably know, is just the source code of Cpython.  So that causes a few issues.  Nevertheless, we manage to do that and to provide very decent speed for Python programs.  So to have an idea of the architecture of PyPy, I like to start from the architecture  of Cpython, which is a bit simpler.  So, well, PyPy is written in C as you know.  And in the Python executable, you have two main parts.  You have the compiler that takes the source code, turns it into byte code, and you have  the byte code interpreter that takes each byte code and runs a specific operation corresponding  to the byte code.  PyPy, it's quite similar, except that, well, in the interpreter, on top of the byte code  interpreter, we have just in time compiler that works by observing hot loops in the code  or any sort of code that runs often.  And then this code is traced, analyzed, and so that it gets runtime information on the  types.  And with this, it emits optimized machine code.  You know, the magic in PyPy comes mostly from the Python tool chain up there, which generates  more or less automatically the runtime compiler and also adds other features that work with  the JIT, like the GC.  And the other good thing about this tool chain is that the language we use is our Python,  which is a subset of Python, so we can easily test the interpreter.  Now, another difference between C Python and PyPy, it's to do with the memory management.  So in C Python, it's just a standard C application, so it uses just malloc and free and all that,  which is not ideal for an interpreter that tends to allocate small objects in random  places all the time.  And also, due to this simplistic model, C Python needs to update reference counts all  the time.  And in contrast, PyPy has quite a performance garbage collector, which is specially tuned  for Python.  And that way, and also, it works well with the JIT.  And well, and there's an observable user difference, which is when at which time does the location  happen.  In PyPy, since we have a garbage collector, it's the allocation happens when the object  is collected by the garbage collector, whereas in C Python, it's when the reference counts  hit zero, which happens because you've told the interpreter to determine the reference  count.  So in C Python, you know when the allocation happens, usually.  And another, well, the most interesting difference between PyPy and C Python, it's speed.  So people often say that PyPy provides magic speedups, and it often does.  But it can do more than that.  It's true that, well, if you're lucky, you can have your program that used to run on  C Python, try PyPy, and cool, it's twice faster.  That sounds great.  But actually, if you, in such a case, if you try to optimize for PyPy, you can get your  code to be four times faster than on C Python.  So why stop at just running PyPy if you want to read the benefits?  Optimize.  And well, as usual, if you want to optimize, you first need to have good benchmarks.  And so a good benchmark is something you really care about.  So it's not a micro benchmark.  It's not a test.  It's something that matches the way you really use your code.  And once you have a benchmark, well, you should use a profiler to find the whole parts of  the code.  And on PyPy, there's basically only one profiler that understands PyPy.  It's called VMProf.  And once you've done that, then you can actually modify the code and a few pointers for doing  that.  First, you need to avoid dynamic features at run times, or at least in a loops.  So it's fine to use metaprogramming as much as you want before you run your main computation.  But in the inner loop, there should be none of that.  But what a side benefit of having a JIT is that you can use function calls because function  calls are usually inline by the JIT.  So well, so you're free to structure your code.  And there are a few optimizations that you need to be aware of in order to hit them and  or to avoid disabling them by accident.  So the one is that attribute access on instances is well optimized.  Actually it's as if every class had a slots declaration and you don't need to look up  an attribute name in a dict.  And the other thing is that we have what we call list strategies.  So when you have a list that contains a homogeneous list containing built-in types like ints or  floats, then access will be very fast.  That's as if you're indexing a C array.  So let's have a few words about the current status.  I'll start by Python 3.  So we had our first free supported release of any Python 3 last Christmas.  The latest version was released in April.  So what we support is 3.5 at the moment.  And it's, well, there are still things missing on Windows, but it's fully supported on Linux,  Mac, BSD.  And so what we're working on, it's 3.6.  We are about halfway, which is always a hard thing to say.  But I hope there's not that much left.  At least the main syntax features are there.  Now it's only the details.  And for Python 3, we need to work on more optimizations for things that didn't exist  in Python 2.  The fact that we have a Unicode everywhere, async stuff is, we need to find ways to make  it a bit better.  It's decent, but it could be better.  And well, that's it for Python 3.  Python 2 had a release at the same time as Python 3.  And so in this release, apart from the, well, yeah, in this release, we mainly had a few  incremental improvements.  So I remind you that CFFI is still the best way to talk to C code, and it keeps being  updated with every release of PyPy.  But the main highlight of the last release was the improvements to extension compatibility  and speed.  So now you can pip install the main scientific libraries.  And we have a few wheels that you can check because they are not, you have no PyPy wheels  on PyPI because of issues with many Linux.  So if you use PyPy, check out this website, and you'll get binary wheels.  So with this, I start the main part of the talk, which is CpyX.  So CpyX, well, what is CpyX, really?  As a user, you could just think of it as Python.h for PyPy.  When you need to compile a C extension, you link it against Python.h.  Well, you do that on Cpy and on PyPy, you do exactly the same.  And with this Python.h, the compiler will know where to find the actual C function you want  to call from the extension.  So what's in CpyX?  Well this Python.h is mostly generated during the PyPy build, and we have a bit of C code,  that sort of unwritten C code, which is mostly a copy from Cpython.  But most of the implementation comes from our Python implementation.  And well, what does it look like?  Looks like this.  So this is our Python code.  This is the code that implements PyList setItem.  And if you've already looked at the Cpython source, you'll notice that it's mostly the  same logic.  Well, we have some error checking, and then we find where the...  Well we fetch the place where we store some item in the list, and we update.  And we do a decref because the old object is not referenced anymore.  So this is...  This looks quite simple, and this is...  The good thing is that it is debuggable and testable, because it is using our Python.  However, this hides a lot of complexity, which is in this decorator at the top.  And this decorator at the top does all the magic of moving between the C world and the  our Python world.  And there's quite a lot of things to do, like all that and that.  So what do we need to do to make Cpy's work?  So we have two very different worlds.  When we have a C extension that tries to talk to the PyPy interpreter, the implementation  languages are different, but this means that the internal object structures are different.  We have a completely different memory model inside PyPy.  We have managed memory, we have garbage collector that can move the objects around.  But that's quite incompatible with the C model, where you refer to things using pointers,  and pointers are basically locations in memory.  So if things move around, C code gets confused.  And in PyPy, we have also a lot more control on what's going on.  We have exceptions that can propagate.  And so we have internal exception inside the interpreter that used, for instance, to implement  the user level exceptions.  And but inside the C extension, there's, well, there are no exceptions in C. So extensions  need to set the thing called an error indicator that the interpreter needs to check from time  to time.  Also, C extensions need to deal with the ref counts because you, well, you need to know  when an object can be deallocated.  But in PyPy, we have garbage collector, so we don't worry about that at all.  And so how do we bridge this?  And the main idea is to have these C objects linked to Python objects.  So they thought that they are basically just proxies for the actual object.  And the specific mechanism is just this line here.  So if you know Cpython, you'll notice that this up PyPy link is the only difference with  this what Cpython does.  And this up PyPy link allows the Cpy X layer to query the memory manager in order to find  the PyPy object.  And well, and then it's just a matter of, well, it's a small matter of programming to  implement all the C semantics.  So you need to remember your in-crafs and decrafs when you are implementing API functions  in Python.  And but there are more problems like what do you do when, if you want to call a C function  from Python, which is the whole point of having a C extension.  And so that's why we have these magic decorators that handle the transformation magically.  And basically, yeah, we had levels of indirection.  Any time we need to cross a boundary, any time there's something to check, because we  are back from the C world where anything can happen.  So that's the general idea.  But well, you probably know the rest of this quote or actually folk saying, which is that  all problems in computer science can be solved by another level of indirection, except the  problem of too many layers of indirection.  And we've recently noticed that we had too many layers of indirection.  So this was last year in Cape Town.  And we've started to improve the performance of CpyX.  So as I said, crossing this boundary is expensive.  Because we have this magic going on, we do it even without realizing, because we don't  need to explicitly convert between the C world and the PyPy world.  It happens behind the scenes.  So we need to be more careful about that.  And by being more careful, we can remove a lot of this back and forth.  Because in many cases, it turned out to be completely useless.  And well, it's an extension of this idea, which is when you have an operation that can  happen completely on the C side, then we should just implement it in C and never cross the  boundary.  So this requires some care as well because of the different underlying assumptions of  PyPy and of the C world.  But it's possible.  And while we've started doing it, it's not complete.  But the results are here.  So in blue, it's just Cpython as a reference.  Everything is PyPy before these optimizations.  And red is after.  And this is a bunch of rather silly microbenchmarks so that we just check the overhead of each  specific operation.  So we used to be between four and six times slower than Cpython, and now we are between  the same speed and 50% faster.  So there's hope for the speed of CpyHackst.  And the future, while it's to continue working on this, we can talk.  There were a few red bars that were still a lot slower than Cpython.  It's probably doable to be faster than Cpython in all cases.  Just requires work.  And while we have PyPy OpenSpace this afternoon at 2, so if you want to come discuss with  your own PyPy problems or your ideas, and well, that's it.  I'm waiting for your questions.  Any one questions?  Were there any C extensions that this PyHackst was written in mind for?  Or is it just for the general case?  Does that make sense?  No, it's for the general case.  Well there are extensions we've been using as tests, and also because there are extensions  we would really like to make compatible with PyPy.  And well, it was mainly NumPy, and it's now fully supports PyPy now I think.  Can you say which rule?  No, because I don't know.  Sorry?  Harris one.  Harris one.  When will PyPy drop support for Python 2?  The current official answer is never.  Any reasons for that?  Well a lot of our users are still on Python 2.  And our tool chain is implemented in Python 2, so that's not a motivation.  And also as language implementers we are quite happy to have something that doesn't change.  So well, these are all the reasons why PyPy likes Python 2.  And well, we see no real reason to drop Python 2.  We have two branches for Python 2 and Python 3 that share a lot of code, so it's not a  major burden to keep supporting Python 2 either.  I have a question about Air Python.  How is it related to C Python?  Is it a subset or something like that?  Well it is a subset.  So you can run our Python code as Python code.  And that's what we do a lot for testing.  And that's quite convenient.  But the exact definition of our Python is just the subset of Python that can be understood  by the tool chain.  So what about Cytan?  Does this extension, the compatibility layer make PyPy compatible with Cytan?  Yes, yes.  Cytan is fully...  Well I won't say fully because there's one piece of code in Cytan that's C Python specific.  But apart from that, Cytan is fully supported and extensions that use Cytan now work on  PyPy.  Scikit-learn, which uses a lot of Cytan, now has experimental support for PyPy, for instance.  And the LXML has supported PyPy for years.  Thank you very much.
Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_0_59.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_60_131.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_132_227.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_228_815.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_816_1511.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_1512_3059.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_3060_3971.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_3972_6731.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_6732_9755.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_9756_15347.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_15348_20327.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_20328_20459.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_20460_22691.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_22692_25103.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_25104_25223.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_25224_25379.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_25380_28667.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_28668_30167.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_30168_32987.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_32988_33395.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_33396_35867.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_35868_37151.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_37152_37511.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_37512_39059.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_39060_39131.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_39132_40247.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_40248_40427.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_40428_41015.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_41016_41063.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_41064_41615.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_41616_42119.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_42120_43271.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_43272_43871.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_43872_43979.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_43980_44027.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_44028_44147.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_44148_44219.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_44220_44375.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_44376_44471.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_44472_44639.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_44640_44855.png Ronan_Lamy_-_Adventures_in_compatibility_emulating_CPython_s_C_API_in_P_44856_44945.png