12 ms·
Exploring Polymorphism in C: Lessons from Linux and FFmpeg's Code Design (2019)
- jdefr89 2y agoWhat is with the incorrect function declarations? I see: int (func)(). Maybe you meant: int * (*func)(void)? Don't mean to be pedantic. Just wanted to point it out so you can fix it.
- deleted 2y ago[deleted]
- KerrAvon 2y ago> The interface type in golang is much more powerful than Java’s similar construct because its definition is totally disconnected from the implementation and vice versa. We could even make each codec a ReadWriter and use it all around. This paragraph completely derailed me — I’m not familiar with golang, but `interface` in Java is like `@protocol` in Objective-C — it defines an interface without an implementation for the class to implement, decoupling it entirely from the implementation. Seems to be exactly the same thing?
- mananaysiempre 2y agoThe difference between Go and Java is that in Go a type need not declare its adherence to an interface up front—any type that has methods of appropriate names and signatures is considered to implement the interface, even if its designers were not aware of the interface’s existence. (This involves a small bit of dynamism in the runtime; easily cached, though, as the set of methods of a given type and the set of all interfaces are both fixed by the time the program runs.) Whether that’s a good thing depends on your design sensibilities (ETA: nominal vs structural).
- jeroenhd 2y agoFor those wishing Java had a similar feature, there's Manifold: https://github.com/manifold-systems/manifold/tree/master/manifold-deps-parent/manifold-ext#structural-interfaces-via-structural https://github.com/manifold-systems/manifold/tree/master/man... Manifold is a very interesting project that adds a lot of useful features to Java (operator overloading, extension classes, and a whole bunch more). I don't know if it's smart to use it in production code because you basically go from writing Java to writing Manifold, but I still think it's a fun project to experiment with.
- sitkack 2y agohttp://manifold.systems/ http://manifold.systems/ > Manifold is a Java compiler plugin, its features include Metaprogramming, Properties, Extension Methods, Operator Overloading, Templates, a Preprocessor, and more. Neat tool. It is like having a programmable compiler built into your language.
- kazinator 2y agoGNU C++ once had this feature; it was called Signatures. It was removed, though. A signature declaration resembled an abstract base class. The target class did not have to inherit the signature: just have functions with matching names and types. The user of the class could cast a pointer to an instance of the class to a pointer to a compatible signature. Code not knowing anything about the class could indirectly call all the functions through the signature pointer.
- pjmlp 2y agoNowadays you can do that with concepts.
- kazinator 2y agoI don't see how concepts can emulate signatures to the full extent that the target object can be manipulated as if it conformed to an abstract base, without any wrapper object being required to handle it. Without signatures, we have to use some kind of delegating shim which takes the virtual function calls, and calls the real object. It could be a smart pointer. With signatures, we don't use smart pointers, just "pointer to signature" pointers. However, I suspect those pointers had to be fat! Because, surely, to delegate the signature function calls to the correct functions in the target object class, we need some vtable-like entity. The signatures feature must generate such a vtable-like table for every combination of signature and target class. But target object has no space reserved in it for that table pointer. The obvious solution is a two-word pointer which holds a pointer to the object, and a pointer to the signature dispatch table specific to the signature type and target object's class. If we can use concepts to do this, with a smart pointer that ends up being two words (e.g. pointer to its own vtable, and a pointer to the target object), we have broken even in that regard.
- pjmlp 2y agoHere is an example then, assuming you mean this kind of abstrations, #include <iostream> using namespace std; template <typename T> concept Speaker = requires (T t) { t.speak(); }; class Duck { public: void speak() const { cout << "quack"; } }; class Dog { public: void speak() const { cout << "auau"; } }; class Cat { public: void speak() const { cout << "miau"; } }; template<Speaker T> void speaking_animal(const T& animal) { animal.speak(); cout << "\n\n"; } template<Speaker... T> void speaking_farm(const T&... animals) { auto space_adder = [&](auto creature) -> void { creature.speak(); cout << " "; }; (space_adder(animals), ...); } int main() { Duck duck; Dog dog; Cat cat; speaking_animal(duck); speaking_animal(dog); speaking_animal(cat); speaking_farm(duck, dog, cat); } Live example, https://godbolt.org/z/vPhf13xEh https://godbolt.org/z/vPhf13xEh
- JavierFlores09 2y agoThe funny thing about Java is that while its design is to be entirely nominally typed, the way it is implemented in the JVM is compatible with structural typing, but there are artificial limitations set to follow the intended design (though of course, if one were to disable these limitations then modeled type safety goes out of the window as Java was simply not designed to be used that way). One community which takes advantage of this fact is the Minecraft modding space, as it is the basis[1] of how modding platforms like Fabric work. 1: https://github.com/SpongePowered/Mixin/wiki/Introduction-to-Mixins---Understanding-Mixin-Architecture https://github.com/SpongePowered/Mixin/wiki/Introduction-to-...
- owlstuffing 2y ago>The difference between Go and Java is that in Go a type need not declare its adherence to an interface up front. Go can't declare adherence up front, and in my view that’s a problem. Most of the time, explicitly stating your intent is best, for both humans reading the code and tools analyzing it. That said, structural typing has its moments, like when you need type-safe bridging without extra boilerplate.
- duskwuff 2y agoYou can assert that your type implements an interface at compile time, though, e.g. var _ AssertedInterface = &MyType{}
- relistan 2y agoOne of the main uses for interfaces in Go is defining the contact for _your_ dependencies. Rather than saying your function takes a socket, if you only ever call Write(), you can take a Writer, or define another interface that is only the set of functions you need. This is far more powerful than declaring that your type implements an interface up front. It allows for things like e.g. multiple image libraries to implement your interface without knowing it, enabling your project to use them interchangeably. And as another commenter said, you can have the compiler verify your compliance with an interface with a simple (though admittedly odd looking) declaration.
- williamdclt 2y ago> It allows for things like e.g. multiple image libraries to implement your interface without knowing it That virtually never happens. Seriously, what would be the odds? It’s so much more usual to purposefully implement an interface (eg a small wrapper the writer thingy that has the expected interface) than to use something that happens to fit the expected interface by pure chance. It’s not a structural vs nominal problem but other, typescript is structural but has the implements keyword so that the interface compliance is checked at declaration, not at the point of use. You don’t have to use it and it will work just like Go, but I found that in 99% of cases it’s what I want: the whole point of me writing this class is because I need an interface implementation, might as well enforce it at this point.
- billfruit 2y agoSo golang supports 'duck typing'?
- theLiminator 2y agoI think in a static context, it's generally referred to as structural typing, but yeah.
- williamdclt 2y agoI don’t agree it’s a structural VS nominal difference. Typescript is structural, but it does have the “implements” keyword. Which makes a million times more sense to me, because realistically when do you ever have a structure that usefully implements an interface without being aware of it?? The common use-case is to implement an existing interface (in which case might as well enforce adherence to the interface at declaration point), not to plug an implementation into an unrelated functionality that happens to expect the right interface.
- cognisent 2y agoTypeScript doesn't require a class to use it, though, because it's structurally typed. All that "implements Foo" in this example does is make sure that you get a type error on the definition of "One" if it doesn't have the members of "Foo". If "Two" didn't have a "name: string" member, then the error would be on the call to "test". interface Foo { name: string } class One implements Foo { constructor(public name: string) {} } class Two { constructor(public name: string) {} } function test(thing: Foo): void { //... } test(new One('joe')); test(new Two('jane'));
- juwjfoobar 2y agointerfaces in Go are structural. Interfaces in Java are nominal and require immediate declaration of intent to implement at type definition.
- psychoslave 2y agoShouldn’t this be named phenomenal rather than structural? In both cases there is a structure assumed, but one is implicitly inferred while the other one is explicitly required.
- relistan 2y agoI think you’re making a joke, but in Go you get both. You can have the compiler enforce that you implement an interface with a simple declaration. Most people do.
- psychoslave 2y agoNo intended joke in that case, but it’s nice to have the feedback it might seen as if it was. I don’t know Go to be frank, just had a very shallow look at it once because of an interview, and apart big names behind it, it didn’t shine in any obvious way — but that’s also maybe aligned with the "boring" tech label it seems associated with (that is, in positive manner for those who praise it).
- layer8 2y agoThe difference is that in Go, an interface is assumed to match if the method signatures match. In other words, the match is done on the type structure of the interface, hence the “structural” designation. Nominal typing, on the other hand, considers that interfaces tend to be associated with important semantic requirements in addition to the type signature, and that mere type-structure matching doesn’t at all guarantee a semantic match. For that reason, the semantics are implicitly bound to the declared name of the interface, and the way for an implementation to claim conformance to those semantics is to explicitly bind itself to that name.
- social_quotient 2y agoI spend a ton of time in FFmpeg, and I’m still blown away by how it uses abstractions to stay modular—especially for a project that’s been around forever and still feels so relevant. Those filtergraphs pulling off polymorphism-like tricks in C? It’s such an elegant way to manage complex pipelines. e.g. ffmpeg -i input.wav -filter_complex " [0:a]asplit=2[a1][a2]; [a1]lowpass=f=500[a1_low]; [a2]highpass=f=500[a2_high]; [a1_low]volume=0.5[a1_low_vol]; [a2_high]volume=1.5[a2_high_vol]; [a1_low_vol][a2_high_vol]amix=inputs=2[a_mixed]; [a_mixed]aecho=0.8:0.9:1000:0.3[a_reverb] " -map "[a_reverb]" output.wav That said, keeping those interfaces clean and consistent as the codebase grows (and ages) takes some real dedication. Also recently joined the mailing lists and it’s been awesome to get a step closer to the pulse of the project. I recommend if you want to casually get more exposure to the breadth of the project. https://ffmpeg.org/mailman/listinfo https://ffmpeg.org/mailman/listinfo
- MuffinFlavored 2y agohow similar are the C abstractions in ffmpeg and qemu given they were started by the same person?
- variadix 2y agoI haven’t worked with ffmpeg’s code, but I have worked with QEMU. QEMU has a lot of OOP (implemented in C obviously) that is supported by macros and GCC extensions. I definitely think it would have been better (and the code would be easier to work with) to use C++ rather than roll your own object model in C, but QEMU is quite old so it’s somewhat understandable. I say that as someone who mostly writes C and generally doesn’t like using C++.
- shmerl 2y agoWhat's the reason for ffmpeg to use C, also historic?
- defrost 2y agoFabrice also wrote the Tiny C compiler, so very much his language of choice .. For those used to the language it was seen as "lighter" and easier to add OO like abstractions to your C usage than bog down in the weight and inconsistencies of (early) C++ https://bellard.org/ https://bellard.org/ https://en.wikipedia.org/wiki/Fabrice_Bellard https://en.wikipedia.org/wiki/Fabrice_Bellard
- glouwbug 2y ago*int (*encode)(*int); Why not compile your snippets? Heads up to the author.
- mlvljr 2y ago[dead]
- sitkack 2y agoI'd say at 20kloc of C, https://www.lua.org/ https://www.lua.org/ gets you as far up the Object Oriented tower as you want.
- quietbritishjim 2y agoThe article is about using OO techniques directly in C code. Lua is implemented in C but it's an entirely separate language. Does its implementation use OO techniques as part of its C source code? If not, then it's not really relevant.
- sitkack 2y agoI don't see how a distinction here is anything but semantically arbitrary. Transitively, it most definitely uses OO techniques. Furthermore, by having such a clean C ffi (in both directions) it allows for the weaving of the Lua based OO techniques back into C code.
- cbarrick 2y agoFor the record, this design pattern is called a virtual method table, or vtable. I'm surprised that this article never mentioned the term. C++ programmers will know this pattern from the `virtual` keyword.
- rzzzt 2y agoYou can take it a step further: - instead of setting the same function pointers on structs over and over again, point to a shared (singleton) struct named "vtable" which keeps track of all function pointers for this "type" of structs - create a factory function that allocates memory for the struct, initializes fields ("vtable" included), let's call it a "constructor" - make sure all function signatures in the shared struct start with a pointer to the original struct as the first parameter, a good name for this argument would be "this" - encode parameter types in the function name to support overloading, e.g. "func1_int_int" - call functions in the form of "obj->vtable->func1_int_int(obj, param1, param2)"
- drivebyhooting 2y agoIs this satire? That’s almost exactly the C++ way.
- relistan 2y agoIt’s not satire, it’s how you do full OO in plain C.
- actionfromafar 2y agoIt's overloaded - it is satire, but also, it isn't.
- rzzzt 2y agoExactly. Thank you for your time, I will and won't be here all week!
- 2y ago
- pjmlp 2y agoThe language is called Go. Other than that, yeah doing by hand what C++ and Objective-C do automatically.
- programmarchy 2y agoFor one, ffmpeg is 9 years older than Go. Plus, when dealing with video files a garbage collected language probably isn't going to cut it. C++ and Obj-C also feel overkill for ffmpeg.
- pjmlp 2y agoApparently someone has not read the article, otherwise you would have had understood my point about Go. Secondly, Apple and Microsoft, do just fine with Objective-C and C++ for their video codecs, without having to manually implement OOP in C.
- programmarchy 2y agoCoreVideo and CoreAudio are both implemented in C on Apple systems. There are higher level APIs like AVFoundation implemented in Obj-C/Swift, but the codecs themselves are written in C. Even the mid-level frameworks like AudioToolbox and VideoToolbox are written in C. I’m not as familiar with Microsoft but imagine it’s similar. Also the article doesn’t actually mention OOP. You can use polymorphism without fully buying into OOP (like Go does). The great thing about C is its interoperability, which is why it’s the go to language for things like codecs, device drivers, kernel modules, etc.
- pjmlp 2y agoI bet they are actually C++ with extern "C" public APIs. Additionally Metal is implemented in Objective-C, with Swift and C++ bindings.
- high_na_euv 2y agoWho would have thought that OOP could be useful!
- vkazanov 2y agoThis is not oop but polymorphism that is useful. And various forms of polymorphism are used in all kinds of programming paradigms. Also, this is a nice way to get the damn banana without getting lost in the jungle.
- jcelerier 2y agopolymorphism used that way is 100% the most traditional OOP possible ever
- vkazanov 2y agoYes, a similar approach to polymorhpism is supertypical for popular OOP-focused languages, along with other concepts.
- flykespice 2y agoisnt polymorphism inherently a subset of oop?
- vkazanov 2y agoNo, all popular functional languages have one flavour of polymorphism or the other. Or, in math speak, Oop means polymorphism but polymorphism doesn't mean oop.
- inopinatus 2y agoThis is an excellent pattern in C. The Dovecot mail server has many fine examples of the style as well e.g. struct dict dict_driver_ldap = { .name = "ldap", .v = { .init = ldap_dict_init, .deinit = ldap_dict_deinit, .wait = ldap_dict_wait, .lookup = ldap_dict_lookup, .lookup_async = ldap_dict_lookup_async, .switch_ioloop = ldap_dict_switch_ioloop, } }; defines the virtual function table for the LDAP module, and any other subsystem that looks things up via the abstract dict interface can consequently be configured to use the ldap service without concrete knowledge of it. (those interested in a deeper dive might start at https://github.com/dovecot/core/blob/main/src/lib-dict/dict-private.h https://github.com/dovecot/core/blob/main/src/lib-dict/dict-...)
- dividuum 2y agoSo does the good old Quake 2 rendering API. The game exported a bunch of functions to the renderer via refimport_t and the renderer in return provided functions via refexport_t. The only visible symbol in a rendering DLL is GetRefAPI_t: https://github.com/id-Software/Quake-2/blob/master/client/ref.h#L224 https://github.com/id-Software/Quake-2/blob/master/client/re... I remember being impressed by this approach, so I shamelessly copied it for my programming game: https://github.com/dividuum/infon/blob/master/renderer.h https://github.com/dividuum/infon/blob/master/renderer.h :)
- dfox 2y agoI somehow suspect that the reason why Quake2 does this lies in the legacy of Quake1 written in DJGPP. DJGPP supports dynamicaly loaded libraries (although the API is technically unsupported and internal-only), but does not have any kind of dynamic linker, thus passing around pair of such structs during library initialization is the only way to make that work.
- jamesfinlayson 2y agoPretty sure Half-Life does something pretty similar - all functionality between the game and engine is done via function pointer structs.
- cytocync 2y ago[dead]
- loph 2y agoMy recollection (which could be rusty, it has been >30 years) is that the Motif API, coded in C, implemented a kind of polymorphism.
- codr7 2y agoNo discussion about polymorphism in C is complete without mentioning this macro: https://stackoverflow.com/questions/15832301/understanding-container-of-macro-in-the-linux-kernel https://stackoverflow.com/questions/15832301/understanding-c...
- brcmthrowaway 2y agoDoes ffmpeg support SVE?
- favorited 2y ago> for instance, Linux handles network socket, special files (like /proc/cpuinfo) or even USB devices as files. This is a powerful idea that can make easy to write or use programs for linux since we can rely in a set of well known operations from this abstraction called file. Benno Rice gave a fantastic talk a few years ago called "What UNIX Cost Us," which he starts off by showing how to write some USB device code in macOS, Windows, and Linux. It only takes a few minutes to demonstrate how pretending that everything is a file can be a pretty poor abstraction, and result in far more confusing code, which is why everyone ends up using libusb instead of sysfs. https://www.youtube.com/watch?v=9-IWMbJXoLM#t=134s https://www.youtube.com/watch?v=9-IWMbJXoLM#t=134s
- DarkUranium 2y agoThe same is visible in having to parse a bunch of Linux's more complex of the /proc entries, vs. simply using syscalls in (say) FreeBSD. "Everything is a file" is not a bad abstraction for some things. It feels like Linux went the route of a golden hammer here.
- favorited 2y agoThat's the gist of his whole talk – that doing things "the UNIX way" (which can be defined to various degrees of specificity) has been cargo culted, and that we should reexamine whether solutions that were pragmatic 50+ years ago is still the best we can do. The specific reason I mentioned it was because his initial example was about how much more ceremony and boilerplate is needed when you need to pretend that USB interfaces are actually magic files and directories.