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

Table of contents

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)
Subscribe to my newsletter
Read articles from Jaremy Creechley directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
