Skip to content

Conversation

2-towns
Copy link
Contributor

@2-towns 2-towns commented Sep 9, 2025

Just opening a draft PR to get early reviews

@2-towns 2-towns changed the title feature: add c binding feat: add c binding Sep 9, 2025
@2-towns 2-towns changed the title feat: add c binding feature: add c binding Sep 9, 2025
@2-towns 2-towns requested review from gmega and emizzle September 9, 2025 18:38
proc sendRequestToCodexThread*(
ctx: ptr CodexContext,
reqType: RequestType,
reqContent: pointer,
Copy link
Contributor

@gmega gmega Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... how will this work when we have to upload data to Codex? I suppose reqContent is meant to be a string or byte array (and not, say, a file descriptor), so we will probably have to constrain loading to happen from files and pass a local path here? Also wondering what'll be the best approach for a download API...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I didn't think deeply to that yet, but maybe we could have some kind of steam mode where the reqContent is a Stream or Reader, so the worker will read small chunks of the files until it is fully uploaded.
And for the download, opposite direction with Writer.

I am not sure of how to do it with Nim + C binding, but in Javascript I would try to consume the data in that way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reqContent is more for describing the codex-local operation (eg start node).
userData I believe is what should be used for passing data. It is a pointer,
which could point to a file path or a seq of bytes (assuming it is not copied).

Copy link
Contributor Author

@2-towns 2-towns Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reqContent is more for describing the codex-local operation (eg start node). userData I believe is what should be used for passing data. It is a pointer, which could point to a file path or a seq of bytes (assuming it is not copied).

If you look at sendRequestToCodexThread, the third parameter is the reqContent, which is in the case of Lifecycle request :

    NodeLifecycleRequest.createShared(
      NodeLifecycleMsgType.CREATE_NODE, configJson
    ),

It creates a request shared object that contains the json config provided by the Go application. So I think the reqContent contains the actual data passed by the caller.

The userData is a bit confusing. It looks like a context that will be used when the Go app receives the callback. It is not intended to be used by the Nim code, but by the Go code when the callback is invoked.
You can have a global userData context plus a userData context per call. I’m not sure how useful this is for Codex right now.

# between threads assumes that there aren't concurrent requests.
# Rearchitecting the signaling + migrating to a MP Channel will allow us to receive
# requests concurrently and spare us the need of locks
lock: Lock
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this is a big deal as requests run async and the actual sending of the request to the Codex thread should be relatively fast, plus this shouldn't be a very "hot" API anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The only issue arises if you have multiple application threads, meaning multiple calls to sendRequestToCodexThread. In that case, the request channel may receive several requests, but currently the worker thread only processes one request per wakeup and sends an ACK to the first application thread:

    # Pop a request from the channel
    let recvOk = ctx.reqChannel.tryRecv(request)
    if not recvOk:
      error "codex thread could not receive a request"
      continue

So we could be in a situation where the second application thread could end up waiting indefinitely for an ACK from the Codex thread.

of LIFECYCLE:
cast[ptr NodeLifecycleRequest](request[].reqContent).process(codex)

handleRes(await retFut, request)
Copy link
Contributor

@gmega gmega Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... does this mean we are calling back the C code directly from the Chronos thread?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To summarize the discussion during the meeting:

The callbacks are currently executed on the Chronos thread (= working thread). This can be an issue if a callback is slow, because the Chronos thread will block and other requests cannot be processed.

One solution would be to introduce a dispatcher thread with a queue for events and a dedicated thread that dequeues them and runs the callbacks.

Another potential approach would be to move to a Multi Producer channel instead of the current Single Producer channel. This would allow multiple threads to enqueue requests concurrently, so even if one thread is blocked by a callback, others can continue processing requests.

deallocShared(request)

if res.isErr():
foreignThreadGc:
Copy link
Contributor

@gmega gmega Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this? Isn't handleRes being called from the Chronos thread?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I’m not sure that’s always the case, especially when a Go routine is involved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean from what I understood, Go routines will run threads that are considered as foreign by Nim so that my be an issue with the GC.

Copy link
Contributor

@emizzle emizzle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely looks like it's going in the right direction! I tried it out locally and it worked well!

Just a few suggestions/small comments even though I know this is a draft. I can imagine you were saving exception handling for "clean up". Best for last right 😂

let count = parseInt(input)
if count != 0 and count < 2:
warn "Invalid number of threads", input = input
proc parse*(T: type ThreadCount, p: string): Result[ThreadCount, string] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably pull all these out into a parser module, so that you can import the parser module from conf and egnode_lifecycle_request. This will prevent the need to import conf from node_lifecycle_request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you will still need to import CodexConf from conf. But yeah I can import only CodexConf and move the parsing method into a parser.nim as you suggested.

Comment on lines +58 to +65
try:
updateLogLevel(config.logLevel)
except ValueError as err:
try:
stderr.write "Invalid value for --log-level. " & err.msg & "\n"
except IOError:
echo "Invalid value for --log-level. " & err.msg
quit QuitFailure
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this already called in setupLogging?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed it from the setupLogging because the compiler was complaining about gcsafe issue.

cast[CodexCallback](ctx[].eventCallback)(
RET_OK, unsafeAddr event[0], cast[csize_t](len(event)), ctx[].eventUserData
)
except Exception, CatchableError:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming that Exception had to be caught to get this to compile? Could be that there is no way to avoid Defects being raised in the call path.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I am not sure this template is not called yet :). But I will remove the Exception for now and add it back if it is needed.

proc readValue*(r: var JsonReader, val: var MultiAddress) =
val = MultiAddress.init(r.readValue(string)).get()

proc readValue*(r: var JsonReader, val: var NatConfig) =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like these deserialisations are actually quite generic and should maybe live elsewhere. In ethers, we redirect all de/serializations to nim-serde instead:

import pkg/serde/json

## Generic conversions to use nim-serde instead of nim-json-serialization for
## json rpc serialization purposes
##  writeValue => `%`
##  readValue  => fromJson

proc writeValue*[T: not JsonNode](
  writer: var JsonWriter[JrpcConv],
  value: T) {.raises:[IOError].} =

  writer.writeValue(%value)

proc readValue*[T: not JsonNode](
  r: var JsonReader[JrpcConv],
  result: var T) {.raises: [SerializationError, IOError].} =

  var json = r.readValue(JsonNode)
  result = T.fromJson(json).getOrRaise(SerializationError)

proc sendRequestToCodexThread*(
ctx: ptr CodexContext,
reqType: RequestType,
reqContent: pointer,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reqContent is more for describing the codex-local operation (eg start node).
userData I believe is what should be used for passing data. It is a pointer,
which could point to a file path or a seq of bytes (assuming it is not copied).

## process proc. See the 'codex_thread_request.nim' module for more details.
ok()

proc runCodex(ctx: ptr CodexContext) {.async.} =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should also not raise errors inside this proc otherwise it may silently
crash the thread?

return callback.success("", userData)

proc codex_upload_init(
ctx: ptr CodexContext, filepath: cstring, callback: CodexCallback, userData: pointer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens with the filepath if you're uploading via chunks? Is it not used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the filepath can be obviously the absolute file path when you upload a file.
But when you upload with chunks, it is used as filename. Then the node.store will use it for the filename and the mimetype.

@2-towns 2-towns changed the title feature: add c binding feat: add c binding Sep 30, 2025
@github-actions github-actions bot added the feat label Oct 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement a Codex library so that clients can use Codex directly from their apps
3 participants