Skip to content

Conversation

@trinitronx
Copy link
Contributor

@trinitronx trinitronx commented Dec 5, 2025

This is a bit of an overall refactor to use a singleton CecConnection which can be shared across threads.

The rationale behind doing this is to be able to share the CecConnection across the FFI boundary to any callback handlers (e.g. functions passed to CecConnectionCfg.command_received_callback()). These callback functions (e.g. on_command_received) were previously not able to utilize the CecConnection attached to the shared hardware CEC adapter. So they could not use it to respond to any unhandled CEC commands (libcec does respond automatically to most, but there are some exceptions).

In any case, the underlying lower level libcec_sys and libcec C code (used through FFI) will spawn any callback functions in separate threads. That poses the issue of how to share a CecConnection object in Rust across this boundary. Using this singleton pattern works and passes Rust's borrow and type checking, thanks to Arc being a thread-safe reference-counting pointer.

This should enable us to handle responding to CEC commands in the future within the on_command_received() callback.

Changes:

  • cargo: Add arrayvec 0.7.1

  • cargo fmt & Initial implementation of thread-local storage CecConnection handling

  • debug: Add global atomic thread counter

  • connection: Refactor conn handling w/static OnceLock<Option<Arc<CecConnection>>>
    Thread-local wasn't working to truly share the same connection across threads
    because the mutable borrow was failing. To complicate matters, CecConnection
    and CecConnectionCfg don't implement Copy or Clone traits, nor are they Sync.

    Option<Arc<..>> implements the Clone trait, so relying on this to store an
    Arc to be shared across threads works. Add to this using OnceLock for the
    global static variable to be initialized only once. Thus, we create a singleton
    pattern: the connection initialized in main() once becomes the single shared
    connection instance, for use in the on_command_received() handler function
    across libcec threads.

    Note: LazyLock was tried, but the initializer proc/closure was always failing to
    borrow and reliably access thread-local variables. Since initialization may have
    been called from multiple threads, any dereferencing call will block the calling
    thread if another initialization routine is currently running.

    The way that lower-level cec_rs and libcec-sys libraries implement the callback
    functions (e.g. ICECCallbacks::commandReceived) is to execute them in another
    thread. However, due to the way that Rust type & borrow checked code is run,
    we would otherwise lose access to the same CecConnection object across the ffi
    boundary:

    Rust main() -> libcec ffi C code -> thread(s?) -> on_command_received()
    

    Using this OnceLock singleton pattern with Option<Arc<...>> seems to work.

…nnection>>>

Thread-local wasn't working to truly share the _same_ connection across threads
because the mutable borrow was failing. To complicate matters, `CecConnection`
and `CecConnectionCfg` don't implement Copy or Clone traits, nor are they Sync.

`Option<Arc<..>>` implements the Clone trait, so relying on this to store an
Arc to be shared across threads works.  Add to this using `OnceLock` for the
global static variable to be initialized only once.  Thus, we create a singleton
pattern: the connection initialized in `main()` once becomes the single shared
connection instance, for use in the `on_command_received()` handler function
across libcec threads.

Note: LazyLock was tried, but the initializer proc/closure was always failing to
borrow and reliably access thread-local variables. Since initialization may have
been called from multiple threads, any dereferencing call will block the calling
thread if another initialization routine is currently running.

The way that lower-level cec_rs and libcec-sys libraries implement the callback
functions (e.g. `ICECCallbacks::commandReceived`) is to execute them in another
thread.  However, due to the way that Rust type & borrow checked code is run,
we would otherwise lose access to the same `CecConnection` object across the ffi
boundary:

    Rust main() -> libcec ffi C code -> thread(s?) -> on_command_received()

Using this `OnceLock` singleton pattern with `Option<Arc<...>>` seems to work.
@trinitronx trinitronx marked this pull request as ready for review December 5, 2025 10:03
@trinitronx trinitronx changed the title thread local storage global arc cec connection handling Thread-local storage global Arc CEC connection handling Dec 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant