diff --git a/docs/migration_1.0/c++/index.html b/docs/migration_1.0/c++/index.html index fce34928..17b0f42a 100644 --- a/docs/migration_1.0/c++/index.html +++ b/docs/migration_1.0/c++/index.html @@ -1,6 +1,6 @@ C++ · Zenoh - pub/sub, geo distributed storage, query

C++

Zenoh 1.0.0 brings a number of changes to the API, with a concentrated effort to bring the C++ API to more closely resemble the Rust API in usage.

The improvements we bring in this update include:

  • A simpler organization of the Zenoh classes, abstracting away the notion of View and Closure.
  • Improved and more flexible Error Handling through error codes and exceptions.
  • Support for serialization of common types like strings, numbers, vectors through Codecs.
  • Ability for users to define their own Codecs (for their own types or to overwrite the default one)!
  • Improved stream handlers and callback support.
  • Simpler attachment API.

Now that the amuse bouche is served, let’s get into the main course!

Error Handling

In version 0.11.0 all Zenoh call failures were handled by either returning a bool value indicating success or failure (and probably returning an error code) or returning an std::variant<ReturnType, ErrorMessage>. For instance:

std::variant<z::Config, ErrorMessage> config_client(const z::StrArrayView& peers);
+

C++

Zenoh 1.0.0 brings a number of changes to the API, with a concentrated effort to bring the C++ API to more closely resemble the Rust API in usage.

The improvements we bring in this update include:

  • A simpler organization of the Zenoh classes, abstracting away the notion of View and Closure.
  • Improved and more flexible Error Handling through error codes and exceptions.
  • Support for serialization of common types like strings, numbers, vectors through Codecs.
  • Ability for users to define their own Codecs (for their own types or to overwrite the default one)!
  • Improved stream handlers and callback support.
  • Simpler attachment API.

Now that the amuse bouche is served, let’s get into the main course!

Error Handling

In version 0.11.0 all Zenoh call failures were handled by either returning a bool value indicating success or failure (and probably returning an error code) or returning an std::variant<ReturnType, ErrorMessage>. For instance:

std::variant<z::Config, ErrorMessage> config_client(const z::StrArrayView& peers);
 
 bool put(const z::BytesView& payload, const z::PublisherPutOptions& options, ErrNo& error);
 

In 1.0.0, all functions that can fail on the Zenoh side now offer 2 options for error handling:

A. Exceptions

B. Error Codes

Any function that can fail now accepts an optional parameter ZError* err pointer to the error code. If it is not provided (or set to nullptr), an instance of ZException will be thrown, otherwise the error code will be written into the err pointer.

static Config client(const std::vector<std::string>& peers, ZError* err = nullptr);
@@ -19,50 +19,73 @@
 } catch (const ZException& e) {
   // handle failure
 }
-

All returned and std::move’d-in objects are guaranteed to be left in an “empty” state in case of function call failure.

Serialization and Deserialization

In version 0.11.0 it was only possible to send std::string/ const char* or std::vector<uint8_t> / uint8_t* using the BytesView class:

publisher.put("my_payload");
-

In 1.0.0, the BytesView class is gone and we introduced the Bytes object which represents a serialized payload.

void publish_data(const Publisher& publisher, const MyData& data) {
-	publisher.put(Bytes<MyCodec>::serialize(data));
+

All returned and std::move’d-in objects are guaranteed to be left in an “empty” state in case of function call failure.

Payload

In version 0.11.0 it was only possible to send std::string/ const char* or std::vector<uint8_t> / uint8_t* using the BytesView class:

publisher.put("my_payload");
+

In 1.0.0, the BytesView class is gone and we introduced the Bytes object which represents a (serialized) payload. +Similarly to 0.11.0 it can be used to store raw bytes or strings:

void publish_string(const Publisher& publisher, const std::string& data) {
+	publisher.put(Bytes(data));
 }
 
-void receive_data(const Sample &sample) {
+void publish_string_without_copy(const Publisher& publisher, std::string&& data) {
+	publisher.put(Bytes(data));
+}
+
+void receive_string(const Sample &sample) {
   std::cout <<"Received: " 
-					  << sample.get_payload().deserialize<MyData, MyCodec>() 
+					  << sample.get_payload().as_string() 
             << "\n";
 };
-

We added a default ZenohCodec, which provides default serialization / deserialization for common numerical types, strings, and containers:

  // stream of bytes serialization
-  std::vector<uint8_t> data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
-  Bytes b = Bytes::serialize(data);
-  assert(b.deserialize<std::vector<uint8_t>>() == data);
-  
-  // arithmetic type serialization
+
+void publish_bytes(const Publisher& publisher, const std::vector<uint8_t>& data) {
+	publisher.put(Bytes(data));
+}
+
+void publish_bytes_without_copy(const Publisher& publisher, std::vector<uint8_t>&& data) {
+	publisher.put(Bytes(data));
+}
+
+void receive_bytes(const Sample &sample) {
+  std::vector<uint8_t> = sample.get_payload().as_vector();
+};
+

Additionally zenoh::ext namespace provides support for serialization/deserialziation of typed data to/into Bytes:

  // arithmetic types
   double pi = 3.1415926;
-  Bytes b = Bytes::serialize(pi);
-  assert(b.deserialize<TYPE>() == pi);
+  Bytes b = ext::serialize(pi);
+  assert(ext::deserialize<doulbe>(b) == pi);
   
-  // Composite types serialization
-  std::vector<float> v = {0.1f, .2f, 0.3f};
-  auto b = Bytes::serialize(v);
-  assert(b.deserialize<decltype(v)>() == v);
+  // Composite types
+  std::vector<float> v = {0.1f, 0.2f, 0.3f};
+  b = ext::serialize(v);
+  assert(ext::deserialize<std::vector<float>>(b) == v);
   
-  std::map<std::string, std::deque<double>> m = {
+  std::unordered_map<std::string, std::deque<double>> m = {
     {"a", {0.5, 0.2}},
     {"b", {-123.45, 0.4}},
     {"abc", {3.1415926, -1.0} }
   };
 
-  b = Bytes::serialize(m);
-  assert(b.deserialize<decltype(m2)>() == m); 
-
-  // alternatively serialize via move 
-  // the string keys will not be copied in this case, but rather move into Bytes
-  b_move = Bytes::serialize(std::move(m));
-

Please note that this serialization functionality is only provided for prototyping and demonstration purposes and, as such, might be less efficient than custom-written serialization methods.

Users can easily define their own serialization/deserialization functions by providing a custom codec:

MyType my_data(...);
-MyCodec my_codec(...);
-Bytes b = Bytes::serialize(my_data, my_codec);
-// or Bytes::serialize<MyCodec>(my_data) if MyCodec is a stateless codec with an empty constructor 
-assert(b.deserialize<std::vector<MyType>>(my_codec) == my_data);
-// or assert(b.deserialize<std::vector<MyType, MyCodec>>() == my_data); if MyCodec is a stateless with an empty constructor
-

For finer control on serialization / deserialization implementation Bytes::Iterator, Bytes::Writer and Bytes::Reader classes are also introduced.

A working example with custom-defined serialization / deserialization can be found here:

https://github.com/eclipse-zenoh/zenoh-cpp/blob/dev/1.0.0/examples/simple/universal/z_simple.cxx

Stream Handlers and Callbacks

In version 0.11.0 stream handlers were only supported for get :

// callback
+  b = ext::serialize(m);
+  assert(ext::deserialize<std::unordered_map<std::string, std::deque<double>>>(b) == m); 
+

Users can easily define serialization/deserialization for their own custom types by using ext::Serializer and ext::Deserializer classes:

struct CustomStruct {
+  std::vector<double> vd;
+  int32_t i;
+  std::string s;
+};
+
+// One needs to implement __zenoh_serialize_with_serializer in the same namespace as CustomStruct
+void __zenoh_serialize_with_serializer(ext::Serializer& serializer, const CustomStruct& s) {
+  serializer.serialize(s.vd);
+  serializer.serialize(s.i);
+  serializer.serialize(s.s);
+}
+
+void serialize_custom() {
+  CustomStruct s = {{0.1, 0.2, -1000.55}, 32, "test"};
+  Bytes b = ext::serialize(s);
+  CustomStruct s_out = ext::deserialize<CustomStruct>(b);
+  assert(s.vd == s_out.vd);
+  assert(s.i == s_out.i);
+  assert(s.s == s_out.s);
+}
+

For lower-level access to the Bytes content Bytes::Reader, Bytes::Writer and Bytes::SliceIterator classes can be used.

Stream Handlers and Callbacks

In version 0.11.0 stream handlers were only supported for get :

// callback
 session.get(keyexpr, "", {on_reply, on_done}, opts);
 
 // stream handlers interface
@@ -72,9 +95,9 @@
 Reply reply(nullptr);
 // blocking
 for (recv(reply); reply.check(); recv(reply)) {
-    auto sample = expect<Sample>(reply.get());
-    std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '"
-              << sample.get_payload().as_string_view() << "')\n";
+  auto sample = expect<Sample>(reply.get());
+  std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '"
+            << sample.get_payload().as_string() << "')\n";
 }
 
 // non-blocking
@@ -86,7 +109,7 @@
   }
   auto sample = expect<Sample>(reply.get());
   std::cout << "\nReceived ('" << sample.get_keyexpr().as_string_view() << "' : '"
-            << sample.get_payload().as_string_view() << "')";
+            << sample.get_payload().as_string() << "')";
 }
 

In 1.0.0, Subscriber, Queryable and get can now use either a callable object or a stream handler. Currently, Zenoh provides 2 types of handlers:

  • FifoHandler - serving messages in Fifo order, blocking once full.
  • RingHandler - serving messages in Fifo order, dropping older messages once full to make room for new ones.
// callback
 session.get(
@@ -103,7 +126,7 @@
 for (auto res = replies.recv(); std::has_alternative<Reply>(res); res = replies.recv()) {
   const auto& sample = std::get<Reply>(res).get_ok();
   std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '"
-            << sample.get_payload().deserialize<std::string>() << "')\n";
+            << sample.get_payload().as_string() << "')\n";
 }
 // non-blocking
 while (true) {
@@ -111,7 +134,7 @@
   if (std::has_alternative<Reply>(res)) {
     const auto& sample = std::get<Reply>(res).get_ok();
 	  std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '"
-            << sample.get_payload().deserialize<std::string>() << "')\n";
+            << sample.get_payload().as_string() << "')\n";
   } else if (std::get<channels::RecvError>(res) == channels::RecvError::Z_NODATA) {
 	  // try_recv is non-blocking call, so may fail to return a reply if the Fifo buffer is empty
 	  std::cout << ".";
@@ -127,7 +150,7 @@
 auto data_callback = [](const Sample &sample) {
   std::cout << ">> [Subscriber] Received ('"
             << sample.get_keyexpr().as_string_view() 
-            << "' : '" << sample.get_payload().deserialize<std::string>() 
+            << "' : '" << sample.get_payload().as_string() 
             << "')\n";
 };
 
@@ -145,11 +168,11 @@
 const auto& messages = subscriber.handler();
 //blocking
 for (auto res = messages.recv(); std::has_alternative<Sample>(res); res = messages.recv()) {
-		// recv will block until there is at least one sample in the Fifo buffer
-		// it will return an empty sample and alive=false once subscriber gets disconnected
-		const Sample& sample = std::get<Sample>(res);
-    std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '"
-              << sample.get_payload().deserialize<std::string>() << "')\n";
+  // recv will block until there is at least one sample in the Fifo buffer
+  // it will return an empty sample and alive=false once subscriber gets disconnected
+  const Sample& sample = std::get<Sample>(res);
+  std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '"
+            << sample.get_payload().as_string() << "')\n";
 }
 // non-blocking
 while (true) {
@@ -157,7 +180,7 @@
   if (std::has_alternative<Sample>(res)) {
     const auto& sample = std::get<Sample>(res);
 	  std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '"
-            << sample.get_payload().deserialize<std::string>() << "')\n";
+            << sample.get_payload().as_string() << "')\n";
   } else if (std::get<channels::RecvError>(res) == channels::RecvError::Z_NODATA) {
 	  // try_recv is non-blocking call, so may fail to return a sample if the Fifo buffer is empty
 	  std::cout << ".";
@@ -177,24 +200,24 @@
 pub.put(s, options);
 
 // subscriber callback function receiving message with attachment
-data_handler(const Sample &sample) {
-  std::cout << ">> [Subscriber] Received " ('"
+void data_handler(const Sample &sample) {
+  std::cout << ">> [Subscriber] Received \" ('"
             << sample.get_keyexpr().as_string_view() 
             << "' : '" 
             << sample.get_payload().as_string_view()
 		        << "')\n";
   if (sample.get_attachment().check()) {
-      // reads full attachment
-      sample.get_attachment().iterate([](const BytesView &key, const BytesView &value) -> bool {
-          std::cout << "   attachment: " << key.as_string_view() << ": '" << value.as_string_view() << "'\n";
-          return true;
-      });
-
-      // or read particular attachment item
-      auto index = sample.get_attachment().get("index");
-      if (index != "") {
-          std::cout << "   message number: " << index.as_string_view() << std::endl;
-      }
+    // reads full attachment
+    sample.get_attachment().iterate([](const BytesView &key, const BytesView &value) -> bool {
+      std::cout << "   attachment: " << key.as_string_view() << ": '" << value.as_string_view() << "'\n";
+      return true;
+    });
+
+    // or read particular attachment item
+    auto index = sample.get_attachment().get("index");
+    if (index != "") {
+      std::cout << "   message number: " << index.as_string_view() << std::endl;
+    }
   }
 };
 

In 1.0.0, attachment handling was greatly simplified. It is now represented as Bytes (i.e. the same class we use to represent serialized data) and can thus contain data in any format.

// publish a message with attachment
@@ -207,8 +230,8 @@
   {"index", "0"}
 };    
 pub.put(
-  Bytes::serialize("my_payload"), 
-  {.encoding = Encoding("text/plain"), .attachment = std::move(attachment_map)}
+  Bytes("my_payload"), 
+  {.encoding = Encoding("text/plain"), .attachment = ext::serialize(attachment_map)}
 );
 
 
@@ -217,13 +240,13 @@
   std::cout << ">> [Subscriber] Received ('"
             << sample.get_keyexpr().as_string_view() 
             << "' : '" 
-            << sample.get_payload().deserialize<std::string>()
+            << sample.get_payload().as_string()
             << "')\n";
   auto attachment = sample.get_attachment();
   if (!attachment.has_value()) return;
   // we expect attachment in the form of key-value pairs
-  auto attachment_deserialized = attachment->get().deserialize<std::unordered_map<std::string, std::string>>();
-  for (auto&& [key, value]: attachment) {
+  auto attachment_deserialized = ext::deserialize<std::unordered_map<std::string, std::string>>(attachment->get());
+  for (auto&& [key, value]: attachment_deserialized) {
     std::cout << "   attachment: " << key << ": '" << value << "'\n";
   }
 };
@@ -234,7 +257,7 @@
 ...
 
 session.get(keyexpr, "", {on_reply, on_done}, opts);
-

In 1.0.0:

session.get(keyexpr, "", on_reply, on_done, {.target = Z_QUERY_TARGET_ALL, .payload = Bytes::serialize(value)});
+

In 1.0.0:

session.get(keyexpr, "", on_reply, on_done, {.target = Z_QUERY_TARGET_ALL, .payload = ext::serialize(value)});
 
Next up: C / Pico
diff --git a/docs/migration_1.0/c_pico/index.html b/docs/migration_1.0/c_pico/index.html index 10e7a436..0c484032 100644 --- a/docs/migration_1.0/c_pico/index.html +++ b/docs/migration_1.0/c_pico/index.html @@ -1,6 +1,6 @@ C / Pico · Zenoh - pub/sub, geo distributed storage, query

C / Pico

General API changes

We have reworked the type naming to clarify how types should be interacted with.

Owned types

Owned types are allocated by the user and it is their responsibility to drop them using z_drop (or z_close for sessions).

Previously, we were returning Zenoh structures by value. In Zenoh 1.0.0, a reference to memory must be provided. This allows initializing user allocated structures and frees return value for error codes.

Here is a quick example of this change:

  • Zenoh 0.11.x
z_owned_session_t session = z_open(z_move(config));
+

C / Pico

General API changes

We have reworked the type naming to clarify how types should be interacted with.

Owned types

Owned types are allocated by the user and it is their responsibility to drop them using z_drop (or z_close for sessions).

Previously, we were returning Zenoh structures by value. In Zenoh 1.0.0, a reference to memory must be provided. This allows initializing user allocated structures and frees return value for error codes.

Here is a quick example of this change:

  • Zenoh 0.11.x
z_owned_session_t session = z_open(z_move(config));
 if (!z_check(session)) {
     return -1;
 }
@@ -10,7 +10,7 @@
   return -1;
 }
 z_close(z_move(session))
-

The owned objects have a “null” state.

  • If the constructor function (e.g. z_open) fails, the owned object will be set to its null state.
  • The z_drop releases the resources of the owned object and sets it to the null state to avoid double drop. Calling z_drop on an object in a null state is safe and does nothing.
  • Calling z_drop on an uninitialized object is invalid.

Owned types support move semantics, which will consume the owned object and turn it into a moved object, see next section.

Moved types

Moved types are obtained when using z_move on an owned type object. They are consumed on use when passed to relevant functions. Any non-constructor function accepting a moved object (i.e. an object passed by owned pointer) becomes responsible for calling drop on it. The object is guaranteed to be in the null state upon such function return, even if it fails.

Loaned types

Each owned type now has a corresponding z_loaned_xxx_t type, which is obtained by calling

z_loan or z_loan_mut on it, or eventually received from Zenoh functions / callbacks.

It is no longer possible to directly access the fields of an owned object that has been loaned, the accessor functions on the loaned objects should instead be used.

Here is a quick example:

  • Zenoh 0.11.x
void reply_handler(z_owned_reply_t *reply, void *ctx) {
+

The owned objects have a “null” state.

  • If the constructor function (e.g. z_open) fails, the owned object will be set to its null state.
  • The z_drop releases the resources of the owned object and sets it to the null state to avoid double drop. Calling z_drop on an object in a null state is safe and does nothing.
  • Calling z_drop on an uninitialized object is invalid.

Owned types support move semantics, which will consume the owned object and turn it into a moved object, see next section.

Moved types

Moved types are obtained when using z_move on an owned type object. They are consumed on use when passed to relevant functions. Any non-constructor function accepting a moved object (i.e. an object passed by moved pointer) becomes responsible for calling drop on it. The object is guaranteed to be in the null state upon such function return, even if it fails.

Loaned types

Each owned type now has a corresponding z_loaned_xxx_t type, which is obtained by calling

z_loan or z_loan_mut on it, or eventually received from Zenoh functions / callbacks.

It is no longer possible to directly access the fields of an owned object that has been loaned, the accessor functions on the loaned objects should instead be used.

Here is a quick example:

  • Zenoh 0.11.x
void reply_handler(z_owned_reply_t *reply, void *ctx) {
     if (z_reply_is_ok(reply)) {
         z_sample_t sample = z_reply_ok(reply);
         printf(">> Received ('%.*s')\n", (int)sample.payload.len, sample.payload.start);
@@ -44,80 +44,136 @@
 if (z_declare_subscriber(&sub, z_loan(session), z_loan(ke), z_move(callback), NULL) < 0) {
     return -1;
 }
-

Payload and Serialization

Zenoh 1.0.0 handles payload differently. Before one would pass the pointer to the buffer and its length, now everything must be serialized into z_owned_bytes_t.

To simplify serialization/deserialization we provide support for some primitive types like uint8_t* + length, (null-)terminated strings and arithmetic types.

  • Zenoh 0.11.x
char *value = "Some data to publish on Zenoh";
+

Payload and Serialization

Zenoh 1.0.0 handles payload differently. Before one would pass the pointer to the buffer and its length, now everything must be converted/serialized into z_owned_bytes_t.

Raw data in the form of null-terminated strings or buffers (i.e. uint8_t* + length) can be converted directly into z_owned_bytes_t +as follows:

  • Zenoh 0.11.x
int8_t send_string() {
+  char *value = "Some data to publish on Zenoh";
 
-if (z_put(z_loan(session), z_keyexpr(ke), (const uint8_t *)value, strlen(value), NULL) < 0) {
-    return -1;
+  if (z_put(z_loan(session), z_keyexpr(ke), (const uint8_t *)value, strlen(value), NULL) < 0) {
+      return -1;
+  }
+  return 0;
 }
-
  • Zenoh 1.0.0
char *value = "Some data to publish on Zenoh";
-z_owned_bytes_t payload;
-z_bytes_serialize_from_str(&payload, value);
 
-if (z_put(z_loan(session), z_loan(ke), z_move(payload), NULL) < 0) {
-  return -1;
+int8_t send_buf() {
+  uint8_t *buf = (uint8_t*)z_malloc(16);
+  // fill the buf
+  if (z_put(z_loan(session), z_keyexpr(ke), buf, 16, NULL) < 0) {
+      return -1;
+  }
+  return 0;
 }
-

To implement custom (de-)serialization functions, Zenoh 1.0.0 provides z_bytes_iterator_t, z_bytes_reader_t and z_owned_bytes_wrtiter_t types and corresponding functions. -Alternatively, it is always possible to perform serialization separately and only send/receive uint8_t arrays, by only calling trivial z_bytes_serialize_from_slice and z_bytes_deserialize_into_slice functions:

void send_data(const z_loaned_publisher_t* pub, const uint8_t *data, size_t len) {
+
  • Zenoh 1.0.0
int8_t send_string() {
+  char *value = "Some data to publish on Zenoh";
   z_owned_bytes_t payload;
-  z_bytes_serialize_from_buf(&payload, data, len);
-  z_publisher_put(pub, z_move(payload), NULL);
-  // no need to drop the payload, since it is consumed by z_publisher_put
-}
+  z_bytes_copy_from_str(&payload, value); // this copeies value to the payload
+
+  if (z_put(z_loan(session), z_loan(ke), z_move(payload), NULL) < 0) {
+    return -1;
+  }
+  // z_bytes_to_string can be used to convert received z_loaned_bytes_t into z_owned_string_t
+  return 0;
+}
 
-void receive_data(const z_loaned_bytes_t* payload) {
-  z_owned_slice_t slice;
-  z_bytes_deserialize_into_slice(payload, &slice);
+void receive_string(const z_loaned_bytes_t* payload) {
+  z_owned_string_t s;
+  z_bytes_to_string(payload, &s);
 
-  // do something with serialized data
-  // raw ptr can be accessed via z_slice_data(z_loan(slice))
-  // data length can be accessed via z_slice_len(z_loan(slice))
+  // do something with the string
+  // raw ptr can be accessed via z_string_data(z_loan(s))
+  // data length can be accessed via z_string_len(z_loan(s))
 
-  // in the end slice should be dropped since it contains a copy of the payload data
-  z_drop(z_move(slice));
+  // in the end string should be dropped since it contains a copy of the payload data
+  z_drop(z_move(s));
 }
-

Note that it is no longer possible to access the underlying payload data pointer directly, since Zenoh cannot guarantee that the data is delivered as a single fragment. -So in order to get access to raw payload data one must use z_bytes_reader_t and related functions:

z_bytes_reader_t reader = z_bytes_get_reader(z_loan(payload));
-uint8_t data1[10] = {0};
-uint8_t data2[20] = {0};
 
-z_bytes_reader_read(&reader, data1, 10); // copy first 10 payload bytes to data1
-z_bytes_reader_read(&reader, data2, 20); // copy next 20 payload bytes to data2
-

Note that all z_bytes_serialize_from… functions involve copying the data. -On the other hand, it is also possible to allow Zenoh to consume your data directly (which avoids the need to make an extra copy) using z_bytes_from_buf or z_bytes_from_str. -The user would need to provide a delete function to be called on data, when Zenoh finishes its processing:

void my_custom_delete_function(void *data, void* context) {
+
+int8_t void send_buf() {
+  uint8_t *buf = (uint8_t*)z_malloc(16);
+  // fill the buf
+  z_bytes_from_buf(&payload, buf, 16, my_custom_delete_function, NULL); // this moves buf into the payload
+  // my_custom_delete_function will be called to free the buf, once the corresponding data is send
+  // alternatively z_bytes_copy_from_buf(&payload, buf, 16) can be used, if coying the buf is required.
+  if (z_put(z_loan(session), z_loan(ke), z_move(payload), NULL) < 0) {
+    return -1;
+  }
+  // z_bytes_to_slice can be used to convert received z_loaned_bytes_t into z_owned_slice_t
+  return 0;
+}
+
+/// possible my_custom_delete_function implementation
+void my_custom_delete_function(void *data, void* context) {
   // perform delete of data by optionally using extra information in the context
   free(data);
 }
 
-void send_move_data(const z_loaned_publisher_t *publisher) {
-  uint8_t *my_data = malloc(10);
-  // fill my_data as necessary
-  z_owned_bytes_t b;
-  z_bytes_from_buf(&b, my_data, 10, my_custom_delete_function, NULL);
-  z_publisher_put(publisher, z_move(b));
-}
+void receive_buf(const z_loaned_bytes_t* payload) {
+  z_owned_slice_t s;
+  z_bytes_to_slice(payload, &s);
 
-// an example of sending a data with more complex destructor
-// a case of std::vector<uint8_t> from c++ stl library
+  // do something with the string
+  // raw ptr can be accessed via z_slice_data(z_loan(s))
+  // data length can be accessed via z_slice_len(z_loan(s))
 
-void delete_vector(void *data, void* context) {
-  std::vector<uint8_t> *v = (std::vector<uint8_t> *)context;
-  delete v;
-  // in this case data pointer is not used for destruction
-}
+  // in the end string should be dropped since it contains a copy of the payload data
+  z_drop(z_move(s));
+}
+

The structured data can be serialized into z_owned_bytes_t by using provided serialization functionality. Zenoh provides +support for serializing arithmetic types, strings, sequences and tuples.

More comprehensive serialization/deserialization examples are provided in +https://github.com/eclipse-zenoh/zenoh-c/blob/main/examples/z_bytes.c and https://github.com/eclipse-zenoh/zenoh-pico/blob/main/examples/unix/c11/z_bytes.c.

To simplify serialization/deserialization we provide support for some primitive types like uint8_t* + length, null-terminated strings and arithmetic types. +Primitive types can be serialized directly into z_owned_bytes_t:

// Serialization
+uint32_t input_u32 = 1234;
+ze_serialize_uint32(&payload, input_u32);
+
+// Deserialization
+uint32_t output_u32 = 0;
+ze_deserialize_uint32(z_loan(payload), &output_u32);
+// now output_u32 = 1234
+z_drop(z_move(payload));
+

while tuples and/or arrays require usage of ze_owned_serializer_t and ze_deserializer_t:

typedef struct my_struct_t {
+  float f;
+  int32_t n;
+} my_struct_t;
 
-void send_move_vector(std::vector<uint8_t> *v, const z_loaned_publisher_t *publisher) {
-  z_owned_bytes_t b;
-  z_bytes_from_buf(&b, v.data(), v.size(), delete_vector, (void*)v);
-  z_publisher_put(publisher, z_move(b));
-}
-

Yet another alternative is to send the statically allocated constant data (hence that does not require to be deleted) without making an extra copy. This can be achieved using the z_serialize_from_static_buf or z_serialize_from_static_str functions:

const char *my_constant_string = "my string";
+...
 
-void send_static_data(const z_loaned_publisher_t *publisher) {
-  z_owned_bytes_t b;
-  z_bytes_from_static_str(&b, my_constant_string);
-  z_publisher_put(publisher, z_move(b));
-}
+// Serialization
+my_struct_t input_vec[] = {{1.5f, 1}, {2.4f, 2}, {-3.1f, 3}, {4.2f, 4}};
+ze_owned_serializer_t serializer;
+ze_serializer_empty(&serializer);
+ze_serializer_serialize_sequence_length(z_loan_mut(serializer), 4);
+for (size_t i = 0; i < 4; ++i) {
+  ze_serializer_serialize_float(z_loan_mut(serializer), input_vec[i].f);
+  ze_serializer_serialize_int32(z_loan_mut(serializer), input_vec[i].n);
+}
+ze_serializer_finish(z_move(serializer), &payload);
+
+// Deserialization
+my_struct_t output_vec[4] = {0};
+ze_deserializer_t deserializer = ze_deserializer_from_bytes(z_loan(payload));
+size_t num_elements = 0;
+ze_deserializer_deserialize_sequence_length(&deserializer, &num_elements);
+assert(num_elements == 4);
+for (size_t i = 0; i < num_elements; ++i) {
+  ze_deserializer_deserialize_float(&deserializer, &output_vec[i].f);
+  ze_deserializer_deserialize_int32(&deserializer, &output_vec[i].n);
+}
+// now output_vec = {{1.5f, 1}, {2.4f, 2}, {-3.1f, 3}, {4.2f, 4}}
+z_drop(z_move(payload));
+

To implement custom (de-)serialization, Zenoh 1.0.0 provides ze_owned_bytes_serializer, ze_bytes_deserializer_t or lower-level z_owned_bytes_wrtiter_t and z_bytes_reader_t types and corresponding functions.

Note that it is no longer possible to access the underlying payload data pointer directly, since Zenoh cannot guarantee that the data is delivered as a single fragment. +So in order to get access to raw payload data one must use either z_bytes_reader_t or alternatively z_bytes_slice_iterator_t and their related functions:

z_bytes_reader_t reader = z_bytes_get_reader(z_loan(payload));
+uint8_t data1[10] = {0};
+uint8_t data2[20] = {0};
+
+z_bytes_reader_read(&reader, data1, 10); // copy first 10 payload bytes to data1
+z_bytes_reader_read(&reader, data2, 20); // copy next 20 payload bytes to data2
+
+// or
+z_bytes_slice_iterator_t slice_iter = z_bytes_get_slice_iterator(z_bytes_loan(&payload));
+z_view_slice_t curr_slice;
+while (z_bytes_slice_iterator_next(&slice_iter, &curr_slice)) {
+  // curr_slice provides a view on the corresponding fragment bytes.
+  // Note that there is no guarantee regarding each individual slice size
+}
 

Channel Handlers and Callbacks

In version 0.11.0 Channel handlers were only supported for z_getand z_owned_queryable_t:

// callback
 z_owned_closure_reply_t callback = z_closure(reply_handler);
 z_get(z_loan(session), z_keyexpr(keyexpr), "", z_move(callback), &opts);
@@ -128,13 +184,13 @@
 z_get(z_loan(session), z_keyexpr(keyexpr), "", z_move(channel.send), &opts);
 z_owned_reply_t reply = z_reply_null();
 for (z_call(channel.recv, &reply); z_check(reply); z_call(channel.recv, &reply)) {
-    if (z_reply_is_ok(&reply)) {
-      z_sample_t sample = z_reply_ok(&reply);
-      // do something with sample and keystr
-    } else {
-      printf("Received an error\n");
-    }
-    z_drop(z_move(reply));
+  if (z_reply_is_ok(&reply)) {
+    z_sample_t sample = z_reply_ok(&reply);
+    // do something with sample and keystr
+  } else {
+    printf("Received an error\n");
+  }
+  z_drop(z_move(reply));
 }
 z_drop(z_move(channel));
 
@@ -143,17 +199,17 @@
 z_get(z_loan(s), keyexpr, "", z_move(channel.send), &opts);
 z_owned_reply_t reply = z_reply_null();
 for (bool call_success = z_call(channel.recv, &reply); !call_success || z_check(reply);
-    call_success = z_call(channel.recv, &reply)) {
-    if (!call_success) {
-        continue;
-    }
-    if (z_reply_is_ok(z_loan(reply))) {
-      const z_loaned_sample_t *sample = z_reply_ok(&reply);
-      // do something with sample
-    } else {
-      printf("Received an error\n");
-    }
-    z_drop(z_move(reply));
+  call_success = z_call(channel.recv, &reply)) {
+  if (!call_success) {
+    continue;
+  }
+  if (z_reply_is_ok(z_loan(reply))) {
+    const z_loaned_sample_t *sample = z_reply_ok(&reply);
+    // do something with sample
+  } else {
+    printf("Received an error\n");
+  }
+  z_drop(z_move(reply));
 }
 z_drop(z_move(channel));
 

In 1.0.0, z_owned_subscriber_t, z_owned_queryable_t and z_get can use either a callable object or a stream handler. In addition, the same handler type now provides both a blocking and non-blocking interface. For the time being Zenoh provides 2 types of handlers:

  • FifoHandler - serving messages in Fifo order, dropping new messages once full. It is worth noting that it will drop new messages only if the queue is full and the default multi-thread feature is disabled.
  • RingHandler - serving messages in Fifo order, dropping older messages once full to make room for new ones.
// callback
@@ -169,33 +225,33 @@
 
 // blocking
 while (z_recv(z_loan(handler), &reply) == Z_OK) {
-    // z_recv will block until there is at least one sample in the Fifo buffer
-    if (z_reply_is_ok(z_loan(reply))) {
-        const z_loaned_sample_t *sample = z_reply_ok(z_loan(reply));
-        // do something with sample
-    } else {
-        printf("Received an error\n");
-    }
-    z_drop(z_move(reply));
+  // z_recv will block until there is at least one sample in the Fifo buffer
+  if (z_reply_is_ok(z_loan(reply))) {
+    const z_loaned_sample_t *sample = z_reply_ok(z_loan(reply));
+    // do something with sample
+  } else {
+    printf("Received an error\n");
+  }
+  z_drop(z_move(reply));
 }
 
 // non-blocking
 while (true) {
-    z_result_t res = z_try_recv(z_loan(handler), &reply);
-    if (res == Z_CHANNEL_NODATA) {
-      // z_try_recv is non-blocking call, so will fail to return a reply if the Fifo buffer is empty
-      // do some other work or just sleep
-    } else if (res == Z_OK) {
-      if (z_reply_is_ok(z_loan(reply))) {
-          const z_loaned_sample_t *sample = z_reply_ok(z_loan(reply));
-          // do something with sample
-      } else {
-          printf("Received an error\n");
-      }
-      z_drop(z_move(reply));
-     } else { // res == Z_CHANNEL_DISCONNECTED
-       break; // channel is closed - no more replies will arrive
-     }
+  z_result_t res = z_try_recv(z_loan(handler), &reply);
+  if (res == Z_CHANNEL_NODATA) {
+    // z_try_recv is non-blocking call, so will fail to return a reply if the Fifo buffer is empty
+    // do some other work or just sleep
+  } else if (res == Z_OK) {
+    if (z_reply_is_ok(z_loan(reply))) {
+      const z_loaned_sample_t *sample = z_reply_ok(z_loan(reply));
+      // do something with sample
+    } else {
+      printf("Received an error\n");
+    }
+    z_drop(z_move(reply));
+  } else { // res == Z_CHANNEL_DISCONNECTED
+    break; // channel is closed - no more replies will arrive
+  }
 }
 

The same now also works for Subscriber and Queryable :

// callback
 // callback
@@ -233,16 +289,16 @@
 
 // non-blocking
 while (true) {
-    z_result_t res = z_try_recv(z_loan(handler), &sample);
-    if (res == Z_CHANNEL_NODATA) {
-      // z_try_recv is non-blocking call, so will fail to return a sample if the Fifo buffer is empty
-      // do some other work or just sleep
-    } else if (res == Z_OK) {
-      // do something with sample
-      z_drop(z_move(sample));
-    } else { // res == Z_CHANNEL_DISCONNECTED
-      break; // channel is closed - no more samples will be received
-    }
+  z_result_t res = z_try_recv(z_loan(handler), &sample);
+  if (res == Z_CHANNEL_NODATA) {
+    // z_try_recv is non-blocking call, so will fail to return a sample if the Fifo buffer is empty
+    // do some other work or just sleep
+  } else if (res == Z_OK) {
+    // do something with sample
+    z_drop(z_move(sample));
+  } else { // res == Z_CHANNEL_DISCONNECTED
+    break; // channel is closed - no more samples will be received
+  }
 }
 

The z_owned_pull_subscriber_t was removed, given that RingHandler can provide similar functionality with ordinary z_owned_subscriber_t. Since the callback in 1.0.0. carries a loaned sample whenever it is triggered, we can save an explicit drop on the sample now.

Attachment

In 0.11.0, attachments were a separate type and could only be a set of key-value pairs:

// publish message with attachment
@@ -254,7 +310,7 @@
 options.attachment = z_bytes_map_as_attachment(&map);
 
 if (z_put(z_loan(s), z_keyexpr(keyexpr), (const uint8_t *)value, strlen(value), &options) < 0) {
-    return -1;
+  return -1;
 }
 
 z_bytes_map_drop(&map);
@@ -262,63 +318,51 @@
 // receive sample with attachment
 
 int8_t attachment_reader(z_bytes_t key, z_bytes_t val, void* ctx) {
-    printf("   attachment: %.*s: '%.*s'\n", (int)key.len, key.start, (int)val.len, val.start);
-    return 0;
+  printf("   attachment: %.*s: '%.*s'\n", (int)key.len, key.start, (int)val.len, val.start);
+  return 0;
 }
 
 void data_handler(const z_sample_t* sample, void* arg) {
-    // checks if attachment exists
-    if (z_check(sample->attachment)) {
-        // reads full attachment
-        z_attachment_iterate(sample->attachment, attachment_reader, NULL);
-
-        // reads particular attachment item
-        z_bytes_t index = z_attachment_get(sample->attachment, z_bytes_from_str("index"));
-        if (z_check(index)) {
-            printf("   message number: %.*s\n", (int)index.len, index.start);
-        }
+  // checks if attachment exists
+  if (z_check(sample->attachment)) {
+    // reads full attachment
+    z_attachment_iterate(sample->attachment, attachment_reader, NULL);
+
+    // reads particular attachment item
+    z_bytes_t index = z_attachment_get(sample->attachment, z_bytes_from_str("index"));
+    if (z_check(index)) {
+      printf("   message number: %.*s\n", (int)index.len, index.start);
     }
-    z_drop(z_move(keystr));
+  }
+  z_drop(z_move(keystr));
 }
-

In 1.0.0, attachments were greatly simplified. They are now represented as z_..._bytes_t (i.e. the same type we use to represent serialized data) and can thus contain data in any format.

// publish attachment
+

In 1.0.0, attachments were greatly simplified. They are now represented as z_..._bytes_t (i.e. the same type we use to represent payload data) and can thus contain data in any format.

// publish attachment
 typedef struct {
   char* key;
   char* value;
 } kv_pair_t;
 
-typedef struct kv_it {
-    kv_pair_t *current;
-    kv_pair_t *end;
-} kv_it;
-
-bool create_attachment_iter(z_owned_bytes_t* kv_pair, void* context) {
-  kv_it* it = (kv_it*)(context);
-  if (it->current == it->end) {
-    return false;
-  }
-  z_owned_bytes_t k, v;
-  // serialize as key-value pair
-  z_bytes_serialize_from_str(&k, it->current->key);
-  z_bytes_serialize_from_str(&v, it->current->value);
-  z_bytes_serialize_from_pair(kv_pair, z_move(k), z_move(v));
-  it->current++;
-  return true;
-};
-
 ...
 
 kv_pair_t attachment_kvs[2] = {;
   (kv_pair_t){.key = "index", .value = "1"},
   (kv_pair_t){.key = "source", .value = "C"}
 }
-kv_it it = { .begin = attachment_kvs, .end = attachment_kvs + 2 };
 
 z_owned_bytes_t payload, attachment;
-// serialzie key value pairs as attachment using z_bytes_serialize_from_iter
-z_bytes_serialize_from_iter(&attachment, create_attachment_iter, (void*)&it);
+// serialzie key value pairs as attachment
+
+ze_owned_serializer_t serializer;
+ze_serializer_empty(&serializer);
+ze_serializer_serialize_sequence_length(z_loan_mut(serializer), 2);
+for (size_t i = 0; i < 2; ++i) {
+    ze_serializer_serialize_str(z_loan_mut(serializer), attachment_kvs[i].key);
+    ze_serializer_serialize_str(z_loan_mut(serializer), attachment_kvs[i].value);
+}
+ze_serializer_finish(z_move(serializer), &payload);
 options.attachment = &attachment;
 
-z_bytes_serialize_from_str(&payload, "payload");
+z_bytes_copy_from_str(&payload, "payload");
 z_publisher_put(z_loan(pub), z_move(payload), &options);
 
 // receive sample with attachment
@@ -342,24 +386,18 @@
   }
 
   // read attachment key-value pairs using bytes_iterator
-  z_bytes_iterator_t iter = z_bytes_get_iterator(attachment);
-  z_owned_bytes_t kv;
-  while (z_bytes_iterator_next(&iter, &kv)) {
-      z_owned_bytes_t k, v;
-      z_owned_string_t key, value;
-      z_bytes_deserialize_into_pair(z_loan(kv), &k, &v);
-
-      z_bytes_deserialize_into_string(z_loan(k), &key);
-      z_bytes_deserialize_into_string(z_loan(v), &value);
-
-      printf("   attachment: %.*s: '%.*s'\n",
-        (int)z_string_len(z_loan(key)), z_string_data(z_loan(key)),
-        (int)z_string_len(z_loan(value)), z_string_data(z_loan(value)));
-      z_drop(z_move(kv));
-      z_drop(z_move(k));
-      z_drop(z_move(v));
-      z_drop(z_move(key));
-      z_drop(z_move(value));
+  ze_deserializer_t deserializer = ze_deserializer_from_bytes(z_loan(payload));
+  size_t num_elements = 0;
+  ze_deserializer_deserialize_sequence_length(&deserializer, &num_elements);
+  z_owned_string_t key, value;
+  for (size_t i = 0; i < num_elements; ++i) {
+    ze_deserializer_deserialize_string(&deserializer, &key);
+    ze_deserializer_deserialize_string(&deserializer, &value);
+    printf("   attachment: %.*s: '%.*s'\n",
+      (int)z_string_len(z_loan(key)), z_string_data(z_loan(key)),
+      (int)z_string_len(z_loan(value)), z_string_data(z_loan(value)));
+    z_drop(z_move(key));
+    z_drop(z_move(value));
   }
 }
 

Encoding

Encoding handling has been reworked: before one would use an enum id and a string suffix value, now only the encoding metadata needs to be registered using z_encoding_from_str.

There is a set of predefined constant encodings subject to some wire-level optimization. To benefit from this, the provided encoding should follow the format: "<predefined constant>;<optional additional data>"

All predefined constants provided can be found in here Encoding Variants

  • Zenoh 0.11.x
char *value = "Some data to publish on Zenoh";
@@ -446,7 +484,7 @@
     _0: [u8; 224],
 }
 

Get attachment

const struct z_loaned_bytes_t *z_sample_attachment(const struct z_loaned_sample_t *this_);
-

In zenoh-pico, we recommend users follow the accessor pattern even though the struct z_sample_t is explicitly defined in the library.

Usage of z_bytes_clone

In short, z_bytes_t is made of reference-counted data slices. In 1.0.0, we aligned the implementation of z_bytes_clone and made it perform a shallow copy for improved efficiency.

  • Zenoh 0.11.x
ZENOHC_API struct zc_owned_payload_t zc_sample_payload_rcinc(const struct z_sample_t *sample);
+

In zenoh-pico, we recommend users follow the accessor pattern even though the struct z_sample_t is explicitly defined in the library.

Usage of z_bytes_clone

In short, z_owned_bytes_t is made of reference-counted data slices. In 1.0.0, we aligned the implementation of z_bytes_clone and made it perform a shallow copy for improved efficiency.

  • Zenoh 0.11.x
ZENOHC_API struct zc_owned_payload_t zc_sample_payload_rcinc(const struct z_sample_t *sample);
 
  • Zenoh 1.0.0
ZENOHC_API void z_bytes_clone(struct z_owned_bytes_t *dst, const struct z_loaned_bytes_t *this_);
 

NOTE: We don’t offer a deep copy API. However, users can create a deep copy by deserializing the z_bytes_t into a zenoh object. For example, use z_bytes_deserialize_into_slice to deserialize it into a z_owned_slice_t and then call z_slice_data to obtain a pointer to uint8_t data.

Zenoh-C Specific

Shared Memory

Shared Memory subsystem has been heavily reworked and improved. The key functionality changes are:

  • Buffer reference counting is now robust across abnormal process termination
  • Support plugging of user-defined SHM implementations
  • Dynamic SHM transport negotiation: Sessions are interoperable with any combination of SHM configuration and physical location
  • Support aligned allocations
  • Manual buffer invalidation
  • Buffer write access
  • Rich buffer allocation interface

⚠️ Please note that SHM API is still unstable and will be improved in the future.

SharedMemoryManager → SHMProvider + SHMProviderBackend

  • Zenoh 0.11.x
// size to dedicate to SHM manager
 const size_t size = 1024 * 1024;
@@ -454,7 +492,7 @@
 const z_id_t id = z_info_zid(z_loan(s));
 char idstr[33];
 for (int i = 0; i < 16; i++) {
-    sprintf(idstr + 2 * i, "%02x", id.id[i]);
+  sprintf(idstr + 2 * i, "%02x", id.id[i]);
 }
 idstr[32] = 0;
 // create SHM manager
@@ -494,8 +532,8 @@
 // allocation behavior and handle allocation failures automatically
 z_shm_provider_alloc_gc(&alloc, z_loan(provider), alloc_size, alignment);
 if (!z_check(alloc.buf)) {
-    printf("Failed to allocate an SHM buffer, even after GCing\n");
-    exit(-1);
+  printf("Failed to allocate an SHM buffer, even after GCing\n");
+  exit(-1);
 }
 
Next up: Python