What is Fennel?
Fennel is programming language with Lisp-like syntax running on top of Lua. Since it keeps Lua semantics, code transcompiled from Fennel to Lua is very clean. For more information check official website: https://fennel-lang.org/
Why use Fennel in Defold?
While languages like Haxe and Typescript have been shown to work with Defold, those use ahead-of-time compilation, which breaks workflow in my opinion. Fennel can also transcompile to Lua in AOT fashion, which was described by @TheKing0x9 in his post.
Fennel provides another option and can be embedded as Lua module (single file) and compile code at run-time, which means less steps, when working in Defold. I focused solely on this.
Quick and dirty example
I translated Simple Linker example to Fennel, repository can be found here: GitHub - ajtos/defold-fennel-example
Fennel code in “fnlsrc” directory can be edited directly via Defold Editor, no additional steps are needed, just push Build button. File “main/board.script” is probably the most interesting one.
How it works?
Lets start with what didn’t work. This is idomatic example of embedding Fennel into Lua application from Fennel – Setting up Fennel
local fennel = require("fennel")
table.insert(package.loaders or package.searchers, fennel.searcher)
local mylib = require("mylib") -- will compile and load code in mylib.fnl
Second “require” for loading Fennel code won’t work with Defold. Defold tracks assets e.g. scripts attached to objects, Lua code loaded via “require”. In theory script above should load “mylib.fnl”, since instructions for loading modules are changed with insertion of custom rules into “package.loaders”. Yet Defold doesn’t know that we want to load “*.fnl” file, it is using simple regular expression for resource tracking and will complain about missing “mylib.lua” file.
Dummy “mylib.lua” (just empty file) could be created to trick Defold into building project, but this creates another problem. Defold doesn’t know about “mylib.fnl” and will strip that file from bundle, since it isn’t referenced anywhere.
Another approach that didn’t work, involved putting Fennel code directly into “mylib.lua” and inserting Fennel loader (with some modification) at the begging of “package.loaders” table. Problem here is that Defold compiles Lua files into bytecode and then launches the game. Since Luajit doesn’t know a thing about Fennel code yet, this will end up with build errors.
Current approach (simplified code):
local fennel = require("fennel")
local script = sys.load_resource("/fnlscr/fish.fnl")
local scriptcompiled = fennel.compileString(script)
loadstring(scriptcompiled))()
Custom resources specified in game.project can be used for storing additional data in Defold, in this case “/fnlsrc” directory. However those files can’t be accessed with usual function from Lua like “require”. Instead “sys.load_resources” function must be used and it returns string of loaded data.
This string is then compiled from Fennel to Lua code with fennel.compileString function. “loadstring(code)()” idom is then used for loading and running chunk in Lua virtual machine.
What was modified in fennel.lua?
Earlier I mentioned how Defold tracks dependencies in Lua scripts by parsing “require” function. This is problematic for fennel.lua, since this is amalgamated module containing multiple “require” functions, which don’t point to real files. This can be solved by using dummy approach mentioned before. Yet there seems to be a better way. Defold parser isn’t particularly sophisticated, so “require” calls can be changed to “_G.require” and this will avoid dependency resolution.
Even then fennel.lua modified this way won’t work out of box. Normal approach for amalgamated modules is populating “package.preload” table with references to functions in loaded chunk. This then tricks “require”, which should look into table and not search for real files. Problem is that “package.loaders” in Defold is stripped and contains only one function, which doesn’t even look at “package.preload”. So we need to create custom loader:
table.insert(package.loaders, 1, function(modname) return package.preload[modname] end)
I would like to thank @britzl here for providing solution, Defold developers were very helpful.
Issues/caveats?
- Script properties (go.property) must be used directly in script, so this means Lua code.
- Hot reloading doesn’t work, since Defold doesn’t track Custom Resources. Creating string with Fennel code directly in Lua script should work, but isn’t that nice either.
local script = [[
(fn is-striped [fish]
(or (= fish.type type-striped-v) (= fish.type type-striped-h)))
]]
local scriptcompiled = fennel.compileString(script)
loadstring(scriptcompiled))()
- By default function definitions in Fennel are local, use code like bellow if needed:
(fn _G.init [self])
- By default functions in Fennel return last value, this can create errors for functions like on_input in Defold
(you can put true/false at the end instead)(better solution in reply). - Fennel won’t work in editor scripts, since Editor uses luaj virtual machine, which is buggy (and seems unmaintained)
Questions?
If you managed so far, I would like to apologize for quality of my English writing, since this isn’t even my second language. Fennel code in linked example project isn’t the best either - I just used anitfennel (lua-to-fennel translator) and fnlfmt formatter, so it isn’t representative of Fennel in any way.
I believe that even at current state this is worth sharing and hopefully improvements can be made. Lack of hot reloading is kinda disappointing. Yet I feel that interoperability is better compared to other attempts at using non-native language with Defold and maybe will find use by someone, who is looking for alternatives to Lua in conjunction with Defold.