 So, let's go. Time is up. I guess we can start. OK. Hello, once again. Next topic is going to be GStreamer. So we have, again, Sebastian here. Last year, he was already here talking about that. So some more features, some more stuff. GeoObjects are classing. OK. Thank you very much. Round of applause. Yeah. Hello. Thank you for coming. Today, it's not actually going to be about GStreamer, but about the object subclassing in Rust for extending GTK and GStreamer or other libraries. But what exactly that means, we will see in a second. So first of all, some words about me. I'm Sebastian Dröger. You saw it in the schedule. I'm a GStreamer developer and maintainer since a few years. Also working on GNOME since quite some years. I'm also the maintainer of the GStreamer Rust bindings and working together with Geom and others on the GTK, GLIP, et cetera bindings. I'm currently working at Centicular. I'm one of the founders. And we do consulting around GStreamer and other stuff. And some of our work is also involving Rust nowadays, fortunately. But that's enough about me. So what are we going to talk about today? So today, the topic is going to be subclassing or how to do inheritance in Rust, like known from other object-oriented languages. Like, I don't know, C++, Java, Objective-C, you name it. And specifically, we look at that today from a GTK-RS point of view. But many of the ideas can also be transferred to other systems. And most importantly, the main topic today will be why is it useful? How is it implemented? And how can it actually be used? So some of you are probably then going to say, but Rust does not support this. Well, that's true. Rust does not support inheritance. And generally, not what people think about when they hear object-oriented programming. But that doesn't really matter, because Rust has a very powerful type system. And we can implement something that looks very closely to inheritance to object-oriented programming in other languages. And we can make it quite nice in Rust. It's a very powerful language, fortunately. Others might say object-oriented programming, it sucks. We don't really want to do that. But I think that's a bit short-sighted, because it's a nice tool to have in some cases. It's something that you probably don't want to use in normal Rust code. But it's a tool that you might have to use in some specific contexts. And generally, in Rust, things like the traditional object-oriented programming paradigm doesn't really fit into the language that well. There are nicer patterns and paradigms to write, well, nicer code. But as I said, it's a tool that can be useful to have. So why are we talking about this specifically? First of all, OOP is everywhere. Almost every major language out there is based on traditional OOP. There's very few languages that are not based on that. I think from the modern ones, it's mostly Go that doesn't really follow that paradigm. But apart from that, and Rust, of course, but I mentioned that before. And it would really be a shame if we couldn't use all this code that is already out there that was written over the last decades. Because it would mean we would have to really reinvent the universe. We have to write everything again from scratch. And it would slow us down quite a lot. Like developing, for example, a UI toolkit is not something you do during a weekend. It's going to take a while. So it might be useful to be able to use a UI toolkit that is written in another language. And most likely, that's going to use OOP paradigms. So why exactly? I said interoperability with other platforms. So in our case, GNOME, GTK, Gstreamer, we talked about that before. Well, also other things like the HTML DOM. So there are multiple points of view that we have to consider here. Either we only want to make use of existing libraries like what we saw before with GTK RS. There's already something out there that is making use of OOP paradigms. We want to be able to somehow make use of that from Rust. But maybe from the other side, we also want to be able to extend these libraries. We want to, I don't know, write a new GTK widget, write a new fancy button that is doing whatever kind of thing, or write some kind of view widget that is showing things in a way that GTK couldn't do by itself. Or maybe things like what Jordan mentioned in the beginning. Maybe we really want to rewrite some library completely in Rust, but somehow we need to be able to export or to provide an API from this Rust code that can be used from other languages nicely. Or in the case of R SVG, it even has to be completely compatible with the old C API and ABI. So those are all cases where you probably, even if you don't like normal OOP, you probably want to look into it nonetheless. And in this case, Rust is really ideal for interoperability with other platforms. There's no garbage collection by default. There's no big runtime. There's basically no overhead. Don't have to tell you that, you all know that. But that's something that makes it very interesting to be able to express all these things that are available in other languages in Rust. Because, well, in the end, ideally we would like to have everything written in Rust, but out there there is still a lot of other stuff that still is in other languages and needs to be able to somehow use all our things. So some words about the object because that's the main thing we are going to talk about today. It's a C library for doing traditional OOP. Well, as you all probably know, a C doesn't have any language support for OOP, basically the same thing as Rust. But what the object provides is things like classes, interfaces, well, inheritance, virtual methods. It has some kind of runtime type introspection. It basically provides you an object system that is very close to what Objective-C is doing and all that in C. And because C doesn't have support for all of this, you have to write a lot of things yourself that like in C++, the compiler would do for you. You explicitly have to handle things like a Vtable for virtual methods and stuff like that. You explicitly have to register types. So it's in C at least a lot of manual work to do. But in the end you get something like an object system that you also know from other languages. And the object, well, it's used by GNOME, GTK, GStreamer, and lots of other stuff out there. And at least personally, one thing that is very, very useful with the object is a project called object introspection. Guillaume mentioned it shortly. It's a project that allows you to get API description as an XML file that contains like what kind of classes do we have, what interfaces do we have, what's the base class of this, what methods are available, what are the parameters of these methods and stuff like that. And it allows you to automatically generate bindings for all kinds of languages like Rust, but the same thing for Python, JavaScript, C++, Go, and well, you name it. And the other nice thing with Geoptic is well, it's providing a stable OP-based API API. Something that Rust does not have. The only thing with Rust that you have is a stable ABI right now as well, the C ABI. But by exporting something like a Geoptic API ABI from a Rust library, you still can provide a more rich API to users from other languages. And thanks to the object introspection, make it really, really easy to, well, I don't know, use your Rust code from some Python application while at the same time having some Go application using the same library. And you never wrote any Python or Go code yourself. It's just automatically generated for you. So first topic is going to be how to actually use Geoptic from Rust. That's all old stuff, all stuff that you already could do in the last year and the last two years. I will show quite some type definitions, quite some code on the slides, how it actually works. And basically what we're going to do here, we are going to use types that are already existing in, like GTK, like the existing button class, for example. And how does that look in Rust and how does it actually work? So first some example code. I hope it's big enough. So at the very top, we just create a little window. Then we create a button. We set a label on the button. It's just saying to test. Then we are adding this button to a window and tell the window that it should show all the things. So generally all this code that looks like more or less normal Rust code, if you don't look too closely. Well, one thing that might look a bit weird is we have this button set label thing and we never declared the button as mutable. Okay, but that's just interior mutability. That's nothing too fancy in Rust. It's not very different to like using ref cell or using a mutex or things like that. So okay, that's maybe a little bit weird. But on the other hand, if you then take a look at the types, you will notice that here adding the button to a window where it's not actually a function that is on the window type. It's something that is provided by the base class of the window by GTK container. But nonetheless, you can directly call it on the window. Similar, the show all is something that is provided by GTK widget. So already here, it looks like normal Rust code, but in the end, we already make use of virtual methods or we make use of, in this case, inheritance by calling methods that are defined on the parent classes of the types that we have here. So how does it look under the hood? First of all, all this information, it's probably also quite useful to understand the type, well, generally the API documentation of GTK RS and other bindings because some of the types might look a bit weird and you might not be able to find the methods that you're actually looking for directly. So knowing how this works a bit under the hood is quite useful. So first of all, how do objects actually look like? So conceptually, you have something like a struct that just contains a pointer to the C object, to the C type. It's really just a little wrapper around the pointer that we got from C. On top of that, things like clone and dropper implemented, which are then providing things like reference counting. So whenever you clone the button, you don't actually get a copy of the button, but instead it's increasing the reference count. Whenever it's going out of scope, well, the reference count is decreased again. Once it reaches zero, it will go away. And basically, all these objects, they are behaving like as if they are an RC, like the Rust reference counting wrapper, containing a ref cell and then the actual things in there. So you have reference counting as said before, you have things like weak references. And for the actual state in there, you have interior mutability because this is OP after all. It's all about objects containing state that is somehow mutated. So that's what we have here. And on top of all these little structs, there's lots of infrastructure via traits that allows you to transform raw C pointers to the corresponding Rust types and back. But I'm not going too much into details there. But basically, what Glib defines is some kind of ownership system for pointers. So basically, if you have a function that is returning your pointer, it's going to, you will have in the API description that the pointer that you get returned, it's something you have to free or something that you don't own. And well, all this translation infrastructure, it's based around these ideas. So you can say, I call this C function and the pointer that comes back, I will take ownership of that or I will not take ownership of that and that's the basic idea behind of all that. Then if you look a bit closer in the documentation, you will go to the input blocks for the different types and you will notice there's almost nothing. So the only thing that you will see in the input blocks are constructors and static functions. You don't have anything in there that is taking self as a first parameter except for something called final types, types that don't and can't have any further subclasses anymore. And yeah, then if you go into the Rust documentation, click on the source link, the only thing that you will see and for example, the constructor for the bottom is just a single line. It's calling directly into the C code. There's not much wrapping around it. It's really just taking the pointer from C, wrapping it into the struct we saw before and that's it. And most of this code, like the one that I showed here, it's completely auto-generated from the API description. And right, so here we also see that that comes later. Never mind. So now you might wonder where are all the methods. So all the methods that you might want to call on actual instances and for that there are traits defined. So for example, for GTK container, what I mentioned before where we had the window and we were putting the button into the window, you will find a trait called container Xt and all the traits they are called, well the type name and then Xt and that's where all the methods of these types are then available. Well, if you see a trait that is just called Xt, it's going to be completely auto-generated. If you find a trait that is Xt manual, then it's manually written because that's still necessary for some things, but yeah, that's details. All these traits they are implemented generically over all types that are a subclass of these specific types or in our case of GTK container, there are subclasses like a GTK bin or also in the end the GTK window. And what will happen is we have a generic implementation for the container Xt for all types T that are a container and similarly here the add method. It's taking a parameter, a widget that you add to something and that's also generically implemented for everything that is a widget. So you can put a button in there, you can put, I don't know, a label in there, whatever is a widget. And then if you again look at the code that is actually generated, it's also relatively short. It's just calling the C function below it, then there's some relatively long function chains for actually getting the pointer out of it. So what happens here is basically that by calling SRF, so everything that implements the is add trade, it also requires you to have SRF for getting a pointer, for getting a reference to a widget again. So at this point here, we would have a reference to a widget and from the widget, we can then get a raw pointer that we then pass to the C function. And generally all the traits from GTK RS, all these extension traits, they are then something you can import via the prelude module. So each of these traits, they have a prelude module that you can just with a wildcard include and then you have access to all the traits. Then we saw before this is a trade, it's really just a marker trade and the thing that it's telling the type system is whatever is implementing is something, it's a subclass of something or it's implementing the something interface. And that's really the core thing that allows us to model the class hierarchies with a Rust type system. And yeah, as I mentioned shortly before, it's also implying that the type is implementing SRF P so you always are able to get a reference to a P. And yeah, you also in functions always use it in a generic way like this so that if you have a function that can accept anything that implements a specific interface or it's a subclass of something, you would use this trade. And yeah, if you look at the documentation a bit, it's also implying another trade, the object type trade and that's really the core of everything. So every object type, well, that's why it's called like that, is implementing this trade and it's basically the type system mapping between the Rust types that we saw before, the struct that contains just a pointer and the corresponding FFI types, the corresponding C types, provides you things to convert from and to a raw pointer. It gives you access to the type ID of the thing that you have there and it also gives you access to all kinds of convenience trades. So everything that is implementing the object type trade is also implementing clones so you can always clone them. It's implementing equality trade, debug trade and all those things. So whenever you have something that's an object type, you know you can use them like all the others. Then, well, it's not so spectacular to be able to call methods of your parent classes but generally if you have things like type hierarchy, you also want to be able to cast in the other direction. So in the G-Lib bindings, there's a trade called cast for that. So for upcasting, like for going from a button to its base classes to make it, I don't know, a widget for example, that's completely zero cost. It's something that also can't fail so you call it, it's basically for free but it's also something, at least if you call methods, you don't really have to do the upcasting. And on the other side of the downcasting, say you have a widget but you actually know that it is a button, you might want to cast it to a button. That's also something that is almost for free. The only thing that's happening is that during runtime, there are some checks that this pointer is actually a button and that it's safe to actually cast it to a button. And well, in the end, everything in there is done via transmute or via casting pointers because all the rust representations of these objects, they are really just a little struct around the pointer so they all have the same memory representation. So it's safe to convert between the different types as long as you ensure that the pointer that is behind it is really your target type. And the way you use it is basically like this. If you have a button, you can upcast it to a widget. As you can see, this can never fails. You don't get a result or something back. And if you want to downcast it, well, that's something that gives you a result back and it can fail if it's not actually a button in this case. So for wrapping up the first part, we have the extension traits that are for all the methods on the instances of our objects. We have the input blocks that are mostly for constructions and static functions. Almost everything is auto-generated at this point. All the usage from your point of view is completely safe rust. If you are able to somehow crash your application by using these bindings, then that's a bug. That's not a bug in your application. That's something you should tell us about. But I hope it's not possible at this point. Well, upcasting can be done implicitly. Downcasting, of course, has to be explicit like in C++, Java, other languages. And most of the things that I mentioned on the slides before, they are actually auto-generated also from a macro. So like the actual struct type is auto-generated. And it's this little macro. You just give it the different types, the FFI types, the names, what base classes it has and what interfaces it's implementing. So there's a lot of automation inside the bindings. And one thing I didn't really mention before, but basically if you write an application using GTK-RS or using the GISMA bindings, in the end, it's compiling down to assembly that looks very close to what a C compiler would output for exactly the same code. So there's really not much happening in there. It's mostly stuff that happens on the type system side and at compile time. So if you want to look at the code yourself, well, you can find it there. Basically inside the GTK repository, you will find files like SRC slash button RS that's going to be the manual code. And then there's SRC auto button dot RS, which is all the auto-generated code. G-Lib bindings, you can find them here. And most of the stuff I was talking about so far is going to be in this file here, which is all the basic infrastructure. Now let's get to the new topic. So as the other two talks before I mentioned already, currently a big problem is it's not possible or not easily possible to create subclasses of objects from Rust. And you generally need to do that when interfacing with the object-based libraries to be able to extend the existing API. I don't know to implement a new kind of widget, as mentioned before, or to implement a custom model for like a tree view for a list box or things like that. It's also something that you need for like extending GStreamer by new codecs, new filters, et cetera. You also need to be able to somehow create subclasses then. And generally you need this whenever there's whenever you want to do some Rust code that is in the end exporting some API that is object-based. And yeah, this is all new stuff. It's in Git. It's going to be in the next release, hopefully in the next few weeks. So generally it's all in the subclass module of Glib and all the other crates compared to C. Well in C it's quite some boilerplate that you have to write. You still have to write quite some boilerplate to do it in Rust. It's going to be more than like in Vala especially or in Python, but it's a little bit less than in C. Generally it's all a little bit safer than in C because most of the relationships between things are expressed in the type system. And overhead-wise, as I said before, it's compiling down more or less to the same assembly. And yeah, it's going to be lots of traits in generic functions again. And some parts on your site might also still require writing unsafe code, but most things fortunately not. So let's dive in. First we have again a very central trait which is the object subclass trait. And this would be something that you would implement to say, okay, I want to define a new G object. You would say this is the parent type, the one I'm inheriting from. You also have to give some type mappings for the corresponding FFI structs, for the corresponding structs that would be seen on the C side. It's also the thing that is providing you the registration of this type, initialization of the class and the instance, and all these things. We see that in a little bit, what that is, how that looks like. And it also provides you a weight from, like if you're implementing a button, you would implement what you would implement that on, yeah, on your implementation of the button, but you would still have the rust wrapper type on the other side, but somehow inside your implementation, you want to get to your private state. Like you know, if you implement something in, say in Java or C++, you always have some private state assigned to your classes, and somehow from the public object, you still need to get to your private state, and that's also something that is implemented in this straight here, or implemented via this straight here. So basically, the way how it looks like is like this. You say, I want to implement my object. You have to give it a name. You have to say, okay, in this case, it should be direct subclass of glib object. Then you have to give it to the FFI structs that are corresponding to the instance and to the class struct. The class struct is basically something like the V table. It contains function pointers for the virtual methods, but more about that later. And yeah, for the default cases, where you don't really have to extend anything, there are some generic ones that you can always use as long as you don't have to define new virtual methods. Then, of course, some macros that create some boilerplate, and then, as mentioned before, you have functions for initializing the class and for creating an actual instance of your new subclass. And yeah, so in the class initialization, you would then do things like saying, okay, my class, it also has this and that property of that type with that description and things like that. So in there, you would be able to configure your type, what kind of additional API it provides. And yeah, I said before, the thing that is returned here, the self, that's not going to be a glib object directly, but it's like the private side, the implementation side of your object. But you are able to convert it back in both directions. Yeah, so that I already mentioned before. So one thing where you currently need, still need to write unsafe code is if you want to define your own virtual methods, because there you still need to provide some kind of functions that are converting from C to Rust and back. That's something that can't be auto-generated yet, but it's usually quite mechanical and there's not much you could do wrong. Yeah, that part I will just skip for now. So that would be one of these examples of the functions that are converting a virtual method from the C side to the Rust side. And yeah, as you can see here, well, it's an unsafe external C function, so you can pass it as a pointer to C API. In this case, it would be stored in the V table. And well, it's a generic function, so that's something that I didn't know before starting with this, that you can even define generic external C functions and have function pointers to that, so that's very, very useful here. And basically what happens here is you get from the object, the instance passed in as a pointer, and from the instance, we are then able to get the actual implementation, the private part of your implementation, and on that we are then calling the constructed method, the Rust method. And as you can see, it's really very mechanical, there's not much you could do wrong, but currently we don't have a way to auto-generate this. And yeah, it's really not doing much, it's more or less just translation. And yeah, on the Rust side, there's no boxing happening anywhere, there's no additional heap allocations happening anywhere, there's no dynamic dispatch, there are no trade objects or something. It's all really combined on to exactly the code that you would have written in C. Then for each class that you want to implement, there are class-specific input traits. So there's something like a button, there would be something like button-imple, that doesn't exist yet, but there's something like object-imple for direct subclass of the object. And in there are all the Vitto methods. 15 minutes, okay. There are all the Vitto methods that you could implement. And usually what is also provided is default implementations. So you don't really have to implement all of them, but only the ones that you really want to. And what it also provides you is a way for calling into the implementation of the parent class. So like what you have in C++ or Java with base or with super, you can call the implementation of a Vitto method that is defined by the parent class. And generally, like if you have, for example, a GTK widget-imple, it would also depend or require the GLIP object-imple. So by that, you also have the subclass in relationship enforced in this point. So you have to implement all the traits that are on your path in the class hierarchy where you want to extend. And this is really where all the interesting things are then happening. And an example of that is the object-imple trait. So it basically looks like this. This would be the Vitto method that you overwrite. As you can see by default, it's just calling the parent implementation. But in here, if you implement the trait, you can then do whatever you want to do. And the parent one, as you can see, is a little bit more involved. Basically, it's these two lines, they are then getting the Vtable of the parent class of yours and then calling the corresponding C function pointer that is behind this virtual method. And then it's calling back into the parent implementation of this. So currently, all these things are something that have to be written manually. And you would have to write one of these for every possible class that you might want to extend. So there has to be one for GTK buttons, one for GTK widgets, one for, I don't know, G-streamer elements and stuff like that. There is a branch for the code generator that allows to generate these, but that still has to be updated to the latest reality. So there's still some more work to be done. And generally, it's not easily possible to generate all these things for all the classes because some types that are provided by GTK, they are simply unsaved by design from a Rust point of view. So we still have to figure out some things there for doing it more automatically. Yeah, so some more remarks about the memory layout. So as we saw, where was it? Here you have to provide also the FFI types that are then in the end passed to the C functions. And so in the case of the instance struct, what you have is in the beginning, it always needs to have the instance struct of the parent class as a first element, but that also means this instance struct you have to put the wrapper attribute in it, say it should have a C representation. And the interesting part about that is then that as the parent instance is at the very beginning of it, you can simply cast to it, it's behaving exactly as if it would be of the parent type and that's basically how all this works internally. And the other thing is your private data, the actual implementation of your subclasses, that's something that is stored directly before this pointer, it's exactly the same allocation. So what you then have is first all the memory of your parent classes, then the memory of your own subclass, and right before this pointer you then have all the private data. And that also includes your, from your subclass, the thing that is implementing the object subclass trade. It's also just stored inside the same allocation and it's all something that the object is taking care of for you. And as said before from the Rust side, there are no additional heap allocations, there is no dynamic dispatch or anything, it's really exactly the same thing that would have happened if you were writing the same thing in C. Yeah, so now we had all the little pieces for actually defining a subclass. Then somehow you need to be able to actually create instances of that. So basically the object subclass trade, it's giving you a method call to get type, which on the first call will register your type and on the future calls will just give you the type ID and you can generically then create objects like this. That gives you a generic Glib object and then you can cast that to whatever subtype you want to have it to be. Yeah, and another thing I mentioned before is that whatever, the thing that you're actually implementing that's not really a Glib object or it's not a Rust wrapper type around your Glib object yet it's just the private part of it. So whenever you want to use it like a real GDK-RS object thingy, then you still have to use the Glib wrapper macro that I mentioned before. So that might sound a bit confusing, but so if you want to look at the code, there are quite a few examples and especially so GDK-RS has a repository with lots of examples in the pending branch. There's an example that is creating a list box model based on all this and that's showing you in detail all the little steps with lots of comments that explain what is done there, how all of this is then actually used. Yeah, similarly for GSTMR, there's a repository that contains 16, 17 different GSTMR elements at this point that allow you to extend GSTMR with new filters, with capabilities for supporting new formats and stuff like that. So those would be also other examples that you could take a look at and they are a bit more involved than the list box example. And another thing that you can take a look at is lib-rsvg, it has now a branch called subclass and in there, there's basically no C code left anymore in that branch, it's all written in Rust and it's making use of this to provide a G object to C or to other languages and it's completely ABI compatible with the previous C code. So what else is possible, didn't really fit into the talk with lots of details but you can also define the object properties and signals like, I don't know, the buttons for example have a clicked signal but if you implement your own button you could implement, you could give it its own clicked signal, you can define your own written methods, you can define class methods since two days ago it's also possible to define your own interfaces if you want to do that and it's also possible to define boxed types which are just flat types without a type hierarchy but in a way that you can pass them around through Glib-based APIs that there's a defined way of how to handle the memory management. So that would allow you to pass around normal Rust struct in any way you want. So for the future, as I mentioned, we want to auto-generate more code, a lot exists already but there's still for the sub-classing quite some stuff that should be auto-generated, it will come. Then support for more classes because I'm mostly working on GStreamer for sub-classing there's only the GStreamer side of things done mostly so someone who really cares about GTK and uses GTK would be useful if we would get some help there to also do the same thing for all the GTK types. Then what was also mentioned before is there's a procedural macro in the works called the object class which provides you a way of writing something that looks a little bit like I don't know C sharp mixed with Rust inside it and that allows you to write new sub-classes of the objects and that would also allow you to write safely new virtual methods and stuff like that but it's not really ready yet and it will take a while but in the end I hope it's going to make all this even more nice that you really don't have to worry about any boilerplate code anymore and just write down the things that you want to have implemented. And of course we want to make use of all of this to write more things in Rust, to write more things in Rust that previously only C was really an option and like lib-isvg started to rewrite more GNOME libraries in Rust even gradually just replacing a few functions, replacing a few types or writing new types in Rust so there's lots of stuff that you can now do that was not really possible before. So it's really your chance to get involved. You could help with lib-isvg, you could help writing new gSIMA plugins, you could help replacing parts of other libraries with Rust code, first talk to the maintainers if they really want that or whatever other ideas you have and that's it for today. Do you have any questions? Yeah, that exists. So the question was if there exists types save wrappers for properties and signals so for existing types, yes they are auto-generated and you directly use the corresponding types that are behind that. For other things, if you define your own properties and signals on your own subclass of the object you can then pass around things that are called a glib value and in there you can store anything, you can store integer, strings, objects, whatever but you have then at runtime to check if it really contains what you're expecting and then you can get the thing out of there and that exists for signals and also for properties. So the question was how to ensure that we don't have the vTable on the Rust side from the trade objects and on the GeoObject side for the virtual methods and as long as you don't really try hard to get the Rust vTable, you'd never have a Rust vTable so you would really have to explicitly create a trade object out of your things or store actual trade objects in your private state and then make use of that. It's not really different to other Rust code, you really have to do it explicitly. Okay, I guess then we are done for today.