Support for Swift Runtime

Lua is great high level language, and C is great low level language, but how about something that meets in the middle?

With modern type-safety, deterministic memory management via ARC (no GC sweeps), async/await, propagatable error handling, protocol-oriented design, structs as first-class citizens, and great performance, Swift seems like a good fit for the platform.

Swift can automagically wrap C code, and because of this it's trivial to create Swift-native feeling wrappers around existing C libraries.

This might be useful: GitHub - swift-embedded/swift-embedded: Swift for Embedded Systems ✨

Any thoughts?

6 Likes

Related, a Rust interface to the SDK has already been made: GitHub - pd-rs/crankstart: A barely functional, wildly incomplete and basically undocumented Rust crate whose aim is to let you write Games for the Playdate handheld gaming system in Rust.

One thought about Swift is the runtime size. Would it go in the firmware (is there room for it?) Or in each game (like on macOS/iOS)?

Wouldn't have even thought to request this but as a day-job Swift dev YES PLEASE

1 Like

Cool! Because of achieving ABI stability a few years back, Apple no longer packages Swift dylibs with apps and instead is able to rely on the system's. I have no idea if that applies to the open-source Swift Foundation corelib.

1 Like

That's great! I'm out of touch :sweat_smile:

I was also looking in to this too. The swift runtime is quite memory heavy, sadly. It would probably fit in memory, but idk about creating anything useful.

As for the runtime's size itself, that depends. Since the runtime relies on other libraries, such as stdlib & unicode data it could shape up to be around 16-28mb, without any sort of optimization/cleaning of unused functions. Since Lua doesn't support unicode (right?) and if we want to keep Lua bindable in swift then you could compile the stdlib without unicode support and get an extremely small binary. You'd still have some issue with the stdlib not really being optimized for this CPU and some parts of it might not work outright- so you would need to patch this. Some included data structures like array may be unwieldy too. I know that the memory overhead is also noticeably different depending on how you program too. Not to mention that if included with firmware, there is now a fork of the swift compiler AND stdlib that must be maintained. So the way to go would be including it alongside the app, this also allows for any ABI changes (which do still happen!). Given a lean version of the runtime and stdlib you could probably fit it in a few megabytes, which is not wholly unwieldy.

The rust project linked above actually specifically addresses this problem too by building against an extremely lean version of the rust runtime.

I'd guess that the easiest thing to try would be fork the compiler and add ARM cortex-m support, which I don't think would be too much trouble, given that there is an existing LLVM backend for it. Then compile without the stdlib and see what happens. You could probably reference bits of the rust project to figure out bootstrapping and all that since it should be similar.

Tangentially related: while swift can automagically wrap C code, you do end up needing to create an actual idiomatic swift wrapper / library, which is additional overhead. No one wants to use swift like its C :wink:

Edit: upon searching a bit, this fork seems highly related: https://github.com/madmachineio/swift.

Edit 2: also interesting: https://dspace.cvut.cz/bitstream/handle/10467/82498/F8-DP-2019-Dragomirecky-Alan-thesis.pdf?sequence=-1

okay, I might explore this over the weekend.

Edit 3: A quick investigation of the first link mentioned by @gregc points to it being the most viable approach as well as the simplest. The repo has fallen a bit behind though and if you are Xcode 13 based you likely won't be able to get cross to build. cross is based on old tooling, which has since been moved to swift-tools-support-core and swift-package-manager respectively. I will take a crack at resolving this so we can at least compile bins to inspect. It appears the program size is actually quite small too. So this might actually work!

Edit 4: updated cross here: GitHub - ericlewis/cross: Utility for easy cross compilation of projects to bare metal
don't think it will work still, since spm is tied to swift version too (I think). toolchain has not been updated to 5.6, nor do I think it will be. Going through all the commits to try and rebase will be a bear of a task.

1 Like

Add my vote; Swift for Playdate would be a super interesting alternative, and getting it to work would be a righteous hack, if nothing else.

I spent some time looking at swift-embedded as well, and it seemed pretty promising:

  • he’s targeting much smaller chips — 2MB of storage and 100s of bytes of RAM
  • even matching Lua’s performance with a richer language would be a big win, and the compiler probably can do a lot better than that

A proof of concept based on the existing toolchain (and Swift 5.0) would already be super cool and useful. I don’t know enough about LLVM, the Playdate C tools, and probably ten other things to know where the hard problems are going to be.

On the other hand, figuring out how to effectively bridge the Playdate APIs to Swift seems like a fun challenge.

I plan to do some experimenting. It would be great if we could somehow pool our efforts.

I had a random shower thought this morning. We should be able to do at least 3 things today:

  • import pd_api.h to swift
  • export the event handler from swift
  • build a dylib and package into a pdx to run on sim

Swift can import C libs via SPM with nearly no trouble and we can extern swift functions to C via @_cdecl. There is also probably some linking work to be done, but with that we should be able to minimally build for simulator and use swift (I think). Of course this doesn’t solve the harder problem of needing to build for the thumbv7m arch, but does open a window to doing user land work whilst compiler work is being done. Alternatively, could build a small C bridge to swift in order to deal with mismatched externs that will most likely occur. This may be easier too, as we are back in the pd toolchain.

Yes a way to compile for arm64 / intel would be a fine target for now! Can we make an open source repo for this kind of work, or would that violate some license? It would be trivial to make a pure C SPM package and then we could get to work on making a swiftified wrapper package around it.

That is the plan, though I am not sure it is trivial. Trying to get it working now :smiley: I can set up a repo if it makes sense to. Will be on a plane soon with plenty of time.

I made a little headway getting something to compile. In case it's useful, here's what I have so far:

Put the following lines in a file called Playdate-Bridging-Header.h:

#define TARGET_EXTENSION 1
#include <pd_api.h>

And main.swift:

var savedAPI: PlaydateAPI?

func update(_: UnsafeMutableRawPointer?) -> Int32 {
    let playdate = savedAPI!

    playdate.system.pointee.drawFPS(0, 0)
    return 0
}

func myEventHandler(playdate: PlaydateAPI, event: PDSystemEvent, arg: UInt32) -> Int32 {
    savedAPI = playdate
    playdate.system.pointee.setUpdateCallback(update, nil)
    return 0
}

print("OK")

Compile:

$ swiftc -import-objc-header ./Playdate-Bridging-Header.h -I ~/Developer/PlaydateSDK/C_API/ main.swift
$ ./main
OK

That proves that the types kinda work; I was happy to even get this far.

Caveats:

  • The bridged types get pretty hairy. I tried a few things and drawFPS, being simple, was the first that actually compiled.
  • TARGET_EXTENSION has to be defined, or numerous types get ifdef-ed out and the whole thing dies. I don't know what it's meant to accomplish.

I guess the next thing to figure is how to get swiftc to spit out something the simulator can load, and how to get myEventHandler invoked?

2 Likes

Nice, this is great - I also just compilation but your approach is less hacky. Going to switch to this and see if I can get the swift bits exposed to C properly. I think that is going to be the hardest part because it's not just a simple "extern 'C'" like on other platforms. My version instead creates a proper swift library that can be imported. Currently, it is just this:

module Cpd [system] {
  header "/Users/ericlewis/Developer/PlaydateSDK/C_API/pd_api.h"
  link "pd_api"
  export *
}

A few problems though, this way is deprecated & I had to comment out the TARGET_EXTENSION stuff. I am about to board a flight but I think switching to a proper systemLibrary target will allow for passing the proper flag & then we can import the C lib in order to build out swifty api.

Less concerned with this now, more concerned with getting event handler called. Looking to these libs for some ideas too: GitHub - pd-rs/crankstart: A barely functional, wildly incomplete and basically undocumented Rust crate whose aim is to let you write Games for the Playdate handheld gaming system in Rust.

They handle the whole gamut, which we can’t until compiler support is fixed. But the way dylib works is hopefully quite similar.

I don’t think exposing the event handler directly is going to be possible, but we shall see. I can manage to get the swift code to link and load on simulator, but obviously it’s useless without anything connected.

Edit: looks like it's not being called correctly. Based on my experiments with c/swift interop - it appears that the only thing that needs to match is the symbol (I think) name. Even with mismatched definitions on c or swift side, it still works so long as the name matches. This leads me to believe that I am likely not linking correctly.

Spent some more time on this today and mostly learned that I don't really understand what SPM and swift build do. Based on what hasn't worked, my impression is that this path will indeed require building out a toolchain (like swift-embedded does) to get the linker to do something useful with the swift runtime and our entrypoint.

So, I might try cloning swift-embedded and hacking on it. This might be the quickest way to get to a statically-linked binary for the device; not so sure about the .dylib needed for the simulator (based on staring at what the C makefiles do.)

Alternatively, could stick with swiftc in combination with clang/gcc(?) to compile a minimal C wrapper and link with it. That seems like a shorter path to getting something working in the simulator with a current version of Swift, but might not get us closer to running on the device.

Neither of these exactly sounds like fun. Hopefully somebody has a better idea.

swift-embedded (or the madmachine forks of swift compiler) are basically the only ways towards running on device. I did a little hacking on swift-embedded just to get the cross tool working again, which wasn't too bad. But the toolchain being distributed isn't working and just getting that to work is important to the rest.

the dylib stuff might not be too hard, like I said - it is possible to generate an almost proper dylib. I just hadn't played much with linking.

actually appears that I can get the simulator to at least see my event handler. if I create one and expose it then build a dylib, it appears to get called. I know this by throwing a while loop in the actual function, which causes the sim to freeze. removing it... no freeze! To be even more sure, I did this:

// swiftc test.swift -emit-module -emit-library
@_cdecl("eventHandler")
public func eventHandler(_ playdate: CInt, _ event: CInt, _ arg: CInt) -> CInt {
  if event == 4 { // on pause event, hang.
    while(1 != 2) {
      // noop
    }
  }
  return 0;
}

Now, the sim should launch and work as normal until you press menu, then it will hang.

What this basically means is that the simulator is indeed calling the event handler function above. So I guess what is next is sticking the C lib in, and seeing if we can draw something.

Edit. We can draw something :smiley:

Props to @mossprescott for figuring out the swiftc thing I was lacking for the headers!

Mini-tutorial:

  1. mkdir swift-pd && cd swift-pd
  2. mkdir swift.pdx
  3. touch main.swift Playdate-Bridging-Header.h swift.pdx/pdxinfo
  4. put the following lines in swift.pdx/pdxinfo
name=Hello World Swift
author=
description=
bundleID=com.swift
imagePath=
pdxversion=10900
  1. put the following lines in Playdate-Bridging-Header.h
#define TARGET_EXTENSION 1
#include <pd_api.h>
  1. put the following lines in main.swift
var pd: PlaydateAPI!

func update(_: UnsafeMutableRawPointer?) -> CInt {
    pd.system.pointee.drawFPS(0, 0)
    return 1
}

@_cdecl("eventHandler")
public func eventHandler(_ playdate: PlaydateAPI, _ event: PDSystemEvent, _ arg: CInt) -> CInt {
  pd = playdate

  if event == kEventInit {
    playdate.system.pointee.setUpdateCallback(update, nil)
  }

  return 0;
}
  1. compile: swiftc -import-objc-header ./Playdate-Bridging-Header.h -I ~/Developer/PlaydateSDK/C_API/ main.swift -emit-module -emit-library -o swift.pdx/pdex.dylib
  2. run: open swift.pdx

Note: you will need to kill the simulator before each run.

2 Likes

A slightly more interesting program showing possible API improvements

var pd: PlaydateAPI!

func update(_: UnsafeMutableRawPointer?) -> Int32 {
  let leading = 5
  pd.drawFPS(x: leading, y: 0)
  pd.drawText("Hello World, Swift Edition!", x: leading, y: 15)
  return 1
}

@_cdecl("eventHandler")
public func eventHandler(_ playdate: PlaydateAPI, _ event: PDSystemEvent, _ arg: CInt) -> CInt {
  pd = playdate

  if event == kEventInit {
    playdate.system.pointee.setUpdateCallback(update, nil)
  }

  return 0;
}

extension PlaydateAPI {
  @discardableResult
  func drawText(_ message: String, x: Int, y: Int) -> CInt {
    self.graphics.pointee.drawText(message, message.count, kUTF8Encoding, CInt(x), CInt(y))
  }

  func drawFPS(x: Int, y: Int) {
    self.system.pointee.drawFPS(CInt(x), CInt(y))
  }
}

Edit: I now have a functional sort of swift package. can build no problem from swift build! Looking in to taking advantage of the new spm build plugins to stream line a bit. For now, it is a very simple build.sh to grab the dylib out and copy to the pdex.

Edit 2: Managed to get everything needed squished into a SwiftPM lib. Makes the public interface nicer. Sadly, no way around the swizzling, but keeps us away from C types :smiley:

import Playdate

@_dynamicReplacement(for: UpdateCallback)
func updateCallback() -> Bool {

  Playdate.shared.system.drawFPS(x: 0, y: 0)
  Playdate.shared.graphics.draw(.text("nice", x: 0, y: 20))

  return true
}

@_dynamicReplacement(for: EventCallback(event:))
func eventCallback(event: SystemEvent) {
  Playdate.shared.system.log("\(event)")
}

Alrighty, code time!

1 Like

Thanks, @ericlewis, this got me over the hump.

I had been building a .dylib in various ways but it seems if anything at all isn't right, the simulator dies with a mystifying error (for example, "Couldn't find pdz file main.pdz").

Following your lead pretty closely, I finally got it to run. And with that, I figured it was worth sharing, so I threw it up at GitHub - mossprescott/swift-pd: Bare-bones support for Playdate console apps in Swift. A work-in-progress..

And now I see you did, too :smiley:

1 Like

Yes I did! It didn't occur to me to use PDC as part of this like you did.

I was more focused on packaging and being able to use SwiftPM, which seems to be where we diverged (plus your readme is way better lol) the spm approach seems pretty sound, but there are two thing that need to be solved with it:

  • Finding the SDK path and using that in the module map for CPlaydate (how we get autogen)
  • Outputting the pdx just by running something like swift package simulator, which might be possible with the new build plugins for spm.

If those two things can be solved then it might be possible to use spm entirely, without any special setup on the consumer side.

Edit: both problems are now resolved & you can run swift package --disable-sandbox pdc --run from your project root when you have swift-playdate as a dependency.

Now I wish I could run this on device because the idiomatic swift apis are niiiiice

import Playdate

func updateCallback() -> Bool {

  Graphics.clear(with: .white)
  System.drawFPS(x: 0, y: 0)

  if System.currentButtonState == [.left, .up] {
    Graphics.draw(.text("Pressing up & left!", x: 0, y: 20))
  } else {
    Graphics.draw(.text("Time elapsed: \(System.currentTimeInterval)", x: 0, y: 20))
  }

  Graphics.drawFilledRectangle(
    Rectangle(
      x: 40,
      y: 40,
      width: 60,
      height: 30
    ),
    color: .xor
  )

  Graphics.drawLine(
    from: .zero,
    to: .init(x: 400, y: 240),
    color: .xor,
    stroke: 5
  )

  return true
}

func initialize() {
  do {
    setupDisplay()
    try setupFonts()
    try setupMenu()
  } catch {
    System.error(error.localizedDescription)
  }
}

func setupDisplay() {
  Display.isInverted = true
}

func setupFonts() throws {
  try Graphics.setFont(AshevilleSans, weight: .bold, size: .pt14)
}

enum Fonts: String, CaseIterable {
  case bold
  case light
  case italic
}

func setupMenu() throws {
  Menu.addCheckmarkItem("inverted", isOn: true) { isEnabled in
    Display.isInverted = isEnabled
  }

  Menu.addCheckmarkItem("crnk snd", isOn: true) { isEnabled in
    System.isCrankSoundEnabled = isEnabled
  }

  try Menu.addOptionItem("font", options: Fonts.allCases) { option in
    switch Fonts(rawValue: option) {
    case .bold:
      try Graphics.setFont("Asheville-Sans-14-Bold")
    case .light:
      try Graphics.setFont("Asheville-Sans-14-Light")
    case .italic:
      try Graphics.setFont("Asheville-Sans-14-Light-Oblique")
    case .none:
      break
    }
  }
}

@_dynamicReplacement(for: EventCallback(event:))
func eventCallback(event: SystemEvent) {
  if event == .initialize {
    initialize()
    System.setUpdateCallback(updateCallback)
  }
}

Messing with the swift-embedded project and I’m very close to at least getting something to link that is using the linker script for playdate. Need to figure out how to compile & link the barebones Unicode support, which is the last obstacle to spitting a build out that can be potentially loaded on device.

Edit: I think it’s as simple as compiling the Unicode lib using same toolchain to object files then linking those during the swiftc step. Will try tomorrow as it is late. The lib work I’ve done has only one reliance on foundation (won’t be avail in bare metal) which should be easy to strip out and should compile under the bare metal swift toolchain just fine. Leaving the biggest “unknown” as to wether or not my modifications to the linker script will be kosher with the device (stuffed most of the swift related calls into the heap with everything else)

Edit 1.1: random pointers to anyone else playing with swift-embedded:

  • the prepackaged toolchain is built for x86 which can cause some weirdness on M1
  • building the toolchain from scratch is pretty hard as well and I didn’t try hard enough to succeed, but this version of the compiler is unaware of M1 macs & how everyone switch from master to main.
  • since we now know everything is outdated.. so is cross, the tool that is supposed to make the build part easy :sweat_smile: I ended up sorta piecing together what it was supposed to do. I’ve also forked and fixed it, and I think it’s useless (though I’m wondering now if it’s because of how I tried to compile it at first) because compiler is so much older than the updated spm stuff.
  • you can get reasonably far using just the x86 distributed toolchain the build command is gnarly though (and can probably boil down easily)
  • this is all well & good, but it does require side loading a toolchain.
  • checking out the output bin against a real pdx bin in hopper should reveal if it may function on the device to some extent
  • the spm based Playdate lib approach will need the toolchain to support later versions of spm
  • straight swiftc version should work perfectly
  • as I sign off, just realized creating a barebones makefile for the Unicode / swift steps is probably the way to go.

Edit 2: hoorah! I have managed to get a bin out of this. definitely much more to be done, but excellent first step. Will be a challenge figuring out how to test this though without a device :laughing: the output bin is also like, 5.2mb. it's HUGE.

1 Like