Deinit in Embedded Swift

I’m playing around with an ESP32-C6 Development board and looking at was playing around with wrapping the FreeRTOS Task API with something more Swift-y. Here is my basic Main.swift:

@_cdecl("app_main")
func main() {
  print("Hello from Swift on ESP32-C6!")

  _ = try? Task {
    print("In my task")
  }

  print("Created task, exiting")
}

And here is an included Task.swift

class Task {
  typealias TaskFunction = @convention(c) (UnsafeMutableRawPointer?) -> Void

  let innerTask: () -> Void

  init(_ innerTask: @escaping () -> Void) throws {
    self.innerTask = innerTask

    let taskPointer = Unmanaged.passRetained(self).toOpaque()
    let taskFunction: TaskFunction = {(_ ptr: UnsafeMutableRawPointer?) in
      guard let ptr else { return }

      let unmanaged = Unmanaged<Task>.fromOpaque(ptr)
      let task = unmanaged.takeUnretainedValue()
      print("executing task")
      task.innerTask()
      print("executed task, releasing")
      unmanaged.release()
      print("released, executing vTaskDelete")
      vTaskDelete(nil)
      print("executed vTaskDelete, exiting closure")
    }
    if pdPASS != xTaskCreate(taskFunction, "inner_task", 4096, taskPointer, uxTaskPriorityGet(nil) - 1 , nil) {
      print("Failed to create task")
      Unmanaged<Task>.fromOpaque(taskPointer).release()
    }
    print("exiting init")
  }

  deinit {
    print("Task.deinit")
  }
}

When I build, flash, and monitor the output, I see this at the end:

I (250) main_task: Calling app_main()
Hello from Swift on ESP32-C6!
exiting init
Created task, exiting
I (260) main_task: Returned from app_main()
executing task
In my task
executed task, releasing
released, executing vTaskDelete

I was expecting to see the Task instance deallocate on the unmanaged.release() line, but it never happens.

I think I’m correctly managing the memory as expected, so I’m wondering if this is expected behavior or if I’ve done something wrong.

I’m fairly certain the issue lies in the vTaskDelete(nil) call at the end of executing my closure. I’m assuming this prevents ARC from cleaning up because that function never returns. When I remove that line, I see the Task.deinit right before the program crashes due to FreeRTOS’s rules around tasks.

I’ll need to think through other ways of handling memory with this limitation in mind.

Yes, I feel quite dumb now with how obvious the issue was. The line

let task = unmanaged.takeUnretainedValue()

is the culprit. While the takeUnretainedValue doesn’t call release, this will still create a new retain on the local task variable. To fix, I needed to make it Optional so I could manually nil it out before exiting properly:

      let unmanaged = Unmanaged<Task>.fromOpaque(ptr)
      var task: Task? = unmanaged.takeUnretainedValue()
      print("executing task")
      task?.innerTask()      
      print("executed task, releasing")
      unmanaged.release()
      print("setting task to nil")
      task = nil
      print("released, executing vTaskDelete")
      vTaskDelete(nil)
      print("executed vTaskDelete, exiting closure")

And now I get my proper output:

executing task
In my task
executed task, releasing
setting task to nil
Task.deinit
released, executing vTaskDelete

(Note the last print never executes because as mentioned before vTaskDelete does not return, so that is expected)

2 Likes

Ok, I promise I’ll stop after this one. Even cleaner way to handle is not use an intermediate variable:

      let unmanaged = Unmanaged<Task>.fromOpaque(ptr)
      unmanaged.takeUnretainedValue().innerTask()      
      unmanaged.release()
      vTaskDelete(nil)
3 Likes