Restarting game with active TCP connection can crash Simulator? (in certain conditions!)

,

If you 'restart' (specifically restart game, the little :right_arrow_curving_left:-ish button next to play/pause) in the Simulator, while using the C SDK, when a TCP connection is open, and a TCPConnectionCallback callback is set, the Simulator crashes.

This doesn't seem to happen when a similar approach is implemented in Lua.

SDK/Simulator: 3.1.1
Platform: macOS Tahoe 26.5.2 (and 26.6 will test on 26.6 shortly and edit this to confirm it's still occurring!)

  1. Start game (written in C) in Simulator
  2. Game establishes TCP connection
  3. While connection is open, press 'restart' in Simulator
  4. (Simulator crashes)
  5. Comment out the following line: tcp->setConnectionClosedCallback(tcpconn, TCPConnectionClosedCallback);
  6. Rebuild the game, follow the same steps
  7. The Simulator no longer crashes

Minimal(ish :sweat_smile:) reproduction:

I wrote this while trying to eliminate my game as the source of the issue (started with the C networking example as a base to ensure I wasn't doing anything wild). The Ruby server is super simple but most TCP servers ought to sub in as long as it doesn't immediately close the connection.

main.c

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

#include "pd_api.h"

#define SERVER "localhost"
#define WAIT_PROMPT_1 "(waiting for successful connection...)"
#define WAIT_PROMPT_2 "(please do not restart Simulator yet)"
#define GO_PROMPT "press restart game to reproduce crash!"

PlaydateAPI *pd;
static int update(void *userdata);

const struct playdate_tcp *tcp;

#ifdef _WINDLL
__declspec(dllexport)
#endif
int eventHandler(PlaydateAPI *playdate, PDSystemEvent event, uint32_t arg) {
  (void)arg; // arg is currently only used for event = kEventKeyPressed

  if (event == kEventInit) {
    pd = playdate;
    pd->system->setUpdateCallback(update, pd);

    tcp = pd->network->tcp;
  }

  return 0;
}

enum {
  kRequestingTCPAccess,
  kWaitingForTCPAccess,
  kTCPConnect,
  kTCPConnecting,
  kTCPOpen,
  kNoAccess,
  kDone,
} state = kRequestingTCPAccess;

// --- TCP ---

TCPConnection *tcpconn;

void TCPAccessCallback(bool allowed, void *userdata) {
  pd->system->logToConsole("TCPAccessCallback: %i", allowed);
  state = allowed ? kTCPConnect : kNoAccess;
}

// Crashes Simulator (when restart game is clicked) even when
// function doesn't 'do' anything in body.
void TCPConnectionClosedCallback(TCPConnection *conn, PDNetErr err) {
  // pd->system->logToConsole("TCP connection closed");
}

void tcpOpenCallback(TCPConnection *conn, PDNetErr err, void *ud) {
  if (err == NET_OK) {
    state = kTCPOpen;
  } else {
    pd->system->logToConsole("error opening TCP connection: %i", err);
  }
}

static void tcpConnect(void) {
  tcpconn = tcp->newConnection(SERVER, 65431, false);

  // This callback crashes the Simulator, when "restart" (game) is
  // clicked while the TCP connection is open. To prevent the crash,
  // comment out this line:
  tcp->setConnectionClosedCallback(tcpconn, TCPConnectionClosedCallback);

  PDNetErr err = tcp->open(tcpconn, tcpOpenCallback, NULL);
  state = kTCPConnecting;
}

static int update(void *userdata) {
  int promptToReproduce = 0;
  if (state == kRequestingTCPAccess) {
    state = kWaitingForTCPAccess;
    enum accessReply access = tcp->requestAccess("localhost", 65431, false, "for testing", TCPAccessCallback, NULL);
    if (access == kAccessDeny) {
      pd->system->logToConsole("tcp access denied");
      state = kNoAccess;
    } else if (access == kAccessAllow) {
      state = kTCPConnect;
    }
  } else if (state == kWaitingForTCPAccess) {
    // no-op
  } else if (state == kTCPConnect) {
    tcpConnect();
  } else if (state == kTCPConnecting) {
    // no-op
  } else if (state == kTCPOpen) {
    promptToReproduce = 1;
  } else if (state == kNoAccess) {
    pd->system->logToConsole("Internet access denied!");
    state = kDone;
  }

  pd->graphics->clear(kColorWhite);
  if (promptToReproduce == 1) {
    pd->graphics->drawText(GO_PROMPT, strlen(GO_PROMPT), kASCIIEncoding, 5, 55);
  } else {
    pd->graphics->drawText(WAIT_PROMPT_1, strlen(WAIT_PROMPT_1), kASCIIEncoding, 5, 5);
    pd->graphics->drawText(WAIT_PROMPT_2, strlen(WAIT_PROMPT_2), kASCIIEncoding, 5, 30);
  }

  return 1;
}

server.rb

#!/usr/bin/env ruby

require "socket"

TCP_PORT = 65431
server = TCPServer.new(TCP_PORT)
threads = []

loop do
  new_client = server.accept
  threads << Thread.start(new_client) do |client|
    puts "new client connected"

    loop do
      # read:
      received_data = client.gets
      if received_data.nil?
        puts "client disconnected"
        break
      end
      puts "received: #{received_data}"

      sleep(1.0)

      # write:
      response = "FROMSERV\n"
      client.puts response
      puts "sent: #{response}"
    end
    client.close
    puts "connection closed"
  end
end

Thanks for looking, hopefully this all made sense!!