5 ms·
I'm using Free Pascal for my 3D game engine (recent-ish screenshot[0]). For me there is really two main simple reasons: 1. Lazarus. A game engine is -waaay- mo
by badsectoracula 3y ago
I'm using Free Pascal for my 3D game engine (recent-ish screenshot[0]). For me there is really two main simple reasons:
1. Lazarus. A game engine is -waaay- more than just a 3D engine with the tools being a very important aspect. Lazarus and LCL provide a rich and well featured WYSIWYG RAD IDE and framework for making desktop applications. As a bonus Lazarus as an IDE (even ignoring the LCL framework) is very fast.
2. The language is decent and i know it. This is important because i'm not interested in learning a new language or framework or whatever while also making the engine, i want to focus on one thing. Also i have a lot of code written over the years to cherry pick (i started the project in 2020 but some of the code goes back to 2007-2008). And finally it has a strong aversion to breaking existing code - there is a culture of preserving backwards compatibility.
Free Pascal is far from perfect though and TBH i'm annoyed by some of its constraints - some of them being quite pointless too IMO. For example the language has three ways to create compound types: records, objects and classes. They are almost all identical, except each one of them has limitations that isn't found in others, e.g.:
1. Records. They are like C structs. With "advanced records" language submode (enabled more functionality in a backwards compatibility preserving manner) they can also have methods, properties and "management operators" allowing the compiler to insert calls when things enter or exit the scope (so you can e.g. implement smart pointers, RAII or whatever). However they do not have any form of inheritance or virtual functions. They are value types and as such can be put in the stack, heap and their can be part of another compound type.
2. Objects. They are basically records with inheritance and virtual functions, though they cannot have management operators or some other record-only functionality like a variant section (allowing, e.g. functionality similar to C unions). They are also value types and can be put in stack, heap and/or be part of another compound type.
3. Classes. They are objects with extra functionality, have extra the bells and whistles like dynamic/message-based methods, a published section that expose properties and attributes via RTTI, etc. However they are reference types - they can only be allocated on the heap[1] and as such cannot be put in the stack and cannot be part of another type (only references can be stored).
The thing is none of the above limitations have a technical reason - especially those between objects and records. In fact in the compiler source code all compound types are implemented with mostly the same code with some specialization checks here and there, it isn't like each of the above gets its own separate code path.
But this is far from the only limitation. Another is that there is no way to expose a readonly view of an instance - something like C/C++'s "const" - so you can't -e.g.- have a mutable collection as a private field in a class that is exposed as an immutable one directly, so the compiler knows it can generate code for directly accessing the underlying field but not allowing any modifications to it. The weirder - and misleading - aspect is that the language does have some ways of specifying readonly views in some cases - specifically function parameters can use "const" or "constref" (the latter ensures a value is passed by reference, the former lets the compiler decide) which disallows the value to be modified. However if an object is passed like that you can still make method calls that mutate it even though you cannot assign it to another value, which kinda makes the whole thing feel only skin deep.
On the topic of arbitrary limitations and types, there is also a weird limitation on properties: you can have property getters (and setters, but what i'll describe makes sense only for getters) be either fields or functions. For the former the compiler will generate code that'd be equivalent to accessing the field directly, for the latter it'd be a function call. When you define a property you also declare its type - but if you use a field as a getter it must much the type exactly, on the other hand you can have a function that returns a field as another type. This means that you cannot work around the "lack of const" limitation for collections by making a base class collection that only provides a readonly view and a subclass that allows mutation and then expose the mutable instance as its immutable parent class while letting the compiler generate code that access the field directly - you have to go through a function call and not only the compiler is very stupid at eliminating unnecessary calls, this also changes the RTTI data if you want this to be handled for -e.g.- serialization or via a GUI property editor.
Also when it comes to RTTI which it is still better than what you'd get via -e.g.- C++ (i.e. nothing), it has limitations like only allowing primitive types (so if you have a class for a 3D object and you want to expose its position to be edited via a property editor that uses RTTI to figure out what to edit or to have automatic serialization and deserialization you cannot expose a "Position" property of a "Vector3D" type directly with the RTTI specifying what that "Vector3D" means and instead you have to specify separate "PositionX", "PositionY" and "PositionZ" properties of "Single" type (which is a primitive type for 32bit floats). You can still have a "Position" property of a "Vector3D" outside of RTTI and you can still use a private field of "Vector3D" to store the position and then use getters and setters beneath the scenes to access the individual vector fields for the "PositionX/Y/Z" properties.
All of the above have the end results of using tons of -often inefficient- boilerplate to work around language limitations that in 99% of the cases have no reason to exist in the first place (aside from nobody implementing them - which would be fine, it is a free project, but the thing is the above are things the FPC developers refused to acknowledge as problems - in fact i personally tried to submit a patch that implemented the property case).
As an example, in my 3D game engine i have a "dynamic array" generic (an incredibly common container you'd find in game engines) that i want to be able to allocate on the stack, heap or part of another object, i want it to separate allocation from number of elements (the language itself has dynamic arrays but they always allocate the same number of elements as the array has which means when you insert items you do a lot of allocations and deallocations - that not only fragments the heap but the FPC memory manager isn't that fast in the first place) and of course being able to automatically finalize (i.e. destroy) any managed objects (e.g. a dynamic array of strings with string being copy-on-write "smart" objects defined by the language itself).
In order to get the "allocate on stack or part of object" i couldn't use classes, only objects or records. Because i wanted to expose it directly but without allowing mutation, i really could only use objects since records to not support inheritance. However objects do not support management operators so i'd have to explicitly initialize and finalize the objects. So the workaround there was to store the data in a separate type that is a record with management operators and specify a single field in the (immutable / readonly / base) object of that separate type so that when the object goes out of scope or is finalized itself, its contents are also finalized (remember that i mentioned how this is an arbitrary limitation? The compiler already knows this can happen and handles this, otherwise this workaround wouldn't work!). And also because i cannot have the mutable field accessed directly as its immutable/readonly base type, wherever i use it and i want to expose it i have to use a method getter which returns a pointer to the underlying field (but as the immutable type) - this is needed because otherwise the getter would return a copy of it (remember these are value types).
[0] https://i.imgur.com/zzIH4dl.png https://i.imgur.com/zzIH4dl.png
[1] (there are workarounds like using a proxy array with in-place initialization but they are very bug prone so i ignore them as they are not practical)
- badsectoracula 3y ago(continued) These are some of the annoyances with Free Pascal as the language, but there are others like how a unit (think module in other languages) has an "initialization" and "finalization" section that is executed on startup and shutdown respectively. This is good and useful thing, however the problem is that the initialization also has implicit code that zeroes out any global memory in that unit. This means that if a unit A's initialization code runs a function from unit B that accesses some global in unit B but the initialization code in unit B isn't called yet, any modification made by unit A calling that method would be lost ending up with weird bugs. The order the initialization code runs depends on the order the units are used in a program but if there are circular dependencies this order can be whatever the compiler encounters first. The workaround for this is to make sure the program (not any unit) that you compile has in its "uses" section the units that you don't want them to modify and make sure they do not have any dependencies themselves. Found that the hard way because in my units the initialization section contains RTTI registration calls that were later clobbered by the RTTI registry unit somehow getting initialized after some units that registered some types - this ended up with some assets not being loaded in the engine because the registry couldn't find the type of some serialized objects (because these types happened to be registered before the registry's own initialization code zeroed out its globals and erased any registered types). The obvious fix would be for the global zeroing out to happen before any initialization section code is executed (and that only in systems which actually need it), but for whatever reason FPC doesn't do that. On the positive side the compiler is relatively fast - it takes about a couple of seconds to build the full engine + editor on my (now 5 year old) PC. And while there are issues like i mentioned above, the positives (which include the fast IDE, rich framework - and also i had a much easier time when trying to contributing to Lazarus - fast compiler... and the fact i already have a lot of FP code and know the language) outweigh the negatives. It isn't like there are many alternatives anyway, especially when it comes to something like Lazarus (of which the only potential alternative i can think of - which i haven't explored much - is Qt Creator but i find Lazarus both the better IDE and LCL easier to work with than Qt, not to mention how FPC is waaaaay faster than G++ or Clang++).
- deleted 3y ago[deleted]