C-API with Python wrapper vs In tree pybind interface


I’m in the community bonding period of GSoC for MLIR Python bindings. My previous preparations for the project are on the topic: Study route of MLIR python bindings

I’m trying to do some binding prototypes for Operation.h, Attributes.h, and Types.h in the llvm-project/mlir/include/mlir/IR/ directory.

After discussing with @stellaraccident , I realize there are two methods for Python bindings:

I’m looking forward to a discussion and suggestion between these two binding methods.


I think having a well-principled C API that other languages can use to provide bindings is very valuable (virtually every language has some C interoperability). The interfaces under include/mlir-c are leftovers from our attempt to build Python and Swift bindings simultaneously. At some point, we realized that, given how much C++-first MLIR was, connecting directly to C++ would take us significantly less time than going additionally through C. And we were short on time. It’s the same trade-off for you, designing and implementing C APIs is valuable, but only indirectly contributes to your GSoC project.

Personally, I’d like to see whatever we do on this next to be something that we see as reasonably principled and able to carry forward, and as someone who has had to write this two times quickly out of tree just to have something, I would consider a project in this area to be a success even if it just provided a seed with good patterns that others can build on.

There is no question in my mind that a good cffi based API for some of the higher level parts is completely possible/straightforward. The parts I am thinking of here include context creation, asm printing/parsing, pass invocation, and diagnostic handling. IREE has a pybind11 based implementation of this level of things here. Such an API is useful for tools that want to invoke MLIR based tools. In IREE’s case, we link in some project specific translators to run the compiler pipeline and generate artifacts: the linkage story here is non trivial and means that we will want to do this in a way such that projects can extend MLIR on the c/c++ side, have those symbols made public and bolt additional language bindings on dynamically.

What I can’t quite see is the path to a principled and usable cffi based setup for IR manipulation. Probably some combination of c-apis for OpState (for construction), Operation*, Region, Block, Attribute, Type to at least get the core data structures is a pre-requisite. There are some thick mechanics there but the data structures are simple. Also, it seems like anything usable would need a language specific tablegen story to match.

The issue I ran into pretty quickly was how to handle the extensibility of the Type/Attribute system, which is all c++ APIs and I don’t see a way around having implementation specific APIs to handle them (with, say, baseline parsing/printing coming for free). For construction, my attempt at this with pybind11 was to create a DialectHelper base class and implement accessors for the std stuff directly (see a c++ custom subclass and python side). It works well enough for me for a single project, but I’m not thrilled with how it would layer in a more open system (and it relies on python multiple inheritance and dynamic class construction – sure signs that a python programmer has been here).

We now have three different pybind11 out of tree bindings, each built with admittedly pragmatic aims. I would welcome for whatever comes next in tree to at least get some foundational pieces in place that we/others can build on and extend. At this point, I would prioritize foundations over completeness, because, as an outside project, I can always add a new ad-hoc entrypoint to create or introspect my weird new type, but in the current state, there isn’t enough there that I can just show up with that new entrypoint: I have to start by defining… What is a context, how do I parse, etc.

Regarding the c vs pybind11 angle – as a python developer looking to wrap something, I will always choose pybind11 (for many reasons). However, MLIR is not principally a python project, and I have observed that when people stand up and say “we should have a good C API and base things on that” they’ve rarely been wrong. It has higher startup costs but is the only way to solve various issues that arise when trying to scale usage outside of a single project/binary. If we think that is the right direction for MLIR, I would consider the GSOC project successful if it got the pattern established and got to the point that we could start rebasing some of our uses on top of it. I’d also suggest taking some time to prototype it – these things tend to be highly sensitive to getting some basics right.

If we’d rather take an intree pybind11 approach, then we now have several examples to choose from and should probably have a discussion about what worked/didn’t and what to carry forward.

I would love to see that MLIR provides a principled C API (for IR builder). Currently, I am looking at using MLIR with some android libraries, so I am also trading off whether to enhance the C API of MLIR or write a wrapper to an Android specific interface (e.g. AIDL) for our project/dialect.

Would you be able to share what the most important parts of the API are for your case?

Sure. I am mainly looking at APIs needed for building an IR, so that I can then feed it as input to a MLIR-based compilation pipeline that can be built independently from its Android clients.

I’m in a similar spot where I would like to use MLIR from my Rust project to emit some IR to disk. This requires a solid C API to funnel through the FFI of Rust. Having the IR building part exposed would allow other languages to more easily feed into a flow with MLIR downstream.

Builders are tricky to expose because we need them to scale automatically to new ops and dialects. In C++, this is done through templates and overloads, sometimes calling table-generated functions, which isn’t trivial to replicate in C. We could either consider exposing Operation::create level, but that will give APIs a JSONic feel we want to avoid, or consider table-generating C wrappers around build calls, but that will require a mechanism for converting types in builder ODS declarations and something to avoid function overloads.

I haven’t been able to find a nice way around some massaging. For my python bindings, I exposed a low level wrapper around create and then hand coded some helper classes on top of it for the dialects I’m interested in.

Example of a python level factory: https://github.com/google/mlir-npcomp/blob/master/python/npcomp/dialect/Basicpy.py

Which extends a native helper class that provides more builtins (which were not convenient at the python level): https://github.com/google/mlir-npcomp/blob/master/python_native/NpcompDialect.cpp

And extends the common helper subclass: https://github.com/google/mlir-npcomp/blob/bb871e7601b5cd72482beeb8f1daf6876f65cf29/python_native/MlirIr.h#L164 ([binding methods here] (https://github.com/google/mlir-npcomp/blob/bb871e7601b5cd72482beeb8f1daf6876f65cf29/python_native/MlirIr.cpp#L176))

They are defined to play well with python multiple inheritance so I can create an instance from any number of these dialect helper classes.

None of this would apply to C but you get an idea looking here how many types/factories need to be wrapped to have much of anything. I could envision tablegening the top level build calls but there is a fair amount of the API that will need to be built and solid to even think about this. Even then, it is a little “jsonish” because I don’t have ODS names for results, and if generating build methods, you’ll want the rest before too long.

As a pragmatic step, I would focus on getting C-API bindings for the low level Operation class, then try to write out some C Op wrappers for a couple of core dialects to get a good feel for it, then look at extending ODS to match that. It’s a lot of work to get it all.

Another angle is that if someone is serious about providing APIs for building IR for their dialect for a particular language, then they may have their own ODS that could emit their language API wrappers around the low level C APIs (and emit ODS separately for registering ops with MLIR).