Complex C code gave ChatGPT a headache and Claude came to the rescue

For Figuro I want to have a file monitoring API that watches for the CSS theme to be modified. Unfortunately all the libraries in Nim are either wrappers around C libraries or incomplete.

I tried finding a few C projects and translating them, but it’s a pain because it requires using low level system APIs which also need binding in Nim.

Notes About Translating

I also tried converting the code automatically using c2nim which is very helpful. Unfortunately for this sort of system code you end up with lots of missing details.

For example on MacOS file watching libraries tend to use FSEventStreamContext which I’ve never even heard about before. It requires plumbing some handlers for allocating and deallocating things event structs. Or something as I don’t really care about the details. Others figured that out, I just want to use it.

Most of converting C to Nim (or any programming language to another) is just going through a fairly simple set of mini-algorithms and mappings. For example C enum flags can be converted into Nim’s set[SomeEnum] which is much easier to work with and typesafe. It also casts back to the proper C integer values!

Now programming this knowledge into something like c2nim takes a lot of work. I know this well since I’ve contributed a fair bit to c2nim in the past. It’s fun but becomes pretty tedious since there are dozens if not hundreds of these mini-algorithms and C isn’t very strict almost to the point that it’s really considered a typed language. It’s more kinda-typed, definitely unlike Haskell’s higher kinded types.

Isn’t this what LLMs should be great at however? They really boil down to very complex [Markov chains](https://en.wikipedia.org/wiki/Markov_chain) compiled over the entire internet with some fascinating emergent behavior. It almost seems like they can reason about a problem. Really though it’s more like they learn complex flow diagrams and decision trees from all the hard work us humans have put into everything the past umpteen millennium. That’s also why I think UBI makes sense as automation builds upon the work of generations of minds!

Okay back to the core topic of translating code.

Trying out ChatGPT

Eventually I figured I’d give ChatGPT a go. It started out great! But then it essentially gave up:

# Enums translated to Nim
type
  DmonWatchFlags* = enum
    recursive = 0x1,
    followSymlinks = 0x2,
    outOfScopeLinks = 0x4,
    ignoreDirectories = 0x8

  DmonAction* = enum
    create = 1,
    delete,
    modify,
    move

... more structs

# Utility functions
proc isRange*(ch: char, from: char, to: char): bool =
  result = uint8(ch - from) <= uint8(to - from)

proc isUpperChar*(ch: char): bool =
  result = isRange(ch, 'A', 'Z')

proc toLowerChar*(ch: char): char =
  if isUpperChar(ch):
    result = ch + '\x20'
  else:
    result = ch

proc toLower*(dst: var string, str: string) =
  for c in str:
    dst.add(toLowerChar(c))

# Initialization function
proc dmonInit*() =
  echo "Initializing dmon..."

proc dmonDeinit*() =
  echo "Deinitializing dmon..."

The beginning looks like it’s off to a great start. However both dmonInit and dmonDeinit are filler procs. So I tried prompting ChatGPT for more and it kept doing the same habit of trying to skimp out on doing a full translation.

Also, ChatGPT noticeably began to struggle outputting the code when you included fuller details. It’s unclear if that’s due to throttling by OpenAI or that the model requires more resources.

On to Claude

Next I figured I’d try Claude Sonnet 3.5 from Anthropic. Word on the street is that it’s much better at coding.

It’s first attempts was similar to ChatGPT in that it gave a summary of how to do the conversion. Well great, it wants to put me to work for it. I’m starting to feel like LLMs have great management potential! :P

Okay, but once I prompted Claude to give me the full full translation it obliged! Well at least until:

But a quick follow up prompt got it to finish:

Results

Below is the full output for the MacOS specific conversion for those curious about such things. Does it compile? Nope! But that looks like a great start. I’ve been skimming it and comparing it to the C code and it’s done a good job converting.

I tend to trust LLMs for translating the code since they do better against something that exists and this sort of common system code which has a lot of similar implementation in many languages. It’s not creating de novo code.


import posix
import macros
import os
import strutils
import locks
import tables

# MacOS-specific imports
{.passL: "-framework CoreServices -framework CoreFoundation".}

type
  CFIndex* = clong
  CFTimeInterval* = cdouble
  CFStringRef* = distinct pointer
  CFArrayRef* = distinct pointer
  CFRunLoopRef* = distinct pointer
  CFAllocatorRef* = distinct pointer
  CFRunLoopMode* = distinct pointer
  FSEventStreamRef* = distinct pointer
  FSEventStreamEventId* = culonglong
  FSEventStreamEventFlags* = culong
  dispatch_semaphore_t* = distinct pointer

const
  kCFStringEncodingUTF8* = 0x08000100'i32
  kCFRunLoopDefaultMode* = CFRunLoopMode(CFStringRef("kCFRunLoopDefaultMode"))
  kCFRunLoopRunTimedOut* = 2
  kFSEventStreamEventFlagItemCreated* = 0x00000100'u32
  kFSEventStreamEventFlagItemRemoved* = 0x00000200'u32
  kFSEventStreamEventFlagItemModified* = 0x00001000'u32
  kFSEventStreamEventFlagItemRenamed* = 0x00000800'u32
  kFSEventStreamCreateFlagFileEvents* = 0x00000010'u32

type
  DmonWatchId* = distinct uint32

  DmonWatchFlags* = enum
    Recursive = 0x1
    FollowSymlinks = 0x2 
    OutOfScopeLinks = 0x4
    IgnoreDirectories = 0x8

  DmonAction* = enum
    Create = 1
    Delete
    Modify 
    Move

  DmonWatchCallback* = proc(watchId: DmonWatchId, action: DmonAction, 
                          rootdir, filepath, oldfilepath: string,
                          userData: pointer) {.cdecl.}

  FSEventStreamContext* {.pure, final.} = object
    version*: CFIndex
    info*: pointer
    retain*: pointer
    release*: pointer
    copyDescription*: pointer

  DmonFsEvent = object
    filepath: string
    eventId: FSEventStreamEventId
    eventFlags: FSEventStreamEventFlags
    watchId: DmonWatchId
    skip: bool
    moveValid: bool

  DmonWatchState = ref object
    id: DmonWatchId
    watchFlags: uint32
    fsEvStreamRef: FSEventStreamRef
    watchCb: DmonWatchCallback
    userData: pointer
    rootdir: string
    rootdirUnmod: string
    init: bool

  DmonState = object
    watches: array[64, DmonWatchState]
    freeList: array[64, int]
    events: seq[DmonFsEvent]
    numWatches: int
    modifyWatches: Atomic[int]
    threadHandle: Thread[void]
    threadLock: Lock
    threadSem: dispatch_semaphore_t
    cfLoopRef: CFRunLoopRef
    cfAllocRef: CFAllocatorRef
    quit: bool

# CoreFoundation Functions
proc CFStringCreateWithCString*(alloc: CFAllocatorRef, cStr: cstring, encoding: int32): CFStringRef {.importc.}
proc CFArrayCreate*(alloc: CFAllocatorRef, values: ptr pointer, numValues: CFIndex, callbacks: pointer): CFArrayRef {.importc.}
proc CFRunLoopGetCurrent*(): CFRunLoopRef {.importc.}
proc CFRunLoopRunInMode*(mode: CFRunLoopMode, seconds: CFTimeInterval, returnAfterSourceHandled: bool): cint {.importc.}
proc CFRunLoopStop*(loop: CFRunLoopRef) {.importc.}
proc CFRelease*(cf: pointer) {.importc.}

# FSEvents Functions
proc FSEventStreamCreate*(
  allocator: CFAllocatorRef, 
  callback: proc (
    streamRef: FSEventStreamRef,
    clientCallBackInfo: pointer,
    numEvents: csize_t,
    eventPaths: pointer,
    eventFlags: ptr FSEventStreamEventFlags,
    eventIds: ptr FSEventStreamEventId
  ) {.cdecl.},
  context: ptr FSEventStreamContext,
  pathsToWatch: CFArrayRef,
  sinceWhen: FSEventStreamEventId,
  latency: CFTimeInterval,
  flags: FSEventStreamEventFlags
): FSEventStreamRef {.importc.}

proc FSEventStreamScheduleWithRunLoop*(
  streamRef: FSEventStreamRef,
  runLoop: CFRunLoopRef,
  runLoopMode: CFRunLoopMode
) {.importc.}

proc FSEventStreamStart*(streamRef: FSEventStreamRef): bool {.importc.}
proc FSEventStreamStop*(streamRef: FSEventStreamRef) {.importc.}
proc FSEventStreamInvalidate*(streamRef: FSEventStreamRef) {.importc.}
proc FSEventStreamRelease*(streamRef: FSEventStreamRef) {.importc.}

# Grand Central Dispatch Functions
proc dispatch_semaphore_create*(value: clong): dispatch_semaphore_t {.importc.}
proc dispatch_semaphore_signal*(sem: dispatch_semaphore_t): clong {.importc.}
proc dispatch_semaphore_wait*(sem: dispatch_semaphore_t, timeout: uint64): clong {.importc.}
proc dispatch_release*(obj: pointer) {.importc.}

const DISPATCH_TIME_FOREVER* = not 0'u64

var
  dmonInitialized: bool
  dmon: DmonState

proc fsEventCallback(
  streamRef: FSEventStreamRef,
  userData: pointer,
  numEvents: csize_t,
  eventPaths: pointer,
  eventFlags: ptr FSEventStreamEventFlags,
  eventIds: ptr FSEventStreamEventId) {.cdecl.} =

  let watchId = cast[DmonWatchId](userData)
  assert(uint32(watchId) > 0)

  let watch = dmon.watches[uint32(watchId) - 1]
  let paths = cast[ptr UncheckedArray[cstring]](eventPaths)

  for i in 0..<numEvents:
    var ev = DmonFsEvent()
    let path = $paths[i]

    # Convert path to unix style and make relative to watch root
    var absPath = unixPath(path)
    let watchRoot = watch.rootdir.toLowerAscii

    if not absPath.startsWith(watchRoot):
      continue

    ev.filepath = absPath[watchRoot.len..^1]
    ev.eventFlags = eventFlags[i]
    ev.eventId = eventIds[i]
    ev.watchId = watchId

    dmon.events.add(ev)

proc processEvents() =
  for i in 0..<dmon.events.len:
    var ev = addr dmon.events[i]
    if ev.skip:
      continue

    # Coalesce multiple modify events
    if (ev.eventFlags and kFSEventStreamEventFlagItemModified) != 0:
      for j in (i+1)..<dmon.events.len:
        let checkEv = addr dmon.events[j]
        if (checkEv.eventFlags and kFSEventStreamEventFlagItemModified) != 0 and
           checkEv.filepath == ev.filepath:
          ev.skip = true
          break

    # Handle renames
    elif (ev.eventFlags and kFSEventStreamEventFlagItemRenamed) != 0 and
         not ev.moveValid:
      for j in (i+1)..<dmon.events.len:
        let checkEv = addr dmon.events[j]
        if (checkEv.eventFlags and kFSEventStreamEventFlagItemRenamed) != 0 and
           checkEv.eventId == ev.eventId + 1:
          ev.moveValid = true
          checkEv.moveValid = true
          break

      # If no matching rename found, treat as create/delete
      if not ev.moveValid:
        ev.eventFlags = ev.eventFlags and not kFSEventStreamEventFlagItemRenamed
        let absPath = watch.rootdir / ev.filepath

        if not fileExists(absPath):
          ev.eventFlags = ev.eventFlags or kFSEventStreamEventFlagItemRemoved
        else:
          ev.eventFlags = ev.eventFlags or kFSEventStreamEventFlagItemCreated

  # Process final events
  for i in 0..<dmon.events.len:
    let ev = addr dmon.events[i]
    if ev.skip:
      continue

    let watch = dmon.watches[uint32(ev.watchId) - 1]
    if watch == nil or watch.watchCb == nil:
      continue

    if (ev.eventFlags and kFSEventStreamEventFlagItemCreated) != 0:
      watch.watchCb(ev.watchId, Create, watch.rootdirUnmod, ev.filepath, nil, watch.userData)

    if (ev.eventFlags and kFSEventStreamEventFlagItemModified) != 0:
      watch.watchCb(ev.watchId, Modify, watch.rootdirUnmod, ev.filepath, nil, watch.userData)

    elif (ev.eventFlags and kFSEventStreamEventFlagItemRenamed) != 0:
      for j in (i+1)..<dmon.events.len:
        let checkEv = addr dmon.events[j]
        if (checkEv.eventFlags and kFSEventStreamEventFlagItemRenamed) != 0:
          watch.watchCb(checkEv.watchId, Move, watch.rootdirUnmod,
                       checkEv.filepath, ev.filepath, watch.userData)
          break

    elif (ev.eventFlags and kFSEventStreamEventFlagItemRemoved) != 0:
      watch.watchCb(ev.watchId, Delete, watch.rootdirUnmod, ev.filepath, nil, watch.userData)

  dmon.events.setLen(0)

proc monitorThread() {.thread.} =
  dmon.cfLoopRef = CFRunLoopGetCurrent()
  dispatch_semaphore_signal(dmon.threadSem)

  while not dmon.quit:
    if dmon.modifyWatches.load() != 0 or not tryLock(dmon.threadLock):
      sleep(10)
      continue

    if dmon.numWatches == 0:
      sleep(10)
      unlock(dmon.threadLock)
      continue

    for i in 0..<dmon.numWatches:
      let watch = dmon.watches[i]
      if not watch.init:
        assert(not watch.fsEvStreamRef.isNil)
        FSEventStreamScheduleWithRunLoop(watch.fsEvStreamRef, dmon.cfLoopRef, kCFRunLoopDefaultMode)
        discard FSEventStreamStart(watch.fsEvStreamRef)
        watch.init = true

    discard CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, false)
    processEvents()

    unlock(dmon.threadLock)

  CFRunLoopStop(dmon.cfLoopRef)
  dmon.cfLoopRef = nil

proc unwatchState(watch: DmonWatchState) =
  if not watch.fsEvStreamRef.isNil:
    FSEventStreamStop(watch.fsEvStreamRef)
    FSEventStreamInvalidate(watch.fsEvStreamRef)
    FSEventStreamRelease(watch.fsEvStreamRef)
    watch.fsEvStreamRef = nil

proc initDmon*() =
  assert(not dmonInitialized)

  initLock(dmon.threadLock)
  dmon.threadSem = dispatch_semaphore_create(0)
  assert(not dmon.threadSem.isNil)

  createThread(dmon.threadHandle, monitorThread)

  discard dispatch_semaphore_wait(dmon.threadSem, DISPATCH_TIME_FOREVER)

  for i in 0..<64:
    dmon.freeList[i] = 64 - i - 1

  dmonInitialized = true

proc deinitDmon*() =
  assert(dmonInitialized)

  dmon.quit = true
  joinThread(dmon.threadHandle)

  dispatch_release(dmon.threadSem)
  deinitLock(dmon.threadLock)

  for i in 0..<dmon.numWatches:
    if dmon.watches[i] != nil:
      unwatchState(dmon.watches[i])

  dmon = DmonState()
  dmonInitialized = false

proc watchDmon*(rootdir: string, watchCb: DmonWatchCallback,
                flags: uint32, userData: pointer): DmonWatchId =
  assert(dmonInitialized)
  assert(not rootdir.isEmptyOrWhitespace)
  assert(watchCb != nil)

  dmon.modifyWatches.store(1)
  withLock dmon.threadLock:
    assert(dmon.numWatches < 64)
    if dmon.numWatches >= 64:
      echo "Exceeding maximum number of watches"
      return DmonWatchId(0)

    let numFreeList = 64 - dmon.numWatches
    let index = dmon.freeList[numFreeList - 1]
    let id = uint32(index + 1)

    if dmon.watches[index] == nil:
      dmon.watches[index] = DmonWatchState()

    inc dmon.numWatches

    let watch = dmon.watches[id - 1]
    watch.id = DmonWatchId(id)
    watch.watchFlags = flags
    watch.watchCb = watchCb
    watch.userData = userData

    # Validate directory
    if not dirExists(rootdir):
      echo "Could not open/read directory: ", rootdir
      dec dmon.numWatches
      return DmonWatchId(0)

    # Handle symlinks
    var finalPath = rootdir
    if (flags and uint32(DmonWatchFlags.FollowSymlinks)) != 0:
      try:
        finalPath = expandSymlink(rootdir)
      except OSError:
        echo "Failed to resolve symlink: ", rootdir
        dec dmon.numWatches
        return DmonWatchId(0)

    # Setup watch path
    watch.rootdir = finalPath.normalizedPath
    if not watch.rootdir.endsWith("/"): 
      watch.rootdir.add "/"

    watch.rootdirUnmod = watch.rootdir
    watch.rootdir = watch.rootdir.toLowerAscii

    # Create FSEvents stream
    let cfPath = CFStringCreateWithCString(nil, watch.rootdirUnmod.cstring, kCFStringEncodingUTF8)
    defer: CFRelease(cfPath)

    let cfPaths = CFArrayCreate(nil, cast[ptr pointer](unsafeAddr cfPath), 1, nil)
    defer: CFRelease(cfPaths)

    var ctx = FSEventStreamContext(
      version: 0,
      info: cast[pointer](id),
      retain: nil,
      release: nil,
      copyDescription: nil
    )

    let streamFlags = FSEventStreamEventFlags(kFSEventStreamCreateFlagFileEvents)

    watch.fsEvStreamRef = FSEventStreamCreate(
      nil,                              # Use default allocator
      fsEventCallback,                  # Callback function
      addr ctx,                         # Context with watch ID
      cfPaths,                         # Array of paths to watch
      FSEventStreamEventId(0),         # Start from now
      0.25,                           # Latency in seconds
      streamFlags                     # File-level events
    )

    if watch.fsEvStreamRef.isNil:
      echo "Failed to create FSEvents stream"
      dec dmon.numWatches
      return DmonWatchId(0)

    dmon.modifyWatches.store(0)
    result = DmonWatchId(id)

proc unwatchDmon*(id: DmonWatchId) =
  assert(dmonInitialized)
  assert(uint32(id) > 0)

  let index = int(uint32(id) - 1)
  assert(index < 64)
  assert(dmon.watches[index] != nil)
  assert(dmon.numWatches > 0)

  if dmon.watches[index] != nil:
    dmon.modifyWatches.store(1)
    withLock dmon.threadLock:
      unwatchState(dmon.watches[index])
      dmon.watches[index] = nil

      dec dmon.numWatches
      let numFreeList = 64 - dmon.numWatches
      dmon.freeList[numFreeList - 1] = index

    dmon.modifyWatches.store(0)
0
Subscribe to my newsletter

Read articles from Jaremy Creechley directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jaremy Creechley
Jaremy Creechley