unit icon indicating copy to clipboard operation
unit copied to clipboard

RUST: Questions about the C API

Open andreivasiliu opened this issue 2 years ago • 13 comments

Hi! I'm trying to make some unofficial Rust bindings for libunit.a (see unit-rs), and I have some questions about the C API.

Right now my bindings are pretty bad. It seems there's several things I got wrong and I might have several misconceptions that are making my bindings a lot more restrictive than they should be. Still, I am really impressed with the technical aspects of Unit and how it works, and it's been really fun to work with it, and I'd like to rework my bindings to better match the API's capabilities.

If anyone answers any of these questions, I'd like to improve the descriptions in the nxt_unit.h header. Would a PR for that be accepted?

The questions:

Multi-threading and thread local storage

Assuming that contexts and requests are only accessed with a locked mutex, can Unit's C API functions be called from a different thread than the thread which created the context/request? In other words, do the context/request objects rely on thread-specific things like variables in thread-local-storage?

More specifically...

nxt_unit_init() returns a context object that must then be destroyed with nxt_unit_done(). Can nxt_unit_done() be called from a different thread?

nxt_unit_ctx_alloc() creates a secondary context based on the main context. Can nxt_unit_done() be called from a different thread than the one which created the context?

Can nxt_unit_run() be called on a different thread than the one which created the context?

The request_handler() callback will be called on the thread that runs nxt_unit_run(), and it will be given a request object. Can methods that use this request object (like nxt_unit_response_send, nxt_unit_response_buf_alloc, etc) be called on a different thread than the one which received the request object?

If I get a request from nxt_unit_dequeue_request(), can I send that request to a different thread and call API functions on it there?

Request body streaming

From my experiments, Unit supports a max of 8MB bodies, buffers the whole body, and then calls this data_handler() callback at most once. Is that correct, or should I expect it to be called multiple times for slow-writing clients?

Also from my experiments, if data_handler() is to be called, then before that, in request_handler(), the nxt_unit_request_read() API always returns 0 bytes. Is that always the case? Does nxt_unit_request_read() always return all or nothing? Or can I expect partial results?

I don't see blocking/non-blocking variants for nxt_unit_request_read(). Can I safely assume that nxt_unit_request_read() is always non-blocking?

Is the NXT_UNIT_AGAIN error code related in any way to the above?

Is the nxt_unit_app_test.c example incorrect for requests with large request bodies?

Clean shutdown

Let's say a thread wants to quit (e.g. it experienced a fatal error). Is my only option to exit() the process? Is there any way to trigger a graceful shutdown of this process, so that all other threads can finish whatever request they are handling, and then be given a QUIT message?

Also, what happens if nxt_unit_done() is called on the main context when there are still secondary contexts created from the main one? Will they cleanly shut down, or is this undefined behavior?

Does the main context have to live for at least as long as the contexts spawned from it, or can it be done'd earlier?

Request response buffers

Can nxt_unit_response_buf_alloc() be called multiple times before sending one of the buffers? In other words, can multiple buffers exist at the same time?

Can I send response buffers in reverse order?

What is nxt_unit_buf_next() for? Does its result affect nxt_unit_buf_send() in any way?

Is it safe to call nxt_unit_request_done() on a request before sending or deallocating all of the buffers? If yes, will the buffers be automatically deallocated?

Since there is a non-blocking version of nxt_unit_response_write(), then I assume nxt_unit_response_write() is the blocking variant. When this blocks, the entire thread will be unavailable to process other requests. Is this vulnerable to clients with slow-reading, or will the Unit server accept and buffer the whole response even if the client doesn't read it?

Does nxt_unit_buf_send() block? If yes, is it susceptible to slow-reading clients? Does it ever return NXT_UNIT_AGAIN?

Misc questions

When is the close_handler() callback ever called? Is that only for websockets?

How do nxt_unit_run(), nxt_unit_run_ctx(), and nxt_unit_run_shared() differ?

If I call nxt_unit_malloc() on one context, can I call nxt_unit_free() on a different context?

What is NXT_UNIT_AGAIN for, and what returns this? Can I return or send this myself from anywhere?

andreivasiliu avatar Jul 22 '22 11:07 andreivasiliu

Hi @andreivasiliu – First, THANK YOU VERY MUCH for working on the initial Rust bindings and sorry for the long delay! As far as I can see you created the Rust bindings manually.

Did you tried to auto generate the Rust bindings from the header files. I have played around with this and would like to get your feedback on this.

Furthermore, there is a Scala Implementation of the same Unit API (Likewise in Go and NodeJS). Maybe we can find some answers to your questions while looking into this code. I would like to talk with @hongzhidao, @hongzhidao and @ac000 about your questions. Gentlemen, please feel free to pick a question and share your thoughts. Will do the same.

The Rust bindings and the possibilities we will have with those are a great step into the right direction to a more widely adoption! looking forward to see this issue grow and be filled with a ton of useful information.

tippexs avatar Jul 27 '22 12:07 tippexs

As far as I can see you created the Rust bindings manually.

They are created automatically, based on nxt_unit.h from unit-dev (see wrapper.h), and bindgen (see build.rs).

However, they are only used internally, since the generated bindings are very unsafe to use directly from Rust. The generated bindings use raw pointers that behave like C pointers; Rust code can only use these through the use of unsafe, hence the need tor a safe wrapper around them in order to turn them into APIs that match Rust's much stronger memory guarantees (lifetimes, thread safety, unwind safety, etc).

The Rust bindings and the possibilities we will have with those are a great step into the right direction to a more widely adoption! looking forward to see this issue grow and be filled with a ton of useful information.

Thank you very much!

andreivasiliu avatar Jul 27 '22 14:07 andreivasiliu

Got your point with the bindings and sorry for missing it in my inital review of your repo. So the goal is clear: Having a stable and reliable Rust wrapper around the C-API bindings. I will have a chat with the other engineers to answer the questions you have just posted and come back with answers asap!

tippexs avatar Jul 28 '22 13:07 tippexs