Fortran is used for heavy number crunching in mathematical and scientific computing. This post will start by covering calling Fortran from C. It will then discuss using Fortran on the Panic Playdate, both the simulator and hardware.

Software Versions

$ date -u "+%Y-%m-%d %H:%M:%S +0000"
2022-10-10 21:55:03 +0000
$ uname -vm
Darwin Kernel Version 21.6.0: Mon Aug 22 20:19:52 PDT 2022; root:xnu-8020.140.49~2/RELEASE_ARM64_T6000 arm64
$ ex -s +'%s/<[^>].\{-}>//ge' +'%s/\s\+//e' +'%norm J' +'g/^$/d' +%p +q! /System/Library/CoreServices/SystemVersion.plist | grep -E 'ProductName|ProductVersion' | sed 's/^[^ ]* //g' | sed 'N; s/\n/ /g'
macOS 12.6
$ echo "${SHELL}"
/bin/bash
$ "${SHELL}" --version  | head -n 1
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin21)
$ cat "${HOME}/Developer/PlaydateSDK/VERSION.txt"
1.12.3
$ gfortran -v 2> >(tail -1)
gcc version 12.2.0 (MacPorts gcc12 12.2.0_0+stdlib_flag)
$ arm-none-eabi-gfortran -v 2> >(tail -1)
gcc version 11.3.1 20220712 (Arm GNU Toolchain 11.3.Rel1)

Instructions

Calling Fortran from C

This section assumes that gcc and gfortran are installed. First, create a new project.

PROJECT="c_fortran_interop_example"
mkdir "${PROJECT}"
cd "${PROJECT}"

Add main.c.

main.c

#include <stdio.h>
#include <stdlib.h>
#include "fast_sqrt.h"

int main(int argc, char **argv) {
  for (int i=1; i<argc; i++) {
    double input = atof(argv[i]);
    double output = fast_sqrt(input);
    printf("The square root of %.3f is %.3f.\n", input, output);
  }
  return 0;
}

Add fast_sqrt.h for interoperation with C.

fast_sqrt.h

#ifndef FAST_SQRT_H
#define FAST_SQRT_H

extern double fast_sqrt(double);

#endif  // FAST_SQRT_H

Add the Fortran implementation of fast_sqrt().

fast_sqrt.f90

function fast_sqrt( x ) result( y ) bind( C, name="fast_sqrt" )
  use iso_c_binding, only: c_double
  implicit none

  real(c_double), VALUE :: x
  real(c_double) :: y

  y = sqrt(x)
end function

Create a Makefile to capture simple logic for building and testing the program. The C and Fortran files are compiled into object files to combine into a binary. The testing code calls the program with the numbers from zero to ten as test parameters.

Makefile

.PHONEY: all clean force run test

CFLAGS=-Wall
FC=gfortran
FCFLAGS=-Wall

TARGET=fast_sqrt
OBJS=main.o fast_sqrt.o

all: $(TARGET)

force: clean all

$(TARGET): $(OBJS)
	$(FC) $(CFLAGS) $^ -o $@

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

%.o: %.f90
	$(FC) $(FCFLAGS) -c $< -o $@

test: $(TARGET) run

run:
	for i in {0..10}; do ./$(TARGET) $$i; done

clean:
	rm -rf *.o $(TARGET)

Build and test the program.

$ make test
cc -Wall -c main.c -o main.o
The square root of 0.000 is 0.000.
The square root of 1.000 is 1.000.
The square root of 2.000 is 1.414.
The square root of 3.000 is 1.732.
The square root of 4.000 is 2.000.
The square root of 5.000 is 2.236.
The square root of 6.000 is 2.449.
The square root of 7.000 is 2.646.
The square root of 8.000 is 2.828.
The square root of 9.000 is 3.000.
The square root of 10.000 is 3.162.

Running Fortran on the Playdate Simulator

Calling Fortran from C on the Playdate simulator is much the same as in the previous section. The Playdate simulator runs on the development machine and uses the host architecture. Therefore, cross-compilation is not necessary. The Hello World C API example is a reasonable project template, so make a copy.

PROJECT="fortran_test"
PROJECT_PATH="my_playdate_projects"
cd "${PROJECT_PATH}"
cp -r "${PLAYDATE_SDK_PATH}/C_API/Examples/Hello World/" "${PROJECT}"
cd "${PROJECT}"

Next, update pdxinfo.

Source/pdxinfo

name=FortranTest
author=Brendan Sechter
description=Fortran on Playdate proof of concept.
bundleID=com.sennue.poc_fortrantest
imagePath=

The main.c and main.h files from an earlier post, ASM Playdate Development, can be used for this project. Modify main.c.

src/main.c

#include "fast_sqrt.h"
#include "main.h"
#include "pd_api.h"

const char* fontpath = "/System/Fonts/Asheville-Sans-14-Bold.pft";
const LCDPattern gray50 = {
  // Bitmap
  0b10101010,
  0b01010101,
  0b10101010,
  0b01010101,
  0b10101010,
  0b01010101,
  0b10101010,
  0b01010101,

  // Mask
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
};

void initProgramState(struct ProgramState *ps, PlaydateAPI *pd)
{
  const char *errorMessage;

  ps->pd = pd;
  ps->font = NULL;
  ps->font = pd->graphics->loadFont(fontpath, &errorMessage);
  if (NULL == ps->font) {
    pd->system->error("%s:%i Couldn't load font %s: %s",
      __FILE__, __LINE__, fontpath, errorMessage
    );
  }
  ps->previousInput = 0;
  ps->strokeWidth = 2;
  ps->x = (LCD_COLUMNS - TEXT_WIDTH) / 2;
  ps->y = (LCD_ROWS - TEXT_HEIGHT) / 2;
  ps->dx = 1;
  ps->dy = 1;
}

#ifdef _WINDLL
__declspec(dllexport)
#endif
int eventHandler(PlaydateAPI* pd, PDSystemEvent event, uint32_t arg)
{
  (void)arg; // only used for kEventKeyPressed == event
  static struct ProgramState *ps = NULL;

  switch (event) {
    case kEventInit:
      ps = pd->system->realloc(ps, sizeof(struct ProgramState));
      initProgramState(ps, pd);
      pd->system->setUpdateCallback(update, (void *)ps);
      break;
    case kEventTerminate:
      pd->system->realloc(ps, 0);
      ps = NULL;
      break;
    default:
      // do nothing
    break;
  };

  return 0;
}

static int update(void* userdata)
{
  struct ProgramState *ps = (struct ProgramState *)userdata;
  handleInput(ps);
  draw(ps);
  return 1;
}

void handleInput(struct ProgramState *ps) {
  PlaydateAPI* pd = ps->pd;

  int x_direction = 0;
  int y_direction = 0;

  // dpad input
  PDButtons currentInput;
  pd->system->getButtonState(&currentInput, NULL, NULL);
  if ( currentInput & kButtonUp ) {
    ps->y--;
  } else if ( currentInput & kButtonDown ) {
    ps->y++;
  } else {
    y_direction = 1;
  }
  if ( currentInput & kButtonLeft ) {
    ps->x--;
  } else if ( currentInput & kButtonRight ) {
    ps->x++;
  } else {
    x_direction = 1;
  }
  if (( currentInput & kButtonA ) && !( ps->previousInput & kButtonA )) {
    ps->dx *= -1;
  }
  if (( currentInput & kButtonB ) && !( ps->previousInput & kButtonB )) {
    ps->dy *= -1;
  }
  ps->previousInput = currentInput;

  int steps;

  if (pd->system->isCrankDocked()) {
    steps = 1;
  } else {
    steps = pd->system->getCrankChange();
    if (steps < 0) {
      steps = -steps;
      x_direction = -1;
      y_direction = -1;
    }
  }

  for (int i = 0; i < steps; i++) {
    ps->x += ps->dx * x_direction;
    ps->y += ps->dy * y_direction;

    // bounce
    if ( ps->x < 0 || LCD_COLUMNS < ps->x ) {
      ps->dx *= -1;
    }
    if ( ps->y < 0 || LCD_ROWS < ps->y ) {
      ps->dy *= -1;
    }
  }
}

int adjustTextPosition(int x, int w, int min, int max) {
  if (x < min) {
    return min;
  } else if (max < x + w) {
    return max - w;
  } // else
  return x;
}

void keepTextOnScreen(int *x_ptr, int *y_ptr, int x, int y) {
  *x_ptr = adjustTextPosition(x, TEXT_WIDTH, 0, LCD_COLUMNS);
  *y_ptr = adjustTextPosition(y, TEXT_HEIGHT, 0, LCD_ROWS);
}

void drawOutlinedText(
  PlaydateAPI* pd,
  const char *message,
  int x,
  int y,
  int outlineWidth,
  LCDColor textColor,
  LCDColor outlineColor
) {
  pd->graphics->setDrawMode(outlineColor);
  pd->graphics->drawText(
    message, strlen(message), kASCIIEncoding, x - outlineWidth, y
  );
  pd->graphics->drawText(
    message, strlen(message), kASCIIEncoding, x + outlineWidth, y
  );
  pd->graphics->drawText(
    message, strlen(message), kASCIIEncoding, x, y - outlineWidth
  );
  pd->graphics->drawText(
    message, strlen(message), kASCIIEncoding, x, y + outlineWidth
  );
  pd->graphics->setDrawMode(textColor);
  pd->graphics->drawText(message, strlen(message), kASCIIEncoding, x, y);
}

void draw(struct ProgramState *ps)
{
  PlaydateAPI* pd = ps->pd;
  int stroke = ps->strokeWidth;;
  int x = ps->x;
  int y = ps->y;

  char *message = NULL;
  int text_x, text_y;

  pd->graphics->clear((LCDColor)gray50);
  pd->graphics->setFont(ps->font);

  // distance visual
  pd->graphics->fillRect(stroke, stroke, x-2*stroke, y-2*stroke, kColorBlack);
  pd->graphics->drawLine(0, 0, x, y, stroke, kColorWhite);
  pd->graphics->drawLine(x, 0, x, y, stroke, kColorBlack);
  pd->graphics->drawLine(0, y, x, y, stroke, kColorBlack);

  // distance message
  pd->system->formatString(&message, "d=%.3f", fast_sqrt(x*x + y*y));
  keepTextOnScreen(
    &text_x, &text_y, (x - TEXT_WIDTH) / 2, (y - TEXT_HEIGHT) / 2
  );
  drawOutlinedText(
    pd, message, text_x, text_y, stroke, kDrawModeInverted, kDrawModeCopy
  );

  // position message
  pd->system->formatString(&message, "(%d, %d)", x, y);
  keepTextOnScreen(&text_x, &text_y, x, y);
  drawOutlinedText(
    pd, message, text_x, text_y, stroke, kDrawModeCopy, kDrawModeInverted
  );

  // FPS display
  pd->system->drawFPS(0,0);

  // cleanup
  pd->system->realloc(message, 0);
}

Add main.h.

src/main.h

#ifndef MAIN_H
#define MAIN_H

#include "pd_api.h"

#define TEXT_WIDTH 86
#define TEXT_HEIGHT 16
extern const char* fontpath;
extern const LCDPattern gray50;

struct ProgramState {
  PlaydateAPI *pd;
  LCDFont *font;
  PDButtons previousInput;
  int strokeWidth;
  int x;
  int y;
  int dx;
  int dy;
};

void initProgramState(struct ProgramState *ps, PlaydateAPI *pd);
#ifdef _WINDLL
__declspec(dllexport)
#endif
int eventHandler(PlaydateAPI* pd, PDSystemEvent event, uint32_t arg);
static int update(void* userdata);
void handleInput(struct ProgramState *ps);
void draw(struct ProgramState *ps);

#endif // MAIN_H

Take the fast_sqrt.f90 and fast_sqrt.h files from the previous section.

PREVIOUS_PROJECT_PATH="path_to_project_in_previous_section"
cp "${PREVIOUS_PROJECT_PATH}/fast_sqrt.f90" "${PREVIOUS_PROJECT_PATH}/fast_sqrt.h" src/

Change the PRODUCT and SRC lines in the Makefile.

Makefile Partial Listing

PRODUCT = FortranTest.pdx

# List C source files here
SRC = src/main.c

# List Fortran source files here
FSRC = src/fast_sqrt.f90

# last line of Makefile
include common.mk

Playdate Makefiles rely on centralized machinery that does not support Fortran out of the box. Instead of directly modifying the original ${PLAYDATE_SDK_PATH}/C_API/buildsupport/common.mk file, make a copy of common.mk for this project.

cp "${PLAYDATE_SDK_PATH}/C_API/buildsupport/common.mk" common.mk

Use the following diff as a guide to update common.mk.

$ diff "${PLAYDATE_SDK_PATH}/C_API/buildsupport/common.mk" common.mk
84c84
< _OBJS	= $(SRC:.c=.o)
---
> _OBJS	= $(SRC:.c=.o) $(FSRC:.f90=.o)
106a107,111
> SIMFC=$(shell which gfortran)
> SIMFCFLAGS = -gdwarf-2 -Wall
> FC=$(shell which $(TRGT)gfortran)
> FCFLAGS = $(MCFLAGS) $(OPT) -gdwarf-2 -Wall
>
131c136
< pdc: simulator
---
> pdc: device simulator
140a146,153
> $(OBJDIR)/%.o : %.f90 | OBJDIR DEPDIR
> 	mkdir -p `dirname $@`
> 	$(FC) $(FCFLAGS) -c $< -o $@
>
> $(OBJDIR)/%_simulator.o : %.f90 | OBJDIR DEPDIR
> 	mkdir -p $(dir $@)
> 	$(SIMFC) $(SIMFCFLAGS) -c $< -o $@
>
153,154c166,167
< $(OBJDIR)/pdex.${DYLIB_EXT}: OBJDIR
< 	$(SIMCOMPILER) $(DYLIB_FLAGS) -lm -DTARGET_SIMULATOR=1 -DTARGET_EXTENSION=1 $(INCDIR) -o $(OBJDIR)/pdex.${DYLIB_EXT} $(SRC)
---
> $(OBJDIR)/pdex.${DYLIB_EXT}: OBJDIR $(FSRC:%.f90=$(OBJDIR)/%_simulator.o)
> 	$(SIMCOMPILER) $(DYLIB_FLAGS) -lm -DTARGET_SIMULATOR=1 -DTARGET_EXTENSION=1 $(INCDIR) -o $(OBJDIR)/pdex.${DYLIB_EXT} $(SRC) $(FSRC:%.f90=$(OBJDIR)/%_simulator.o)

Finally, build and run the project to verify it works. The square root demo should boot and run in the simulator.

PRODUCT="$(cat Source/pdxinfo | grep name | cut -d "=" -f 2-).pdx"
make clean simulator
pdc Source "${PRODUCT}"
playdate_simulator "${PRODUCT}"

Running Fortran on Playdate Hardware

The Playdate SDK does not ship with arm-none-eabi-gfortran, but the full toolchain distributed by ARM includes it. Download the latest copy of the ARM toolchain for your host platfrom from the ARM GNU Toolchain Downloads Page, and install it. The author of this post downloaded the macOS Hosted Bare-Metal Target (arm-none-eabi) 11.3.rel1 toolchain.

The toolchain was installed in /Applications/ArmGNUToolchain/ on macOS, The following command can be used to find the installation directory from the command line.

dirname $(find / -name "arm-none-eabi-gfortran" 2>/dev/null)

Add the directory to the PATH environment variable.

ARM_TOOLCHAIN_PATH="/Applications/ArmGNUToolchain/11.3.rel1/arm-none-eabi/bin"
echo 'export PATH="'"${ARM_TOOLCHAIN_PATH}"':${PATH}"' >> "${HOME}/.profile"

Reload .profile if necessary.

source "${HOME}/.profile"

The above diff contains all of the necessary changes to build a PDX file that runs on hardware. After updating the PATH, build and run the program to verify it works. To upload a PDX file to hardware, first run it in the simulator.

PRODUCT="$(cat Source/pdxinfo | grep name | cut -d "=" -f 2-).pdx"
make
playdate_simulator "${PRODUCT}"

Then either “Upload Game to Device” from the “Device” menu or Playdate icon on the lower lefthand corner of the simulator (with the crank controls collapsed). Once the game is on the device, pdutil can launch it.

# after the game is on the device
PRODUCT="$(cat Source/pdxinfo | grep name | cut -d "=" -f 2-).pdx"
PDUTIL_DEVICE="$(ls /dev/cu.usbmodemPD* | head -n 1)"
pdutil "${PDUTIL_DEVICE}" run "/Games/${PRODUCT}"

Verification

Pull out the crank and use the D-pad to move the lower righthand point to (300, 125). The diagonal length should be 325.000. Use a calculator to verify other sets of values.

  300 *   300 =  90000
  125 *   125 =  15625
  325 *   325 = 105625
----------------------
90000 + 15625 = 105625

More Information

The Fortran quickstart tutorial has a section on Derived Types. Specifically, bind(c) offers interoperability with the C programming language. gcc also has documention on Fortran interoperability with C.

This post is based on the “Fortran on Playdate?” thread on the Playdate Developer Forums.

References: