From 8f3df8b10e884bfc43f70961f3e61a6a3be8f989 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 13 Dec 2025 16:50:26 +0100 Subject: [PATCH 01/10] api: cffi api to create account manager with existing events channel to see events emitted during startup. `dc_event_channel_new`, `dc_event_channel_unref`, `dc_event_channel_get_event_emitter` and `dc_accounts_new_with_event_channel` --- deltachat-ffi/deltachat.h | 81 ++++++++++++++++++++++++++++++++++++ deltachat-ffi/src/lib.rs | 86 +++++++++++++++++++++++++++++++++++++++ src/accounts.rs | 16 ++++++-- 3 files changed, 180 insertions(+), 3 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 31475263e1..f37e77f16c 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -3115,6 +3115,35 @@ int dc_receive_backup (dc_context_t* context, const char* qr); */ dc_accounts_t* dc_accounts_new (const char* dir, int writable); +/** + * Create a new account manager with an existing events channel, + * which allows you see events emitted during startup. + * + * The account manager takes an directory + * where all context-databases are placed in. + * To add a context to the account manager, + * use dc_accounts_add_account() or dc_accounts_migrate_account(). + * All account information are persisted. + * To remove a context from the account manager, + * use dc_accounts_remove_account(). + * + * @memberof dc_accounts_t + * @param dir The directory to create the context-databases in. + * If the directory does not exist, + * dc_accounts_new_with_event_channel() will try to create it. + * @param writable Whether the returned account manager is writable, i.e. calling these functions on + * it is possible: dc_accounts_add_account(), dc_accounts_add_closed_account(), + * dc_accounts_migrate_account(), dc_accounts_remove_account(), dc_accounts_select_account(). + * @param dc_event_channel_t Events Channel to be used for this accounts manager, + * create one with dc_event_channel_new(). + * This channel is consumed by this method and can not be used again afterwards, + * so be sure to call `dc_event_channel_get_event_emitter` before. + * @return An account manager object. + * The object must be passed to the other account manager functions + * and must be freed using dc_accounts_unref() after usage. + * On errors, NULL is returned. + */ +dc_accounts_t* dc_accounts_new_with_event_channel(const char* dir, int writable, dc_event_channel_t* events_channel); /** * Free an account manager object. @@ -6000,6 +6029,58 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance); */ char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input); +/** + * @class dc_event_channel_t + * + * Opaque object that is used to create an event emitter which can be used log events during startup of an accounts manger. + * Only used for dc_accounts_new_with_event_channel(). + * To use it: + * 1. create an events channel with `dc_event_channel_new()`. + * 2. get an event emitter for it with `dc_event_channel_get_event_emitter()`. + * 3. use it to create your account manager with `dc_accounts_new_with_event_channel()`, which consumes the channel. + * 4. free the empty channel wrapper object with `dc_event_channel_unref()`. + */ + + /** + * Create a new event channel. + * + * @memberof dc_event_channel_t + * @return An event channel wrapper object (dc_event_channel_t). + */ + dc_event_channel_t* dc_event_channel_new(); + + /** + * Release/free the events channel structure. + * This function releases the memory of the `dc_event_channel_t` structure. + * + * you can call it after calling dc_accounts_new_with_event_channel, + * which took the events channel out of it already, so this just frees the underlying option. + * + * @memberof dc_event_channel_t + */ +void dc_event_channel_unref(dc_event_channel_t* event_channel); + +/** + * Create the event emitter that is used to receive events. + * + * The library will emit various @ref DC_EVENT events, such as "new message", "message read" etc. + * To get these events, you have to create an event emitter using this function + * and call dc_get_next_event() on the emitter. + * + * This is similar to dc_get_event_emitter(), which, however, + * must not be called for accounts handled by the account manager. + * + * @memberof dc_event_channel_t + * @param The event channel. + * @return Returns the event emitter, NULL on errors. + * Must be freed using dc_event_emitter_unref() after usage. + * + * Note: Use only one event emitter per account manager. + * Having more than one event emitter running at the same time on the same account manager + * will result in events randomly delivered to the one or to the other. + */ +dc_event_emitter_t* dc_event_channel_get_event_emitter(dc_event_channel_t* event_channel); + /** * @class dc_event_emitter_t * diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 883d805a9e..b9f825319a 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -11,6 +11,7 @@ #[macro_use] extern crate human_panic; +use std::cell::Cell; use std::collections::BTreeMap; use std::convert::TryFrom; use std::fmt::Write; @@ -4764,6 +4765,91 @@ pub unsafe extern "C" fn dc_accounts_new( } } +pub type dc_event_channel_t = Cell>; + +#[no_mangle] +pub unsafe extern "C" fn dc_event_channel_new() -> *mut dc_event_channel_t { + Box::into_raw(Box::new(Cell::new(Some(Events::new())))) +} + +/// Release the events channel structure. +/// +/// This function releases the memory of the `dc_event_channel_t` structure. +/// +/// you can call it after calling dc_accounts_new_with_event_channel, +/// which took the events channel out of it already, so this just frees the underlying option. +#[no_mangle] +pub unsafe extern "C" fn dc_event_channel_unref(event_channel: *mut dc_event_channel_t) { + if event_channel.is_null() { + eprintln!("ignoring careless call to dc_event_channel_unref()"); + return; + } + let _ = Box::from_raw(event_channel); +} + +#[no_mangle] +pub unsafe extern "C" fn dc_event_channel_get_event_emitter( + event_channel: *mut dc_event_channel_t, +) -> *mut dc_event_emitter_t { + if event_channel.is_null() { + eprintln!("ignoring careless call to dc_event_channel_get_event_emitter()"); + return ptr::null_mut(); + } + + let Some(event_channel) = &*(*event_channel).as_ptr() else { + eprintln!( + "ignoring careless call to dc_event_channel_get_event_emitter() + -> channel was already consumed, make sure you call this before dc_accounts_new_with_event_channel" + ); + return ptr::null_mut(); + }; + + let emitter = event_channel.get_emitter(); + + Box::into_raw(Box::new(emitter)) +} + +#[no_mangle] +pub unsafe extern "C" fn dc_accounts_new_with_event_channel( + dir: *const libc::c_char, + writable: libc::c_int, + event_channel: *mut dc_event_channel_t, +) -> *mut dc_accounts_t { + setup_panic!(); + + if dir.is_null() || event_channel.is_null() { + eprintln!("ignoring careless call to dc_accounts_new_with_event_channel()"); + return ptr::null_mut(); + } + + // consuming channel enforce that you need to get the event emitter + // before initializing the account manager, + // so that you don't miss events/errors during initialisation. + // It also prevents you from using the same channel on multiple account managers. + let Some(event_channel) = (*event_channel).take() else { + eprintln!( + "ignoring careless call to dc_accounts_new_with_event_channel() + -> channel was already consumed" + ); + return ptr::null_mut(); + }; + + let accs = block_on(Accounts::new_with_events( + as_path(dir).into(), + writable != 0, + event_channel, + )); + + match accs { + Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))), + Err(err) => { + // We are using Anyhow's .context() and to show the inner error, too, we need the {:#}: + eprintln!("failed to create accounts: {err:#}"); + ptr::null_mut() + } + } +} + /// Release the accounts structure. /// /// This function releases the memory of the `dc_accounts_t` structure. diff --git a/src/accounts.rs b/src/accounts.rs index a0f93bdf8f..5d86668abb 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -60,8 +60,18 @@ impl Accounts { if writable && !dir.exists() { Accounts::create(&dir).await?; } + let events = Events::new(); + Accounts::open(events, dir, writable).await + } - Accounts::open(dir, writable).await + /// Loads or creates an accounts folder at the given `dir`. + /// Uses an existing events channel. + pub async fn new_with_events(dir: PathBuf, writable: bool, events: Events) -> Result { + if writable && !dir.exists() { + Accounts::create(&dir).await?; + } + + Accounts::open(events, dir, writable).await } /// Get the ID used to log events. @@ -85,14 +95,14 @@ impl Accounts { /// Opens an existing accounts structure. Will error if the folder doesn't exist, /// no account exists and no config exists. - async fn open(dir: PathBuf, writable: bool) -> Result { + async fn open(events: Events, dir: PathBuf, writable: bool) -> Result { ensure!(dir.exists(), "directory does not exist"); let config_file = dir.join(CONFIG_NAME); ensure!(config_file.exists(), "{config_file:?} does not exist"); let config = Config::from_file(config_file, writable).await?; - let events = Events::new(); + let stockstrings = StockStrings::new(); let push_subscriber = PushSubscriber::new(); let accounts = config From 15e171e0cb27ad8f65be4e6e384f23d4cfc017f8 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 13 Dec 2025 16:58:19 +0100 Subject: [PATCH 02/10] fix missing typedef for `dc_event_emitter_t` in `deltachat.h` --- deltachat-ffi/deltachat.h | 1 + 1 file changed, 1 insertion(+) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index f37e77f16c..7cd5707bce 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -22,6 +22,7 @@ typedef struct _dc_lot dc_lot_t; typedef struct _dc_provider dc_provider_t; typedef struct _dc_event dc_event_t; typedef struct _dc_event_emitter dc_event_emitter_t; +typedef struct _dc_event_channel dc_event_channel_t; typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t; typedef struct _dc_backup_provider dc_backup_provider_t; From 228c1bb52c2f88712f16a4b21192b1d9fa42395b Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 25 Dec 2025 14:24:57 +0100 Subject: [PATCH 03/10] use explicit drop in `dc_event_channel_unref` --- deltachat-ffi/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index b9f825319a..69152c0baa 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -4784,7 +4784,7 @@ pub unsafe extern "C" fn dc_event_channel_unref(event_channel: *mut dc_event_cha eprintln!("ignoring careless call to dc_event_channel_unref()"); return; } - let _ = Box::from_raw(event_channel); + drop(Box::from_raw(event_channel)) } #[no_mangle] From b468555fe6697016b635cb8fd0685de4ad641c76 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 25 Dec 2025 14:31:15 +0100 Subject: [PATCH 04/10] clarify warning about multiple event emitters --- deltachat-ffi/deltachat.h | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 7cd5707bce..78a22f8d17 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -3385,8 +3385,12 @@ void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const * Must be freed using dc_event_emitter_unref() after usage. * * Note: Use only one event emitter per account manager. - * Having more than one event emitter running at the same time on the same account manager - * will result in events randomly delivered to the one or to the other. + * The result of having multiple event emitters is unspecified. + * Currently events are broadcasted to all existing event emitters, + * but previous versions delivered events to only one event emitter + * and this behavior may change again in the future. + * Events emitted before creation of event emitter + * are not available to event emitter. */ dc_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts); @@ -6076,9 +6080,13 @@ void dc_event_channel_unref(dc_event_channel_t* event_channel); * @return Returns the event emitter, NULL on errors. * Must be freed using dc_event_emitter_unref() after usage. * - * Note: Use only one event emitter per account manager. - * Having more than one event emitter running at the same time on the same account manager - * will result in events randomly delivered to the one or to the other. + * Note: Use only one event emitter per account manager / event channel. + * The result of having multiple event emitters is unspecified. + * Currently events are broadcasted to all existing event emitters, + * but previous versions delivered events to only one event emitter + * and this behavior may change again in the future. + * Events emitted before creation of event emitter + * are not available to event emitter. */ dc_event_emitter_t* dc_event_channel_get_event_emitter(dc_event_channel_t* event_channel); From eaf4474f21cdc3064866f9b082a044def601c4ab Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 25 Dec 2025 14:33:41 +0100 Subject: [PATCH 05/10] fix typos --- deltachat-ffi/deltachat.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 78a22f8d17..b36b872d05 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -3094,7 +3094,7 @@ int dc_receive_backup (dc_context_t* context, const char* qr); /** * Create a new account manager. - * The account manager takes an directory + * The account manager takes a directory * where all context-databases are placed in. * To add a context to the account manager, * use dc_accounts_add_account() or dc_accounts_migrate_account(). @@ -3118,9 +3118,9 @@ dc_accounts_t* dc_accounts_new (const char* dir, int writable); /** * Create a new account manager with an existing events channel, - * which allows you see events emitted during startup. + * which allows you to see events emitted during startup. * - * The account manager takes an directory + * The account manager takes a directory * where all context-databases are placed in. * To add a context to the account manager, * use dc_accounts_add_account() or dc_accounts_migrate_account(). From 2ed5a185eb534e901629307b2106bdc99f46570f Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 31 Dec 2025 15:58:17 +0100 Subject: [PATCH 06/10] cell to arc mutex --- deltachat-ffi/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 69152c0baa..164ef29185 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -11,7 +11,6 @@ #[macro_use] extern crate human_panic; -use std::cell::Cell; use std::collections::BTreeMap; use std::convert::TryFrom; use std::fmt::Write; @@ -40,7 +39,7 @@ use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession}; use message::Viewtype; use num_traits::{FromPrimitive, ToPrimitive}; use tokio::runtime::Runtime; -use tokio::sync::RwLock; +use tokio::sync::{Mutex, RwLock}; use tokio::task::JoinHandle; mod dc_array; @@ -4765,11 +4764,12 @@ pub unsafe extern "C" fn dc_accounts_new( } } -pub type dc_event_channel_t = Cell>; +pub type dc_event_channel_t = Arc>>; #[no_mangle] pub unsafe extern "C" fn dc_event_channel_new() -> *mut dc_event_channel_t { - Box::into_raw(Box::new(Cell::new(Some(Events::new())))) + let events = Arc::new(Mutex::new(Some(Events::new()))); + Box::into_raw(Box::new(events)) } /// Release the events channel structure. @@ -4796,7 +4796,7 @@ pub unsafe extern "C" fn dc_event_channel_get_event_emitter( return ptr::null_mut(); } - let Some(event_channel) = &*(*event_channel).as_ptr() else { + let Some(event_channel) = &*(*event_channel).blocking_lock() else { eprintln!( "ignoring careless call to dc_event_channel_get_event_emitter() -> channel was already consumed, make sure you call this before dc_accounts_new_with_event_channel" @@ -4826,7 +4826,7 @@ pub unsafe extern "C" fn dc_accounts_new_with_event_channel( // before initializing the account manager, // so that you don't miss events/errors during initialisation. // It also prevents you from using the same channel on multiple account managers. - let Some(event_channel) = (*event_channel).take() else { + let Some(event_channel) = (*event_channel).clone().blocking_lock_owned().take() else { eprintln!( "ignoring careless call to dc_accounts_new_with_event_channel() -> channel was already consumed" From 67cbe08f11a1ca29ae34ee5c76c96e81ae2dba96 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 31 Dec 2025 16:03:26 +0100 Subject: [PATCH 07/10] remove the arc to simplify --- deltachat-ffi/src/lib.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 164ef29185..df8720fae0 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -4764,12 +4764,11 @@ pub unsafe extern "C" fn dc_accounts_new( } } -pub type dc_event_channel_t = Arc>>; +pub type dc_event_channel_t = Mutex>; #[no_mangle] pub unsafe extern "C" fn dc_event_channel_new() -> *mut dc_event_channel_t { - let events = Arc::new(Mutex::new(Some(Events::new()))); - Box::into_raw(Box::new(events)) + Box::into_raw(Box::new(Mutex::new(Some(Events::new())))) } /// Release the events channel structure. @@ -4826,7 +4825,7 @@ pub unsafe extern "C" fn dc_accounts_new_with_event_channel( // before initializing the account manager, // so that you don't miss events/errors during initialisation. // It also prevents you from using the same channel on multiple account managers. - let Some(event_channel) = (*event_channel).clone().blocking_lock_owned().take() else { + let Some(event_channel) = (*event_channel).blocking_lock().take() else { eprintln!( "ignoring careless call to dc_accounts_new_with_event_channel() -> channel was already consumed" From 89872b6c518a0678f596c5fe9ff2490dfb1af412 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 8 Jan 2026 18:25:56 +0100 Subject: [PATCH 08/10] use standard mutex --- deltachat-ffi/src/lib.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index df8720fae0..591394888a 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -17,7 +17,7 @@ use std::fmt::Write; use std::future::Future; use std::ptr; use std::str::FromStr; -use std::sync::{Arc, LazyLock}; +use std::sync::{Arc, LazyLock, Mutex}; use std::time::{Duration, SystemTime}; use anyhow::Context as _; @@ -39,7 +39,7 @@ use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession}; use message::Viewtype; use num_traits::{FromPrimitive, ToPrimitive}; use tokio::runtime::Runtime; -use tokio::sync::{Mutex, RwLock}; +use tokio::sync::RwLock; use tokio::task::JoinHandle; mod dc_array; @@ -4795,7 +4795,10 @@ pub unsafe extern "C" fn dc_event_channel_get_event_emitter( return ptr::null_mut(); } - let Some(event_channel) = &*(*event_channel).blocking_lock() else { + let Some(event_channel) = &*(*event_channel) + .lock() + .expect("call to dc_event_channel_get_event_emitter() failed: mutex is poisoned") + else { eprintln!( "ignoring careless call to dc_event_channel_get_event_emitter() -> channel was already consumed, make sure you call this before dc_accounts_new_with_event_channel" From 626988988965ae00b0bcf11d6b05c55a990cfe82 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 8 Jan 2026 18:40:13 +0100 Subject: [PATCH 09/10] fix last commit --- deltachat-ffi/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 591394888a..b8a3da2e68 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -4828,7 +4828,11 @@ pub unsafe extern "C" fn dc_accounts_new_with_event_channel( // before initializing the account manager, // so that you don't miss events/errors during initialisation. // It also prevents you from using the same channel on multiple account managers. - let Some(event_channel) = (*event_channel).blocking_lock().take() else { + let Some(event_channel) = (*event_channel) + .lock() + .expect("call to dc_event_channel_get_event_emitter() failed: mutex is poisoned") + .take() + else { eprintln!( "ignoring careless call to dc_accounts_new_with_event_channel() -> channel was already consumed" From 1036c3a1cd8dbb8114017130026ca54be1aec295 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 8 Jan 2026 18:55:40 +0100 Subject: [PATCH 10/10] adapt to changes from #7662 --- deltachat-ffi/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index b8a3da2e68..ac5b11b50f 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -4816,7 +4816,7 @@ pub unsafe extern "C" fn dc_accounts_new_with_event_channel( dir: *const libc::c_char, writable: libc::c_int, event_channel: *mut dc_event_channel_t, -) -> *mut dc_accounts_t { +) -> *const dc_accounts_t { setup_panic!(); if dir.is_null() || event_channel.is_null() { @@ -4847,7 +4847,7 @@ pub unsafe extern "C" fn dc_accounts_new_with_event_channel( )); match accs { - Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))), + Ok(accs) => Arc::into_raw(Arc::new(RwLock::new(accs))), Err(err) => { // We are using Anyhow's .context() and to show the inner error, too, we need the {:#}: eprintln!("failed to create accounts: {err:#}");