When designing a new SDK for Firefly Zero, we follow these principles:
- Reduce type convertions. Try to use the same integer type everywhere. WebAssembly is 32-bit system, so in Rust it will be
i32
. In Go,len
and slice indexing useint
, so that’s the best “default” type. If the language supports generic constructors, consider using it to automatically cast 8-bit and 16-bit integers to the default integer type. Using 8-bit or 16-bit integer in some types, likePad
, would potentially reduce the size of heap allocations, but most of the time these definitions never leave the stack, and in WebAssembly on the stack everything takes at least 32 bit. - Provide type casts. If the language provides custom type casting, define it for converting
Pad
toPoint
(and back),Pad
toDPad
,Canvas
toImage
,Style
toLineStyle
(and back), etc. - Provide custom types. While
load_file
can return a raw byte array anddraw_image
can accept any byte array, it’s good to provide customFile
andImage
types (and other custom types too whenever is possible). Even if the language doesn’t provide nominal type safety, a definition likeenemy: Image
is much more descriptive thanEnemy: []byte
. - Host calls are functions. You can optionally provide
Line
type withdraw
method for the user’s convenience. However, there must be always adraw_line
function, like for any other WebAssembly host call. The idea is that host calls are side-effects and that’s how we make it explicit. - Keep the host functions names. If the host function is called
draw_line
, the SDK function wrapping it must be calleddraw_line
(ordrawLine
, orDrawLine
) too. Don’t call itrender_line
or anything else. The called host function names are visible in the runtime error messages and infirefly_cli inspect
, so it should be easy for the user to map it to the SDK function calls in their code. - Follow the language style guides. Follow the naming convention and all the best practice. If there are several competing standards, pick the most commonly used one. Use all possible linters and code formatters.
- Keep modules flat. There should be only 2 public modules:
firefly.audio
with everything related to audio andfirefly
with everything else. Even though the host runtime defines modules likegraphics
,misc
, and more, the SDKs’ public API shouldn’t reflect that structure. Make it fast and easy for users to find and import all they need. - Avoid allocations. It should be possible to use SDK to write allocation-free apps. For example, in Go,
ReadPad
returns(Pad, bool)
instead of*Pad
so it doesn’t escape on the heap. In case of reading a file, most users will want it the file be automatically allocated with the correct size, so you should make both possible. For example, the Go version ofLoadFile
accept an optional buffer and if it isnil
, a new one is automatically allocated. Or in Rust version, there are two implementations:load_file
andload_file_buf
. - Optimize (for size). You should provide a simple to use API but it must never sacrifice binary size or performance. If someone wants to push the limits of what Firefly Zero can do, they should be able to do that without bypassing the official SDK. However, in many cases, the compiler is smart enough to optimize out unused code branches, so always validate your assumptions.
- Keep the arguments order. The host functions follow strict rules for arguments order. In particular, drawing functions always accept
Point
first andStyle
last. Your wrappers should follow the same order. - Use 32-bit float. 32-bit float arithmetic is much faster on the device than 64-bit one.
- Provide helpers. Don’t stop just at the host function wrappers. Provide everything that a typical game might need. For example, arithmetic operations for
Point
, radial coordinates forPad
, or optional integration with a popular 2D math library.