gdjn
allow use of Godot 4 with a civilized1 scripting language, viz. Janet.
building
dependencies
- build-time
- runtime
- godot ≥ 4.3
license
the content of this repository is made available under the terms of the GNU AGPLv3 except where otherwise noted. the full legally-binding english-language text of this license may be found in the root of this repository in the file license.en
.
about
gdscript is a gibbering aberration from beyond the stars; this is known. however, as a poor fool determined to write a video game, using godot no less, i resolved to ameliorate my suffering by binding a better language to the engine. such is the depth of my madness.
“it’ll be simple,” i told myself. “i’ve poked at gdextension before; i know it’s a nightmare from the sewers of R’lyeh but i’ve already paid the toll in blood and pain to wrest loose its secrets. i have pretty good sense of what i’m doing this time. janet is well suited to this. wgah’nagl fhtagn. it will be easy”
yes, i was on drugs when i made this decision. i still am
a week or so later, i found myself implementing a new domain-specific IDL from scratch because that was less work than directly using the GDExtension api. the C side of GDExtension, it must be understood, was not designed for humans to use. it was intended as a scaffolding for godot-cpp, and only exists because C++ does not have a standardized ABI2 — C++ binaries can only talk to other C++ binaries by using C as a gobetween. no, this is not as crazy as it sounds. it’s crazier.
so GDExtension is designed (if i may use the word liberally) with certain notions fixed firmly in mind. chief among them is that the actual C-level calls will be thoroughly ensconced in a suffocating buffer of template hackery, which will employ dark arts and crafts to generate gobs and gobs of impenetrable C at compile time. in fact, GDExtension is so designed that it is basically impossible to use it directly without a code generation layer. unwilling as i was to use C++ (it’s against my religion), i wrote my layer from scratch, in janet.
this was not as bad as it could have been. janet has something magnificent3 in its stdlib that every language should have: a PEG module. the resulting program, tool/class-compile.janet
is under 1k lines! just barely. this tool is fired off by the makefile for the src/*.gcd
files, generating a header file gen/*.h
and an object file out/*.o
that implements the class described in the file. the care and feeding of this file format is described in the GCD section.
object model
godot uses a strongly javalike OOP system, which translates poorly to Janet’s prototype-based OO. gdjn therefore uses the following convention to translate between the two.
the engine executes the “compile-time” phase of a janet script once it is finished loading. it is the responsibility of the compile-time thunk to set up the local environment to represent a class using def statements. the abstract serves as a proxy for method calls. when a lookup is performed against a class proxy, the proxy inspects its attached environment and returns an appropriate object.
unadorned immutable bindings are treated as constants. values with the :field type
annotation are treated as object fields. when a new instance is created, space is allocated for a field of the appropriate type, and initialized to the value of the binding. public var
mutable bindins are treated as static variables.
unadorned functions are treated as static functions.
private functions (those declared with def-
) are available only within the class implementation. they are not exported as methods.
functions with the annotation :method
are treated as methods. when invoked, they are passed a self
-reference as their first argument.
function type signatures can be specified with the annotations :takes [...]
and :gives type
.
tables with the annotation :subclass
are treated as environment tables specifying inner classes. the macro subclass
should generally be used to maintain uniform syntax between outer and inner classes, e.g.
since the annotations are somewhat verbose, macros are provided to automate the process.
janet | gdscript |
---|---|
(def count 10) | const count := 10 |
(def count {:field int} 10) | var count: int = 10 |
(defn open [path] ...) | static func open(path: Variant) ... |
(defn open {:takes [:string] :gives :int} [path] ...) | static func open(path: String) -> int: ... |
(defn close :method [me] ...) | func close() -> void: ... |
GCD language
GCD is a simple IDL which is translated into much more complicated C4. it’s designed to make writing GDExtension classes as close to the native GDScript experience as possible, without the syntactic hanging offenses. you define the structure of the class using GCD, and write inline C to implement your functions. linemarkers are emitted properly so when you inevitably fuck it up, the compiler will be able to apportion the blame properly.
the C “lexing” is pretty robust, and as long as you aren’t doing horrible shit with the preprocessor (on the order of #define fi }
) it shouldn’t choke on anything you write. if you are doing horrible shit with the preprocessor however, you deserve everything you get.
tool/class-compile.janet
accepts the following (nested) forms:
(* ... *)
masks out a comment. these nest properly. normal C comments in a C block will be output with the rest of the text; gcd comments will be elided everywhere.(- ... -)
marks out inline documentation. this is not a comment! it is a syntactical element, allowed only in certain places (before a line item definition, after a return type, at the end of an argument specifier before the terminating comma/semicolon, or after an enum item). javadoc-style doc comments will be generated for the generated C, and it is my hope to also integrate this with godot’s online API documentation system, which i will grudgingly acknowledge is pretty fricken neat.class name (is|extends) base { ··· inner scope ··· };
define a class name inheriting from base. useis
when inheriting from a godot native class; useextends
when inheriting from a gdextension class defined in your program (inheriting from foreign gdextension classes will not work as expected either way). the necessity of this distinction is unfortunate; godot’s virtual method implementation is hilariously broken so we have to manually hack around that particular cesspit of incompetence(fn | impl) name(arg-type arg, ...) -> return-type { ··· inline C ··· };
defines a function. use fn for normal static-dispatch functions; use impl for virtual overrides. these are implemented in bizarrely diverging ways but you don’t have to care about that, because i did it for you. you owe me.- all arguments should be available by name as the proper godot type; e.g.
string-name
becomes agd_stringName
. - a pointer to the C-side storage struct for this class can be named by the token
me
. if you need the godot object, writeme -> self
. - for void functions, omit the
-> return-type
clause.
- all arguments should be available by name as the proper godot type; e.g.
var c-type as gd-type: v1, v2, ...;
defines a public class field (or global, at unit scope). c-type is the actual storage type of the object; omit it to have a sensible choice picked for you based on gd-type. gd-type is the godot type that this field will be presented as.var c-type: v1, v2, ...;
defines a private class field, accessible only to C.- note that you cannot use intializers here (yet, anyway); defining a reasonable syntax for a type that has to translate between C and the Godot type system is a decidedly nontrivial endeavor, and would probably have been more complicated than the rest of the syntax combined. for now, initialize your variables in
new { ··· inline C ··· }
.
- note that you cannot use intializers here (yet, anyway); defining a reasonable syntax for a type that has to translate between C and the Godot type system is a decidedly nontrivial endeavor, and would probably have been more complicated than the rest of the syntax combined. for now, initialize your variables in
(import | use) <header>;
transcludes a C system include into the unit.import
is the equivalent of using#include
in a header file;use
is the equivalent of using it in an implementation file.(import | use) "header";
transcludes a local C file into the unit.(import | use) { ··· inline C ··· };
dumps a block of C code into the header or implementation. use this to e.g. define utility functions.new { ··· inline C ··· };
defines the class constructordel { ··· inline C ··· };
defines the class destructor. remember that in godot-land refcounting is used, so sane RAII is kind of out of the question unless you’re imposing manual memory management on your poor end users- godot types: these are named using lisp-style tokens instead of the normal godot camelcase, both because i prefer it immensely and because it simplifies the more aggravating aspects of generating appropriate C from this trainwreck.
- primitive types:
string
,string-name
,int
,float
,packed-float64-array
etc. - container types:
array
,array[prim-type]
,dictionary
,dictionary[prim-key, prim-val]
- reference types: these are prefixed with the
ref
keyword, e.gref node-3D
,ref rich-text-label
etc. you can also write these as normal camelcase (ref RichTextLabel
) if you like, since i don’t have to care about the internal structure of the identifier at this point.
- primitive types:
note that semicolons are mandatory after every statement. whitespace is irrelevant except as a token-separator.
the interface for each unit is written to gen/*.h
. be sure to call the unit load function at the proper place and time.
manifesto against GDScript
gdscript is a gibbering aberration from beyond the stars
okay, this is, if anything, an understatement. it is possibly the single worst programming language i have yet (however begrudgingly) coded in. the tale of gdscript is one of fantastical hubris, of strife and horror and punishment handed down by the gods from on high like unto the sagas of old.
the creators of gdscript, you see (whom i shall henceforth address by the name of Abdul al-Hazred, for reasons i hope are readily apparent) originally wanted to use lua as godot’s scripting language, and made an attempt to do so (this was sensible) but they concluded (incorrectly) that lua’s facilities for threading were insufficient (i.e. they did not understand how to use the library correctly), so, having zero domain expertise or relevant experience, they resolved to devise their very own language from scratch. (this was not. i am put in mind of how Brendan Eich (piss be upon him) actually wanted to use a Scheme as Netscape’s scripting language — as i would have in his place! — but the bleating idiot suits wanted something they could (however dishonestly) slap the Java branding on, so instead the entire modern web is infested by unending reams of ECMAScript. more ECMAScript than there are stars. more ECMAScript than there is light)
so, knowing nothing of elementary PL theory, they hacked together a lurching screeching nightmare that is the antithesis to everything lua has ever been. shall i number the ways?
- lua has clean, consistent, unambiguous syntax that completely elides any need for syntactic handholding like brackets or (outside one niggling edge case) semicolons or line continuation sigils; gdscript reads the entrails of your indenting discipline and tries to infer structure therefrom (except for enums, for some unfathomable reason, which use C-style bracket blocks), which you inevitably have to hack your way around with the aforementioned semicolons and backslashes, and no matter what you do your code looks hideous. even this is inconsistent: the (woefully inadequate) match statement (“statement”, i will note, not “expression”; the ML weenies in the audience just flinched, and rightly so) absolutely requires whitespace and newlines to delimit its cases.
- lua has a simple but extensible prototype-ish object model. godot has a full-blown class system with all the elegance of GObject, all the efficiency of Java, all the consistency of an American politician, and all the expressive power of MUMPS. single inheritance, no interfaces, no templates, no generics, no macros, no mixins. in a game engine. lunacy, i tell you.
- lua is cleverly designed to use tables to implement namespacing. in point of fact, namespacing is emergent; it’s barely even part of the language. every lua chunk has its own clean namespace (or a shared global environment, an the host program will it so), and can choose what to bring in by binding modules to variables. gdscript barely supports namespacing at all, and in such a broken fucking way the native classes don’t even try to use it. inner classes are defective, with reduced autocomplete metadata from other units, and cannot be used as resources. i honestly cannot fathom how they fucked this up so badly. every godot class you create pretty much pollutes a global namespace, and the situation is so bad that addon writers follow a convention of not naming their classes at all, using path-based imports instead.
- lua is a reasonable functional language. you can use any FP paradigm you want, with a bit of effort. closures are lightweight, simple, and fun to use. there is no difference between an ordinary function and a lambda. gdscript, on the other hand, is a procedural language with lambdas hacked awkwardly in, with syntax that will make you long for the purity of ruby. a closure is a completely different thing from functions, an object of class Callable, and can only be invoked by using
Callable.call(args)
. and of course closures are completely untyped. - lua has a straightforward dynamic type system. not great, but it’s bearable. gdscript has a bizarre and spiritually unclean gradual typing system (i have always been suspicious of this concept and i am now firmly of the opinion that gradual typing is categorically a bug, not a feature.) it is not even internally consistent. “typed arrays” are not a type at all, but bizarre syntactic sugar for an array-specific runtime type tagging system. typed dictionaries were not even available until 4.4, which at the time of writing is still in pre-release. there are no generics, no templates. just
Variant
, and special-casing as far as the eye can see. this is almost the worst part, because to use godot at all, even from another language, you have to interact with this abortion somehow. i am going to have to wrap some kind of elaborate blast shield around it to make it usable from janet and i do not eagerly anticipate the process. - lua compiles down to bytecode. the reference implementation is not terribly impressive speed-wise, but luajit (an ideological variant based on pre-5.3 lua, not an implementation as it is often called) has performance characteristics bordering on C itself. gdscript does not compile at all; at best, it is pre-tokenized. yes, the entire damn thing is interpreted, at runtime. this alone should be enough to rule it out summarily as a game scripting language. i mean, for shame. fucking ruby is probably more efficient at this point (not that i suggest anyone pull an RPGMaker XP here).
this is not exhaustive. this isn’t even getting into the rat fuck screaming lunacy that is the underlying implementation. this is just the particular set of aggravations that came most immediately to mind when i set my cyberquill to electric paper. this is just a fucking taste, a faint flickering preview of how bad it gets. and people use this language for serious work. they claim to like it. the godot team blithely encourages people to learn gdscript, enthusing on its many virtues, as if they should not be too ashamed to show their faces where unsullied souls might see. the absolute mindless gall is beyond all reckoning. i would say that this is the fucking PHP fractal-of-bad-design fiasco all over again, except, like, I’ve used PHP. i prefer PHP to this monstrosity. yes, even ye-olde-worlde PHP, with register_globals
and all. i think it is fair to say that gdscript is to lua as modern American English is to Toki Pona or Japanese.
in broad conclusion, gdscript is not a programming language. it is barely a scripting language. it is a genus of tyranid. it is the diseased, misshapen, gurgling spawn of the unclean things that gnaw the roots of Yggdrasil in the depths beyond depths where gods and titans fear to tread, and sing searing nightmares into the sleeping souls of mortals. it is a thing demonstrative of the Kali Yuga, and foretells in near time the merciful descent of Shiva’s cleansing blade. it is the most intricate work of raw seething blasphemy yet wrought by mortal hands, if indeed mere mortals had anything to do with it.
you maybe have some idea of exactly how angry this thing makes me, simply from the fact that i was willing to endure the process of binding a whole new language to godot out of sheer fulminating spite.
i would like to make one final point, and if you take anything away from the above rant, wote it be this: programming language design is an art unto itself, and being a programmer — even a very good one — gives you fuck all credibility as a language designer. i don’t care how many languages you know, i don’t care how 10x your full stack is, i don’t care if you have 700 years of experience at Current Thing, and i don’t care how sharp your suit is. none of that has any bearing on your competence as a language designer. as an implementer, maybe. but leave the high concept stuff to your betters. do not under any circumstances design Yet Another Language, and work actively to shame those who do.
yes, i am being unironically elitist here: like making art and making war, language design is an intrinsically aristocratic pastime, and no amount of raw labor power or professional experience can suffice in the absence of a suitably disciplined, cultured, and talented mind. comrade stalin (peace be upon him) learned this the hard way about war, and if anything we moderns seem to be learning the same lesson even more slowly about the technical arts.
i myself am capable of reasonably competent language design, and that was a skill i developed over more than a decade of research and experiment and, mostly, plain old omphaloskepsis, things i would not have had time or energy for if i weren’t broadly sheltered from the material conditions of the 21st century by my status as a(n intermittently starving) artist, paid by human patrons rather than bugman employers. i do not even hold a candle to the true masters in this arena.