systems + GENERATED_BODY
This commit is contained in:
parent
76b6833c44
commit
85937c65b6
150
content/programming/blog/systems.tree
Normal file
150
content/programming/blog/systems.tree
Normal file
|
@ -0,0 +1,150 @@
|
|||
%% title = "systems are just a bunch of code"
|
||||
|
||||
% id = "01HV1DGFGNV3DXD8A3CW2J4RZP"
|
||||
- often enough I see people scared to dive deep into the internals of their favorite technologies
|
||||
|
||||
% id = "01HV1DGFGN1KJRA93GWZZ3159S"
|
||||
- because it looks scary, or they think it's not really gonna impact their lives too much.
|
||||
|
||||
% id = "01HV1DGFGNNZN0RR9DFV2G6EXM"
|
||||
- and people tend to form these grandiose noun phrases like _The Object System_, or _The Borrow Checker_, making it seem like there are impenetrable walls that guard these complicated pieces of software,
|
||||
and that only people worthy enough will be granted entry into them.
|
||||
|
||||
% id = "01HV1DGFGNHX9H39FFT4BTZ8RB"
|
||||
- _but great adventurers fear no dungeons, as the deepest dungeons often hide the most precious treasure._
|
||||
|
||||
% id = "01HV1DGFGN729RHYNZB7AS36TD"
|
||||
- over time I've been growing accustomed to _knowing my dependencies_.
|
||||
after all, how can I _depend_ on some code functioning correctly, if I don't even know how it works, or what to do to repair bugs in it?
|
||||
or even where to go to customize it to my liking?
|
||||
|
||||
% id = "01HV1DGFGNA306RSQW9NRV79YQ"
|
||||
- it's largely annoying to me that compilers ship without their source code, nor do they ship with debug symbols.
|
||||
it would be great if whenever `rustc` crashes on me while incremental-compiling the treehouse (and it does so pretty often),
|
||||
I was able to catch the crash with a debugger and step through the code to see what happened.
|
||||
|
||||
% id = "01HV1DGFGN2WE5RK5X31H5AFSP"
|
||||
- but alas, performance and size optimizations make that impossible.
|
||||
I just end up filing bugs in the compiler repo `OR` moving on with my life.
|
||||
|
||||
% id = "01HV1DGFGNJ0ZA14QQH40S5FNS"
|
||||
- I wonder what the world could have been, had we been compiling and optimizing software on users' computers rather than downloading ready-to-use, but opaque binaries.
|
||||
|
||||
% id = "01HV1DGFGN5CAV3RP4GXASQVQJ"
|
||||
- I imagine a world where I can tell the computer "I'd like to debug this crash" after the compiler crashes, and it will download the relevant sources and let me single-step through the
|
||||
code to see what happened.
|
||||
|
||||
% id = "01HV1DGFGNY3FGEKY70D6RMV22"
|
||||
- and then I could edit the source code and submit the patch *right where I'm standing* rather than having to go through complicated and lengthy processes of cloning repositories,
|
||||
bootstrapping, reproducing, and such.
|
||||
*imagine fixing compiler bugs on real codebases rather than having to come up with isolated, reproducible examples first.*
|
||||
|
||||
% id = "01HV1DGFGNXJJX0BMMM3KQ42M0"
|
||||
- I know how hard this is to achieve in practice, which is precisely why I don't use Gentoo.
|
||||
|
||||
% id = "01HV1DGFGN8WEWY1CVV4HNNBQ4"
|
||||
- package managers like Cargo make using someone's code incredibly easy, and that's really cool.
|
||||
we should be encouraged to share reliable, well-tested code instead of reinventing half-baked solutions every time we need a thing.
|
||||
|
||||
% id = "01HV1DGFGN3V7WTM1FQE8GC92D"
|
||||
- but I think at the same time they make understanding someone else's code kind of an afterthought.
|
||||
|
||||
% id = "01HV1DGFGNVD0BPN0S9WCGQY61"
|
||||
+ to integrate a library into your C++ project you usually have to browse through their CMake build files to figure out how they name their build targets.
|
||||
and while doing that, through cursory glances at `CMakeLists.txt`, you gain knowledge of what knobs can be tweaked in the project, how it _builds_.
|
||||
you may even stumble upon a list of source files, which can give you clues as to the underlying architecture.
|
||||
|
||||
while this can hardly be called _understanding_ the code, at least it gives you a _peek_ into how it's structured.
|
||||
|
||||
% id = "01HV1DGFGNW44T5FGJ7W4AT85M"
|
||||
- (and I'm not defending C++ here, I think its dependency management has a terrible UX in the long run and makes sharing code needlessly hard - but like all tools, it has its strengths and weaknesses)
|
||||
|
||||
% id = "01HV1DGFGNGDB48GB1268R6DB5"
|
||||
- Cargo on the other hand makes it needlessly difficult to _poke_ at someone's code.
|
||||
|
||||
% id = "01HV1DGFGNBJZX522X5EBMC6XM"
|
||||
- when vendoring code into C++ projects, you make the build system treat your dependencies as _just another piece of code in your code base_,
|
||||
which means you can insert debugging code wherever you please, and the build system will happily rebuild it.
|
||||
|
||||
% id = "01HV1DGFGNJFFN73F5FF49V3YS"
|
||||
+ Cargo treats dependencies as immutable, which means that a given version of a package only compiles _once_ per your project.
|
||||
you can't go poking at the package's files for debugging purposes - you can't insert `dbg!(x)` expressions where you need them, which is really annoying.
|
||||
the only way I know of is to use [overrides](https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html), but compared to editing an already vendored dependency, those are quite cumbersome to set up...
|
||||
|
||||
% id = "01HV1DGFGNE4REYV9QKV3494AM"
|
||||
- you need to clone the repository at the correct version (I don't even remember the Git command to clone a repository at a given tag)
|
||||
|
||||
% id = "01HV1DGFGNQ54Q71EB7W7F7R80"
|
||||
- which means opening a separate terminal, `cd`ing into your folder with repositories
|
||||
|
||||
% id = "01HV1DGFGNX61NAMNGSMWSMX5W"
|
||||
- also adding the repo as a project in your IDE
|
||||
|
||||
% id = "01HV1DGFGN41KWVQX8J9JG3E9H"
|
||||
- then you need to edit `Cargo.toml` to override the dependency
|
||||
|
||||
% id = "01HV1DGFGNGXC4WBYQ1MZT5HMK"
|
||||
- have fun typing in that full path `/home/daknus/Repositories/cool-library`
|
||||
|
||||
% id = "01HV1DGFGN5X57Z807GMS3F07Q"
|
||||
+ *and* don't forget to revert the changes to `Cargo.toml` once you're done.
|
||||
unlike editing a vendored dependency, which will appear under a very visible path in Git, it's pretty easy to forget to check your `Cargo.toml` diff before staging/committing changes.
|
||||
I mean, I edit my `Cargo.toml` all the time, adding in libaries and stuff.
|
||||
so why would I look at every single change?
|
||||
|
||||
% id = "01HV1DGFGN0DAMASGQD64XXTRK"
|
||||
- *and* before you go at me and say "bah you should be reviewing all changes that end up in your codebase"
|
||||
|
||||
sigh.
|
||||
|
||||
listen. I'm not gonna review all the garbage I commit into my personal website, alright.
|
||||
I value fast prototyping over having a clean Git history in this particular case.
|
||||
|
||||
% id = "01HV1DGFGN6P3BDXEXZEB97GY2"
|
||||
- don't forget that overriding dependencies doesn't always work - if the resolver cannot resolve your override, it will not build your project.
|
||||
|
||||
% id = "01HV1DGFGN72Q12G1AKVGEXRXV"
|
||||
- most of the time the more reliable approach ends up being editing the `workspace.dependencies` entry for your dependency, and changing it to use a `path` instead of a `version`.
|
||||
|
||||
% id = "01HV1DGFGNPWTRV8W582JRPEH2"
|
||||
- this all sounds automatable, but it's pretty annoying nevertheless that the basic functionality of _poking into one of your dependencies_ is hidden away under layers of caching and patching and immutability and stuff.
|
||||
|
||||
% id = "01HV1DGFGNC1JV9ZSJAMQKZ61W"
|
||||
- seriously I just wanna insert a `dbg!(x)`, how hard could it be?
|
||||
|
||||
% id = "01HV1DGFGNWWQ8XX2X67STEC8B"
|
||||
- as an example, when I first started working with Unreal Engine, everything seemed like magic.
|
||||
|
||||
% id = "01HV1DGFGNJGMRWJZZ9Q7WYPFE"
|
||||
- like [how in the world does the `GENERATED_BODY()` macro manage to expand to different things depending on which class or struct it's declared in?][page:programming/technologies/unreal-engine/generated-body]
|
||||
|
||||
% id = "01HV1DGFGN8MZB8YTGB5SFP577"
|
||||
- but the more you poke at it, the more you look at definitions, the more you look at the build tools, the less magical it all seems.
|
||||
it's all just code-generating syntax sugar!
|
||||
|
||||
% id = "01HV1DGFGNVRX5C3TST00V58DK"
|
||||
- **everything is code! *there is no magic*!**
|
||||
|
||||
% id = "01HV1DGFGN569R7MEE9FBXRF7H"
|
||||
- there are no walls blocking you from looking at the code. the grandiose noun phrases are misleading!
|
||||
|
||||
% id = "01HV1DGFGNE1CTAX01DE61GWC5"
|
||||
- UnrealBuildTool is just a bunch of C# files. and the *Gameplay Ability System™* is likewise just a bunch of C++ sources.
|
||||
why can't we come up with more inviting names?
|
||||
|
||||
% id = "01HV1DGFGN129VY5FHHF4GC4Z5"
|
||||
- grandiose noun phrases sound so hostile. *but it's all just code.*
|
||||
|
||||
% id = "01HV1DGFGN96CPWYMJ14EWJJR2"
|
||||
- most of the time not knowing your software is largely fine - not everyone has the time to deeply inspect a piece of software, deciphering functions, consulting manuals, asking the authors (when possible.)
|
||||
|
||||
% id = "01HV1DGFGNJYYN7SKYGCFDXN3M"
|
||||
- what I'm saying is that we should be encouraging more engineering practices and tools that enable us to inspect and poke at our dependencies _when we need it_.
|
||||
|
||||
% id = "01HV1DGFGN0QE49S9VBGVYVZY9"
|
||||
- _don't fear code_; respect it like it's your [Holy Mountain](https://noita.wiki.gg/wiki/Holy_Mountain).
|
||||
a place to tinker with stuff, and occasionally wreak havoc, anger the fuck out of the gods, and let them kick your ass with an `EXCEPTION_ACCESS_VIOLATION` or another `Segmentation fault (core dumped)`.
|
||||
and then [Steve](https://noita.wiki.gg/wiki/Stevari) kills you but you start another run anyways because this game of chasing bugs and perpetually improving software is just too damn addicting
|
||||
|
||||
% id = "01HV1DGFGN2EGFPD48WA8XZ34Z"
|
||||
- next time you encounter a crash in some library you're using, try _stepping into it_ with your debugger. you might find some real gems in there.
|
|
@ -14,6 +14,11 @@
|
|||
% id = "01HP1FESY5WVJG4X80AZ4ZBX5D"
|
||||
- ### random but cool things
|
||||
|
||||
% content.link = "programming/technologies/unreal-engine/generated-body"
|
||||
id = "01HV1DGFHP6GB268MDGGDXMR12"
|
||||
+ how does `GENERATED_BODY()` work exactly?
|
||||
|
||||
% content.link = "programming/technologies/unreal-engine/fixes"
|
||||
id = "01HP1FESY5ZS6YTZXA8QTT5V1Z"
|
||||
+ data validation quick fixes
|
||||
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
%% title = "how does GENERATED_BODY() work?"
|
||||
|
||||
% id = "01HV1DGFHMD7KNNX3FWS6R11SF"
|
||||
- the `UCLASS()`, `USTRUCT()`, and other reflection macros - but especially the `GENERATED_BODY()` macro might seem a bit magical the first time you see them.
|
||||
|
||||
% id = "01HV1DGFHM71ZD6CJY77N0PA8X"
|
||||
- the thing about it is that it defies the usual rules of how C++ macros work.
|
||||
let's have a look at it together.
|
||||
|
||||
% id = "01HV1DGFHM75BEC4B54MSBQ3P9"
|
||||
- _what does it even expand to?_
|
||||
|
||||
% id = "01HV1DGFHM2SAR1JMV8BAGDNE6"
|
||||
- try looking at what your IDE's autocomplete suggests -
|
||||
from a cursory glance at the symbols available in a `UCLASS()`-declared class, you will notice there are a few that are non-standard ones, such as `StaticClass()` or `Super`.
|
||||
this is what the `GENERATED_BODY()` macro ends up expanding to after all the preprocessing is done.
|
||||
|
||||
% id = "01HV1DGFHMZ4F2DYDDJXXJXX63"
|
||||
- but the thing is that `Super` is a typedef to the parent class - how does it know the parent class without passing it in as a macro argument?
|
||||
|
||||
% id = "01HV1DGFHMG2FFHTC9EA5NXB78"
|
||||
- and `StaticClass()` returns a different value for each class. this would require _knowing_ the class beforehand - how does it know?
|
||||
|
||||
% id = "01HV1DGFHMDFMWY2FWRHN3NGAX"
|
||||
- the thing is - it doesn't. `GENERATED_BODY()` by itself is incredibly stupid:
|
||||
```cpp
|
||||
#define BODY_MACRO_COMBINE_INNER(A,B,C,D) A##B##C##D
|
||||
#define BODY_MACRO_COMBINE(A,B,C,D) BODY_MACRO_COMBINE_INNER(A,B,C,D)
|
||||
#define GENERATED_BODY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY);
|
||||
```
|
||||
let's disassemble it piece by piece.
|
||||
|
||||
% id = "01HV1DGFHM4BW1FXVX3AMJYR91"
|
||||
- `BODY_MACRO_COMBINE` is just a macro combining four identifiers together. no magic here.
|
||||
|
||||
% id = "01HV1DGFHM29ENZ6HE36B2SP92"
|
||||
- so `GENERATED_BODY` combines the identifiers `CURRENT_FILE_ID`, `_`, `__LINE__`, and `_GENERATED_BODY`.
|
||||
|
||||
% id = "01HV1DGFHM8JH5RMTV79P8HRDV"
|
||||
+ `CURRENT_FILE_ID` is a preprocessor macro defined by the UnrealBuildTool for each file.
|
||||
for simplicity's sake, let's assume it's the filename with dots replaced by underscores.
|
||||
for instance, `GameplayAbility_h`.
|
||||
|
||||
% id = "01HV1DGFHM6XQ68XJPART8TND3"
|
||||
- the actual form it seems to take is `FID_{Path}` with `{Path}` being the file path relative to the project root directory, with slashes and dots replaced with underscores.
|
||||
for:
|
||||
```
|
||||
Engine/Source/Runtime/Engine/Classes/Engine/Blueprint.h
|
||||
```
|
||||
the file ID is:
|
||||
```
|
||||
FID_Engine_Source_Runtime_Engine_Classes_Engine_Blueprint_h
|
||||
```
|
||||
I haven't inspected the UnrealBuildTool/UnrealHeaderTool sources though, so there may be more to it.
|
||||
|
||||
% id = "01HV1DGFHMVS11W47KWXJC7TY4"
|
||||
- `_` is just an underscore. nothing magical here.
|
||||
|
||||
% id = "01HV1DGFHMPC27J2JN30PRGQSF"
|
||||
- `__LINE__` is a standard C++ macro which expands to the current line number.
|
||||
|
||||
% id = "01HV1DGFHMAEZV26CE77X5AXYF"
|
||||
- and `_GENERATED_BODY` is just an identifier.
|
||||
|
||||
% id = "01HV1DGFHM2Q8EC8EMKN0HMXTB"
|
||||
- therefore for a simple file, let's call it `MyClass.h`:
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
#include "UObject/Object.h"
|
||||
|
||||
#include "MyClass.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class UMyClass : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
```
|
||||
|
||||
after expanding the `GENERATED_BODY()`, we'll get this:
|
||||
|
||||
```cpp
|
||||
// -- snip --
|
||||
|
||||
UCLASS()
|
||||
class UMyClass : public UObject
|
||||
{
|
||||
MyClass_h_10_GENERATED_BODY
|
||||
};
|
||||
```
|
||||
|
||||
% id = "01HV1DGFHM78BW738ENHCN0ASF"
|
||||
- and this identifier is declared as a macro in the UnrealHeaderTool-generated `MyClass.generated.h` -
|
||||
and expands to a bunch of declarations, including the declaration of `Super` and `StaticClass`, as well as constructors if they're not already declared.
|
||||
|
||||
% id = "01HV1DGFHMFZNP6S3E1YNC8QH7"
|
||||
- you can even inspect the source code of `.generated.h` files yourself, by <kbd>Ctrl</kbd>+clicking on them (at least in Rider. I haven't tested Visual Studio.)
|
||||
|
||||
% id = "01HV1DGFHMP6ZP6N4WNWPTT04D"
|
||||
- that's all there is to it.
|
||||
incredibly simple, cursed as heck, yet super effective.
|
|
@ -10,6 +10,15 @@
|
|||
|
||||
[read][page:programming/blog/tairu]
|
||||
|
||||
% id = "01HV1DGFHZ65GJVQRSREKR67J9"
|
||||
- I've been thinking recently how cool it is to be able to single-step into Unreal Engine's source code and edit it while you're working with it, and how uncool it is that I can't do the same thing easily in the Rust world.
|
||||
|
||||
after all, aren't we just dealing with a bunch of code running on the computer? why not let me poke at it?
|
||||
|
||||
### systems are just a bunch of code
|
||||
|
||||
[can *you* can read other people's code?][page:programming/blog/systems] [bonus: dismantling Unreal Engine's GENERATED_BODY][page:programming/technologies/unreal-engine/generated-body]
|
||||
|
||||
% id = "01HTWNETT2S5NSBF3QR4HYA7HN"
|
||||
- last night I couldn't sleep because of type theory. in the process of trying to write down my thoughts, I ended up discovering a class of types which, to my knowledge, no language implements.
|
||||
|
||||
|
|
Loading…
Reference in a new issue