Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SDK docs #172

Merged
merged 15 commits into from
Feb 4, 2025
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
.mypy_cache/
.pytest_cache/
.yarn/
.idea/
*.py[cod]
/python/**/dist/
/python/foxglove-schemas-flatbuffer/foxglove_schemas_flatbuffer/*.bfbs
Expand Down
4 changes: 2 additions & 2 deletions rust/foxglove-proto-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ fn generate_impls(out_dir: &Path, fds: &FileDescriptorSet) -> anyhow::Result<()>
module,
"use crate::schemas::{{descriptors, foxglove::*}};"
));
result = result.and(writeln!(module, "use crate::{{Schema, TypedMessage}};"));
result = result.and(writeln!(module, "use crate::{{Schema, Encode}};"));
result = result.and(writeln!(module, "use bytes::BufMut;"));
result.context("Failed to write impls.rs")?;

Expand All @@ -149,7 +149,7 @@ fn generate_impls(out_dir: &Path, fds: &FileDescriptorSet) -> anyhow::Result<()>
let descriptor_name = camel_case_to_constant_case(name);
writeln!(
module,
"\nimpl TypedMessage for {name} {{
"\nimpl Encode for {name} {{
type Error = ::prost::EncodeError;

fn get_schema() -> Option<Schema> {{
Expand Down
26 changes: 26 additions & 0 deletions rust/foxglove/src/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,18 @@ impl std::fmt::Display for ChannelId {
}
}

/// A Schema is a description of the data format of messages in a channel.
/// It allows Foxglove to validate messages and provide richer visualizations.
eloff marked this conversation as resolved.
Show resolved Hide resolved
/// You can use the well known types provided in the [crate::schemas] module or provide your own.
eloff marked this conversation as resolved.
Show resolved Hide resolved
/// See the [MCAP spec](https://mcap.dev/spec#schema-op0x03) for more information.
#[derive(Clone, PartialEq, Debug)]
pub struct Schema {
/// An identifier for the schema.
pub name: String,
/// The encoding of the schema data. For example "jsonschema" or "protobuf".
/// The [well-known schema encodings](https://mcap.dev/spec/registry#well-known-schema-encodings) are preferred
eloff marked this conversation as resolved.
Show resolved Hide resolved
pub encoding: String,
/// Must conform to the schema encoding. If encoding is an empty string, data should be 0 length.
pub data: Cow<'static, [u8]>,
}

Expand All @@ -48,6 +56,24 @@ impl Schema {
}
}

/// A log channel that can be used to log binary messages.
///
/// A "channel" is conceptually the same as a [MCAP channel]: it is a stream of messages which all
/// have the same type, or schema. Each channel is instantiated with a unique "topic", or name,
/// which is typically prefixed by a `/`.
///
/// [MCAP channel]: https://mcap.dev/guides/concepts#channel
///
/// If a schema was provided, all messages must be encoded according to the schema.
/// This is not checked. See [`TypedChannel`](crate::TypedChannel) for type-safe channels.
/// Channels are immutable, returned as `Arc<Channel>` and can be shared between threads.
///
/// Channels are created using [`ChannelBuilder`](crate::ChannelBuilder).
///
/// # Example
/// ```
/// use foxglove::{ChannelBuilder, Schema};
/// ```
pub struct Channel {
// TODO add public read-only accessors for these for the Rust API.
// TODO add a list of contexts here as well (or restrict to one context per channel?)
Expand Down
26 changes: 21 additions & 5 deletions rust/foxglove/src/channel_builder.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use crate::channel::ChannelId;
use crate::encode::TypedChannel;
use crate::log_sink_set::LogSinkSet;
use crate::typed_channel::TypedChannel;
use crate::{Channel, FoxgloveError, LogContext, Schema, TypedMessage};
use crate::{Channel, Encode, FoxgloveError, LogContext, Schema};
use std::collections::BTreeMap;
use std::sync::atomic::Ordering::Relaxed;
use std::sync::atomic::{AtomicU32, AtomicU64};
use std::sync::Arc;

/// ChannelBuilder is a builder for creating a new [`Channel`] or [`TypedChannel`].
pub struct ChannelBuilder<'a> {
topic: String,
message_encoding: Option<String>,
Expand All @@ -26,21 +27,31 @@ impl<'a> ChannelBuilder<'a> {
}
}

/// Set the schema for the channel. It's good practice to set a schema for the channel
/// and the ensure all messages logged on the channel conform to the schema.
/// This helps you get the most out of Foxglove. But it's not required.
pub fn schema(mut self, schema: impl Into<Option<Schema>>) -> Self {
self.schema = schema.into();
self
}

/// Set the message encoding for the channel.
/// This is required for Channel, but not for [`TypedChannel`].
/// (it's provided by the [`Encode`] trait for [`TypedChannel`].)
/// The [well-known message encodings](https://mcap.dev/spec/registry#well-known-message-encodings) are preferred.
pub fn message_encoding(mut self, encoding: &str) -> Self {
self.message_encoding = Some(encoding.to_string());
self
}

/// Set the metadata for the channel.
/// Metadata is an optional set of user-defined key-value pairs.
pub fn metadata(mut self, metadata: BTreeMap<String, String>) -> Self {
self.metadata = metadata;
self
}

/// Add a key-value pair to the metadata for the channel.
pub fn add_metadata(mut self, key: &str, value: &str) -> Self {
self.metadata.insert(key.to_string(), value.to_string());
self
Expand All @@ -52,6 +63,8 @@ impl<'a> ChannelBuilder<'a> {
self
}

/// Build the channel and return it in an [`Arc`] as a Result.
/// Returns FoxgloveError::DuplicateChannel if a channel with the same topic already exists.
pub fn build(self) -> Result<Arc<Channel>, FoxgloveError> {
static CHANNEL_ID: AtomicU64 = AtomicU64::new(1);
let channel = Arc::new(Channel {
Expand All @@ -71,12 +84,15 @@ impl<'a> ChannelBuilder<'a> {
Ok(channel)
}

pub fn build_typed<T: TypedMessage>(mut self) -> Result<TypedChannel<T>, FoxgloveError> {
/// Build the channel and return it as a [`TypedChannel`] as a Result.
/// `T` must implement [`Encode`].
/// Returns FoxgloveError::DuplicateChannel if a channel with the same topic already exists.
eloff marked this conversation as resolved.
Show resolved Hide resolved
pub fn build_typed<T: Encode>(mut self) -> Result<TypedChannel<T>, FoxgloveError> {
if self.message_encoding.is_none() {
self.message_encoding = Some(<T as TypedMessage>::get_message_encoding());
self.message_encoding = Some(<T as Encode>::get_message_encoding());
}
if self.schema.is_none() {
self.schema = <T as TypedMessage>::get_schema();
self.schema = <T as Encode>::get_schema();
}
let channel = self.build()?;
Ok(TypedChannel::from_channel(channel))
Expand Down
4 changes: 2 additions & 2 deletions rust/foxglove/src/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
/// Set-like: Takes a series of values separated by commas, again optionally terminated by a comma.
/// Just like the map-like pattern, it uses Iterator::collect to transform an array of values into a collection,
/// which could be a HashSet or another set-like collection depending on the type expected in the context.
///
/// Source: https://stackoverflow.com/a/27582993/152580
// Source: https://stackoverflow.com/a/27582993/152580
#[doc(hidden)]
#[macro_export]
macro_rules! collection {
($($k:expr => $v:expr),* $(,)?) => {{
Expand Down
60 changes: 38 additions & 22 deletions rust/foxglove/src/typed_channel.rs → rust/foxglove/src/encode.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{Channel, PartialMetadata, Schema};
use crate::{Channel, ChannelBuilder, FoxgloveError, PartialMetadata, Schema};
use bytes::BufMut;
use schemars::{schema_for, JsonSchema};
use serde::Serialize;
Expand All @@ -9,9 +9,9 @@ const STACK_BUFFER_SIZE: usize = 128 * 1024;

/// A trait representing a message that can be logged to a [`Channel`].
///
/// Implementing this trait for your type `T` enables the use of [`TypedChannel<T>`], which
/// offers a type-checked `log` method.
pub trait TypedMessage {
/// Implementing this trait for your type `T` enables the use of [`TypedChannel<T>`],
/// which offers a type-checked `log` method.
pub trait Encode {
type Error: std::error::Error;

/// Returns the schema for your data.
Expand All @@ -28,15 +28,17 @@ pub trait TypedMessage {
/// Encodes message data to the provided buffer.
fn encode(&self, buf: &mut impl BufMut) -> Result<(), Self::Error>;

/// Returns an estimated encoded length for the message data.
/// Optional. Returns an estimated encoded length for the message data.
///
/// Used as a hint when allocating the buffer for [`TypedMessage::encode`].
/// Used as a hint when allocating the buffer for [`Encode::encode`].
fn encoded_len(&self) -> Option<usize> {
None
}
}

impl<T: Serialize + JsonSchema> TypedMessage for T {
/// Automatically implements [`Encode`] for any type that implements [`Serialize`] and [`JsonSchema`](https://docs.rs/schemars/latest/schemars/trait.JsonSchema.html).
/// See the JsonSchema Trait and schema_for! macro from the [schemars crate](https://docs.rs/schemars/latest/schemars/) for more information.
impl<T: Serialize + JsonSchema> Encode for T {
type Error = serde_json::Error;

fn get_schema() -> Option<Schema> {
Expand All @@ -57,13 +59,21 @@ impl<T: Serialize + JsonSchema> TypedMessage for T {
}
}

/// A typed [`Channel`] for messages that implement [`TypedMessage`].
pub struct TypedChannel<T: TypedMessage> {
/// A typed [`Channel`] for messages that implement [`Encode`].
/// Channels are immutable, returned as `Arc<Channel>` and can be shared between threads.
pub struct TypedChannel<T: Encode> {
inner: Arc<Channel>,
_phantom: std::marker::PhantomData<T>,
}

impl<T: TypedMessage> TypedChannel<T> {
impl<T: Encode> TypedChannel<T> {
/// Constructs a new typed channel with default settings.
///
/// If you want to override the channel configuration, use [`ChannelBuilder::build_typed`].
pub fn new(topic: impl Into<String>) -> Result<Self, FoxgloveError> {
ChannelBuilder::new(topic).build_typed()
}

pub(crate) fn from_channel(channel: Arc<Channel>) -> Self {
Self {
inner: channel,
Expand All @@ -89,12 +99,13 @@ impl<T: TypedMessage> TypedChannel<T> {
self.inner.log_with_meta(&stack_buf[..written], metadata);
}
Err(_) => {
// The stack buffer was likely too small, fall back to heap allocation.
// Unfortunately the interface of TypedMessage does not expose the size we need,
// even though we do get that information from prost.
// (but TypedMessage can be implemented without prost, so we keep it generic).
let mut buf =
Vec::with_capacity(msg.encoded_len().unwrap_or(STACK_BUFFER_SIZE * 2));
// Likely the stack buffer was too small, so fall back to a heap buffer.
let mut size = msg.encoded_len().unwrap_or(STACK_BUFFER_SIZE * 2);
if size <= STACK_BUFFER_SIZE {
// The estimate in `encoded_len` was too small, fall back to stack buffer size * 2
size = STACK_BUFFER_SIZE * 2;
}
let mut buf = Vec::with_capacity(size);
if let Err(err) = msg.encode(&mut buf) {
tracing::error!("failed to encode message: {:?}", err);
}
Expand All @@ -106,12 +117,13 @@ impl<T: TypedMessage> TypedChannel<T> {

/// Registers a static [`TypedChannel`] for the provided topic and message type.
///
/// This macro is essentially just a wrapper around [`LazyLock`](std::sync::LazyLock), which
/// initializes the channel lazily upon first use. If the initialization fails (e.g., due to
/// This macro is a wrapper around [`LazyLock<TypedChannel<T>>`](std::sync::LazyLock),
/// which initializes the channel lazily upon first use. If the initialization fails (e.g., due to
/// [`FoxgloveError::DuplicateChannel`]), the program will panic.
///
/// If you don't require a static variable, you can just use
/// [`ChannelBuilder::build_typed()`](crate::ChannelBuilder::build_typed) directly.
/// If you don't require a static variable, you can just use [`TypedChannel::new()`] directly.
///
/// The channel is created with the provided visibility and identifier, and the topic and message type.
///
/// # Example
/// ```
Expand All @@ -123,12 +135,16 @@ impl<T: TypedMessage> TypedChannel<T> {
///
/// // A pub(crate)-scoped typed channel.
/// static_typed_channel!(pub(crate) BOXES, "/boxes", SceneUpdate);
///
/// // Usage (you would populate the structs, rather than using `default()`).
/// TF.log(&FrameTransform::default());
/// BOXES.log(&SceneUpdate::default());
/// ```
#[macro_export]
macro_rules! static_typed_channel {
($vis:vis $ident: ident, $topic: literal, $ty: ty) => {
$vis static $ident: std::sync::LazyLock<$crate::TypedChannel<$ty>> =
std::sync::LazyLock::new(|| match $crate::ChannelBuilder::new($topic).build_typed::<$ty>() {
std::sync::LazyLock::new(|| match $crate::TypedChannel::new($topic) {
Ok(channel) => channel,
Err(e) => {
panic!("Failed to create channel for {}: {:?}", $topic, e);
Expand All @@ -153,7 +169,7 @@ mod test {
count: u32,
}

impl TypedMessage for TestMessage {
impl Encode for TestMessage {
type Error = serde_json::Error;

fn get_schema() -> Option<Schema> {
Expand Down
Loading
Loading