mod inner;
mod io;
mod tls;
mod types;
pub use types::{
ConnectOptions, ConnectResult, MqttMessage, MqttMessageListener, SubscribeResult, TlsOptions,
WillOptions,
};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Weak};
use std::time::Duration;
use bytes::{Bytes, BytesMut};
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex as AsyncMutex;
use crate::error::{MqttError, MqttResult};
use crate::protocol::{
ack::SimpleAck,
packet::{DisconnectPacket, Packet},
properties::Properties,
publish::PublishPacket,
subscribe::{SubAckReasonCode, SubscribeFilter, SubscribePacket, UnsubscribePacket},
QoS,
};
use inner::{register_pending, unexpected, wait_for, Inner};
use io::{build_connect_packet, keepalive_loop, read_loop, read_one_packet, send_packet};
const MAX_PUBLISH_RETRIES: u32 = 3;
const RECONNECT_POLL_INTERVAL: Duration = Duration::from_millis(250);
struct ClientShared {
options: ConnectOptions,
inner: AsyncMutex<Option<Arc<Inner>>>,
pending_listener: std::sync::Mutex<Option<Arc<dyn MqttMessageListener>>>,
subscriptions: std::sync::Mutex<HashMap<String, QoS>>,
user_disconnected: AtomicBool,
}
impl ClientShared {
async fn connected_inner(&self) -> MqttResult<Arc<Inner>> {
let guard = self.inner.lock().await;
match guard.as_ref() {
Some(inner) if inner.connected.load(Ordering::Relaxed) => Ok(inner.clone()),
_ => Err(MqttError::NotConnected),
}
}
}
#[derive(uniffi::Object)]
pub struct MqttClient {
shared: Arc<ClientShared>,
}
#[uniffi::export(async_runtime = "tokio")]
impl MqttClient {
#[uniffi::constructor]
pub fn new(options: ConnectOptions) -> Self {
MqttClient {
shared: Arc::new(ClientShared {
options,
inner: AsyncMutex::new(None),
pending_listener: std::sync::Mutex::new(None),
subscriptions: std::sync::Mutex::new(HashMap::new()),
user_disconnected: AtomicBool::new(false),
}),
}
}
pub fn set_message_listener(&self, listener: Arc<dyn MqttMessageListener>) {
if let Ok(guard) = self.shared.inner.try_lock() {
if let Some(inner) = guard.as_ref() {
*inner.listener.lock().unwrap() = Some(listener);
return;
}
}
self.shared
.pending_listener
.lock()
.unwrap()
.replace(listener);
}
pub async fn connect(&self) -> MqttResult<ConnectResult> {
self.shared
.user_disconnected
.store(false, Ordering::Relaxed);
let result = connect_once(&self.shared).await?;
if self.shared.options.auto_reconnect {
spawn_reconnect_supervisor(Arc::downgrade(&self.shared));
}
Ok(result)
}
pub fn is_connected(&self) -> bool {
match self.shared.inner.try_lock() {
Ok(guard) => guard
.as_ref()
.map(|i| i.connected.load(Ordering::Relaxed))
.unwrap_or(false),
Err(_) => false,
}
}
pub async fn publish(
&self,
topic: String,
payload: Vec<u8>,
qos: QoS,
retain: bool,
) -> MqttResult<()> {
let inner = self.shared.connected_inner().await?;
let payload = Bytes::from(payload);
match qos {
QoS::AtMostOnce => {
let pkt = Packet::Publish(PublishPacket {
dup: false,
qos,
retain,
topic,
packet_id: None,
payload,
properties: Properties::new(),
});
send_packet(&inner, &pkt).await
}
QoS::AtLeastOnce => {
let id = inner.alloc_packet_id();
let mut dup = false;
let mut attempts: u32 = 0;
loop {
let rx = register_pending(&inner, id);
let pkt = Packet::Publish(PublishPacket {
dup,
qos,
retain,
topic: topic.clone(),
packet_id: Some(id),
payload: payload.clone(),
properties: Properties::new(),
});
send_packet(&inner, &pkt).await?;
match wait_for(&inner, rx).await {
Ok(Packet::PubAck(_)) => return Ok(()),
Ok(other) => return Err(unexpected(&other)),
Err(MqttError::Timeout) if attempts < MAX_PUBLISH_RETRIES => {
attempts += 1;
dup = true;
}
Err(e) => return Err(e),
}
}
}
QoS::ExactlyOnce => {
let id = inner.alloc_packet_id();
let mut dup = false;
let mut attempts: u32 = 0;
loop {
let rx = register_pending(&inner, id);
let pkt = Packet::Publish(PublishPacket {
dup,
qos,
retain,
topic: topic.clone(),
packet_id: Some(id),
payload: payload.clone(),
properties: Properties::new(),
});
send_packet(&inner, &pkt).await?;
match wait_for(&inner, rx).await {
Ok(Packet::PubRec(_)) => break,
Ok(other) => return Err(unexpected(&other)),
Err(MqttError::Timeout) if attempts < MAX_PUBLISH_RETRIES => {
attempts += 1;
dup = true;
}
Err(e) => return Err(e),
}
}
let mut attempts: u32 = 0;
loop {
let rx2 = register_pending(&inner, id);
let rel = Packet::PubRel(SimpleAck::success(id));
send_packet(&inner, &rel).await?;
match wait_for(&inner, rx2).await {
Ok(Packet::PubComp(_)) => return Ok(()),
Ok(other) => return Err(unexpected(&other)),
Err(MqttError::Timeout) if attempts < MAX_PUBLISH_RETRIES => {
attempts += 1;
}
Err(e) => return Err(e),
}
}
}
}
}
pub async fn subscribe(&self, topic_filter: String, qos: QoS) -> MqttResult<SubscribeResult> {
let inner = self.shared.connected_inner().await?;
let id = inner.alloc_packet_id();
let rx = register_pending(&inner, id);
let pkt = Packet::Subscribe(SubscribePacket {
packet_id: id,
filters: vec![SubscribeFilter::new(topic_filter.clone(), qos)],
properties: Properties::new(),
});
send_packet(&inner, &pkt).await?;
match wait_for(&inner, rx).await? {
Packet::SubAck(ack) => {
let code = ack
.reason_codes
.first()
.copied()
.unwrap_or(SubAckReasonCode::FAILURE);
if code.0 < 0x80 {
self.shared
.subscriptions
.lock()
.unwrap()
.insert(topic_filter, qos);
}
Ok(SubscribeResult {
reason_code: code.0,
})
}
other => Err(unexpected(&other)),
}
}
pub async fn unsubscribe(&self, topic_filter: String) -> MqttResult<()> {
let inner = self.shared.connected_inner().await?;
let id = inner.alloc_packet_id();
let rx = register_pending(&inner, id);
let pkt = Packet::Unsubscribe(UnsubscribePacket {
packet_id: id,
topic_filters: vec![topic_filter.clone()],
properties: Properties::new(),
});
send_packet(&inner, &pkt).await?;
match wait_for(&inner, rx).await? {
Packet::UnsubAck(_) => {
self.shared
.subscriptions
.lock()
.unwrap()
.remove(&topic_filter);
Ok(())
}
other => Err(unexpected(&other)),
}
}
pub async fn disconnect(&self) -> MqttResult<()> {
self.shared.user_disconnected.store(true, Ordering::Relaxed);
let mut guard = self.shared.inner.lock().await;
let Some(inner) = guard.take() else {
return Ok(());
};
inner.connected.store(false, Ordering::Relaxed);
let disconnect = Packet::Disconnect(DisconnectPacket::normal());
let _ = send_packet(&inner, &disconnect).await;
if let Some(mut w) = inner.writer.lock().await.take() {
let _ = w.shutdown().await;
}
if let Some(h) = inner.read_task.lock().await.take() {
h.abort();
}
if let Some(h) = inner.keepalive_task.lock().await.take() {
h.abort();
}
Ok(())
}
}
impl Drop for MqttClient {
fn drop(&mut self) {
let Ok(mut guard) = self.shared.inner.try_lock() else {
return;
};
let Some(inner) = guard.take() else {
return;
};
inner.connected.store(false, Ordering::Relaxed);
if let Ok(mut writer) = inner.writer.try_lock() {
writer.take();
}
if let Ok(mut task) = inner.read_task.try_lock() {
if let Some(handle) = task.take() {
handle.abort();
}
};
if let Ok(mut task) = inner.keepalive_task.try_lock() {
if let Some(handle) = task.take() {
handle.abort();
}
};
}
}
fn build_connect_packet_wrapper(options: &ConnectOptions) -> Packet {
Packet::Connect(build_connect_packet(options))
}
async fn connect_once(shared: &Arc<ClientShared>) -> MqttResult<ConnectResult> {
let mut guard = shared.inner.lock().await;
if guard
.as_ref()
.is_some_and(|i| i.connected.load(Ordering::Relaxed))
{
return Err(MqttError::AlreadyConnected);
}
let options = &shared.options;
let connect_fut = tls::connect_transport(options);
let stream: Box<dyn tls::Transport> = tokio::time::timeout(
Duration::from_secs(options.connect_timeout_secs.max(1) as u64),
connect_fut,
)
.await
.map_err(|_| MqttError::Timeout)??;
let (mut reader, mut writer) = tokio::io::split(stream);
let encoded = build_connect_packet_wrapper(options)
.encode(options.version)
.map_err(|e| MqttError::Protocol(e.to_string()))?;
writer.write_all(&encoded).await?;
let mut buf = BytesMut::with_capacity(1024);
let connack = read_one_packet(
&mut reader,
&mut buf,
options.version,
options.connect_timeout_secs,
)
.await?;
let connack = match connack {
Packet::ConnAck(ack) => ack,
other => {
return Err(MqttError::MalformedPacket(format!(
"expected CONNACK, got {other:?}"
)))
}
};
if !connack.reason_code.is_success() {
return Err(MqttError::ConnectionRefused(format!(
"reason code 0x{:02X}",
connack.reason_code.0
)));
}
let listener = {
let mut pending = shared.pending_listener.lock().unwrap();
pending.take().or_else(|| {
guard
.as_ref()
.and_then(|old| old.listener.lock().unwrap().clone())
})
};
let inner = Arc::new(Inner::new(
options.version,
writer,
listener,
options.operation_timeout(),
));
let read_handle = tokio::spawn(read_loop(inner.clone(), reader, buf));
*inner.read_task.lock().await = Some(read_handle);
if options.keep_alive_secs > 0 {
let ka_handle = tokio::spawn(keepalive_loop(inner.clone(), options.keep_alive_secs));
*inner.keepalive_task.lock().await = Some(ka_handle);
}
*guard = Some(inner);
Ok(ConnectResult {
session_present: connack.session_present,
reason_code: connack.reason_code.0,
})
}
async fn replay_subscriptions(shared: &Arc<ClientShared>) {
let subs: Vec<(String, QoS)> = shared
.subscriptions
.lock()
.unwrap()
.iter()
.map(|(topic, qos)| (topic.clone(), *qos))
.collect();
let Ok(inner) = shared.connected_inner().await else {
return;
};
for (topic_filter, qos) in subs {
let id = inner.alloc_packet_id();
let rx = register_pending(&inner, id);
let pkt = Packet::Subscribe(SubscribePacket {
packet_id: id,
filters: vec![SubscribeFilter::new(topic_filter.clone(), qos)],
properties: Properties::new(),
});
if send_packet(&inner, &pkt).await.is_err() {
tracing::warn!(topic = %topic_filter, "re-subscribe failed while reconnecting");
return;
}
if wait_for(&inner, rx).await.is_err() {
tracing::warn!(topic = %topic_filter, "re-subscribe was not acked while reconnecting");
}
}
}
fn spawn_reconnect_supervisor(shared: Weak<ClientShared>) {
tokio::spawn(async move {
loop {
loop {
let Some(strong) = shared.upgrade() else {
return;
};
let is_connected = {
let guard = strong.inner.lock().await;
guard
.as_ref()
.map(|i| i.connected.load(Ordering::Relaxed))
.unwrap_or(false)
};
drop(strong);
if !is_connected {
break;
}
tokio::time::sleep(RECONNECT_POLL_INTERVAL).await;
}
let Some(strong) = shared.upgrade() else {
return;
};
if strong.user_disconnected.load(Ordering::Relaxed) {
return;
}
let mut backoff = strong.options.initial_reconnect_backoff();
let max_backoff = strong.options.max_reconnect_backoff();
drop(strong);
loop {
tokio::time::sleep(backoff).await;
let Some(strong) = shared.upgrade() else {
return;
};
if strong.user_disconnected.load(Ordering::Relaxed) {
return;
}
match connect_once(&strong).await {
Ok(_) => {
replay_subscriptions(&strong).await;
break;
}
Err(e) => {
tracing::debug!(error = %e, ?backoff, "reconnect attempt failed");
backoff = (backoff * 2).min(max_backoff);
}
}
}
}
});
}