Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions deltachat-ffi/deltachat.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -3114,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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* which allows you see events emitted during startup.
* which allows you to see events emitted during startup.

*
* The account manager takes an directory
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* The account manager takes an directory
* The account manager takes a directory

Maybe it's better not to duplicate the documentation, but just add a reference to dc_account_new() and explain the difference. Otherwise API users need to read both and build a diff in their head, and we have more text to maintain.

* 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.
Expand Down Expand Up @@ -5999,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.
Comment on lines +6078 to +6080
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this still the case?, rust code looks like it allows multiple listeners: it uses and async broadcast.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks like it, probably it was forgotten in #5478 to update the documentation. But of course, the only way to be totally sure is actually trying it out.

*/
dc_event_emitter_t* dc_event_channel_get_event_emitter(dc_event_channel_t* event_channel);

/**
* @class dc_event_emitter_t
*
Expand Down
86 changes: 86 additions & 0 deletions deltachat-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -4785,6 +4786,91 @@ pub unsafe extern "C" fn dc_accounts_new(
}
}

pub type dc_event_channel_t = Cell<Option<Events>>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Cell is not thread-safe. If C side uses this from multiple threads, this can result in memory corruption when trying to write into the same Option multiple times. Better use Arc<Mutex<Option<Events>>> so it is not possible to create two account managers by initializing them at the same time.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I.e. Mutex instead of Cell and Arc instead of Box below. OTOH do we need thread safety here? We can just document that it's not thread-safe. By using Mutex we only add thread safety to the buggy scenario which shouldn't happen anyway.

Copy link
Collaborator

Choose a reason for hiding this comment

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

OTOH do we need thread safety here? We can just document that it's not thread-safe.

All CFFI API is declared as thread-safe so we don't have to think about which is safe and which is not:

* ## Thread safety
*
* All deltachat-core functions, unless stated otherwise, are thread-safe.
* In particular, it is safe to pass the same dc_context_t pointer
* to multiple functions running concurrently in different threads.

It's probably not really true that all code is thread-safe because we don't have Mutexes around Message object, but that's a bug in that code, let's not add more bugs.


#[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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

We write this as drop(Box::from_raw(event_channel)) everywhere else.

Copy link
Contributor Author

@Simon-Laux Simon-Laux Dec 18, 2025

Choose a reason for hiding this comment

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

yeah that's clearer, though "everywhere" is not quite correct, at-least not from the place I copied this from. could be a follow up pr to make sure it is the same everywhere, though I'm not sure how important it is for the existing calls.

}

#[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.
Expand Down
16 changes: 13 additions & 3 deletions src/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
if writable && !dir.exists() {
Accounts::create(&dir).await?;
}

Accounts::open(events, dir, writable).await
}

/// Get the ID used to log events.
Expand All @@ -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<Self> {
async fn open(events: Events, dir: PathBuf, writable: bool) -> Result<Self> {
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
Expand Down