diff --git a/config.yaml b/config.yaml index 175c4803..e7df79ba 100644 --- a/config.yaml +++ b/config.yaml @@ -28,7 +28,9 @@ menu: - identifier: "APIs" name: "API" weight: 4000 - - identifier: "migration" - name: "Migration guides" + - identifier: "migration_0.5_to_0.6" + name: "Migration guides v0.5.x → v0.6.x" weight: 5000 - \ No newline at end of file + - identifier: "migration_1.0" + name: "Migration guides v0.11 → v1.0 " + weight: 6000 diff --git a/content/docs/migration/MigrationGuide-C-v0.5.x-v0.6.x.md b/content/docs/migration_0.5_to_0.6/MigrationGuide-C-v0.5.x-v0.6.x.md similarity index 99% rename from content/docs/migration/MigrationGuide-C-v0.5.x-v0.6.x.md rename to content/docs/migration_0.5_to_0.6/MigrationGuide-C-v0.5.x-v0.6.x.md index b008a1bd..7c2a8f1a 100644 --- a/content/docs/migration/MigrationGuide-C-v0.5.x-v0.6.x.md +++ b/content/docs/migration_0.5_to_0.6/MigrationGuide-C-v0.5.x-v0.6.x.md @@ -3,7 +3,7 @@ title: "Migrating from Zenoh-C v0.5.x zenoh-net API to Zenoh-C v0.6.x zenoh API" weight : 5400 menu: docs: - parent: migration + parent: migration_0.5_to_0.6 --- ### Opening a session diff --git a/content/docs/migration/MigrationGuide-Pico-v0.5.x-v0.6.x.md b/content/docs/migration_0.5_to_0.6/MigrationGuide-Pico-v0.5.x-v0.6.x.md similarity index 99% rename from content/docs/migration/MigrationGuide-Pico-v0.5.x-v0.6.x.md rename to content/docs/migration_0.5_to_0.6/MigrationGuide-Pico-v0.5.x-v0.6.x.md index 18bb4d9d..1b86f2d5 100644 --- a/content/docs/migration/MigrationGuide-Pico-v0.5.x-v0.6.x.md +++ b/content/docs/migration_0.5_to_0.6/MigrationGuide-Pico-v0.5.x-v0.6.x.md @@ -3,7 +3,7 @@ title: "Migrating from Zenoh-Pico v0.5.x to Zenoh-Pico v0.6.x" weight : 5600 menu: docs: - parent: migration + parent: migration_0.5_to_0.6 --- ## General considerations about the new Zenoh-Pico v0.6.x API diff --git a/content/docs/migration/MigrationGuide-Python-v0.5.x-v0.6.x.md b/content/docs/migration_0.5_to_0.6/MigrationGuide-Python-v0.5.x-v0.6.x.md similarity index 99% rename from content/docs/migration/MigrationGuide-Python-v0.5.x-v0.6.x.md rename to content/docs/migration_0.5_to_0.6/MigrationGuide-Python-v0.5.x-v0.6.x.md index a28d0839..3f278f60 100644 --- a/content/docs/migration/MigrationGuide-Python-v0.5.x-v0.6.x.md +++ b/content/docs/migration_0.5_to_0.6/MigrationGuide-Python-v0.5.x-v0.6.x.md @@ -3,7 +3,7 @@ title: "Migrating from Zenoh v0.5.x Python API to Zenoh v0.6.x Python API" weight : 5500 menu: docs: - parent: migration + parent: migration_0.5_to_0.6 --- ## Explorability diff --git a/content/docs/migration/MigrationGuide-Rust-v0.5.x-v0.6.x.md b/content/docs/migration_0.5_to_0.6/MigrationGuide-Rust-v0.5.x-v0.6.x.md similarity index 99% rename from content/docs/migration/MigrationGuide-Rust-v0.5.x-v0.6.x.md rename to content/docs/migration_0.5_to_0.6/MigrationGuide-Rust-v0.5.x-v0.6.x.md index 16ce8463..3c852bd8 100644 --- a/content/docs/migration/MigrationGuide-Rust-v0.5.x-v0.6.x.md +++ b/content/docs/migration_0.5_to_0.6/MigrationGuide-Rust-v0.5.x-v0.6.x.md @@ -3,7 +3,7 @@ title: "Migrating from Zenoh v0.5.x Rust API to Zenoh v0.6.x Rust API" weight : 5300 menu: docs: - parent: migration + parent: migration_0.5_to_0.6 --- In zenoh version 0.6.0, zenoh and zenoh-net APIs have been merged into a single API. diff --git a/content/docs/migration/MigrationGuide-ZenohC-ZenohPico.md b/content/docs/migration_0.5_to_0.6/MigrationGuide-ZenohC-ZenohPico.md similarity index 99% rename from content/docs/migration/MigrationGuide-ZenohC-ZenohPico.md rename to content/docs/migration_0.5_to_0.6/MigrationGuide-ZenohC-ZenohPico.md index 1a34a147..ac91e80c 100644 --- a/content/docs/migration/MigrationGuide-ZenohC-ZenohPico.md +++ b/content/docs/migration_0.5_to_0.6/MigrationGuide-ZenohC-ZenohPico.md @@ -3,7 +3,7 @@ title: "Migrating from Zenoh-C to Zenoh-Pico (and vice-versa)" weight : 5700 menu: docs: - parent: migration + parent: migration_0.5_to_0.6 --- Both Zenoh-C and Zenoh-Pico APIs offer a C client API for the zenoh protocol, thus this release took an extra step to make Zenoh-C code to be compatible with Zenoh-Pico code (and vice-versa). Such approach aids users to easily migrate its Zenoh-based code to microcontrollers and embedded systems. diff --git a/content/docs/migration/MigrationGuide-v0.5.x-v0.6.x.md b/content/docs/migration_0.5_to_0.6/MigrationGuide-v0.5.x-v0.6.x.md similarity index 99% rename from content/docs/migration/MigrationGuide-v0.5.x-v0.6.x.md rename to content/docs/migration_0.5_to_0.6/MigrationGuide-v0.5.x-v0.6.x.md index 010356a2..f200e3a2 100644 --- a/content/docs/migration/MigrationGuide-v0.5.x-v0.6.x.md +++ b/content/docs/migration_0.5_to_0.6/MigrationGuide-v0.5.x-v0.6.x.md @@ -3,7 +3,7 @@ title: "Migrating from Zenoh v0.5.x to Zenoh v0.6.x" weight : 5200 menu: docs: - parent: migration + parent: migration_0.5_to_0.6 --- ## Key expressions diff --git a/content/docs/migration_1.0/C++.md b/content/docs/migration_1.0/C++.md new file mode 100644 index 00000000..745e55fa --- /dev/null +++ b/content/docs/migration_1.0/C++.md @@ -0,0 +1,363 @@ +--- +title: "C++" +weight : 6300 +menu: + docs: + parent: migration_1.0 +--- + + +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`. For instance: + +```cpp +std::variant 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. + +```cpp +static Config client(const std::vector& peers, ZError* err = nullptr); +``` + +This also applies to constructors: if a failure occurs, either an exception is thrown or the error code is written to the provided pointer. In the latter case, the returned object will be in an "empty" state (i.e. converting it to a boolean returns `false`). + +```cpp +Config config = Config::create_default(); +// Receiving an error code +Zerror err = Z_OK; +auto session = Session::open(std::move(config), &err); +if (err != Z_OK) { // or alternatively if (!session) + // handle failure +} + +// Catching exception +Zenoh::session s(nullptr); // Empty session object +try { + s = Session::open(std::move(config), &err); +} 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*` using the `BytesView` class: + +```cpp +publisher.put("my_payload"); +``` + +In 1.0.0, the `BytesView` class is gone and we introduced the `Bytes` object which represents a serialized payload. + + + +```cpp +void publish_data(const Publisher& publisher, const MyData& data) { + publisher.put(Bytes::serialize(data)); +} + +void receive_data(const Sample &sample) { + std::cout <<"Received: " + << sample.get_payload().deserialize() + << "\n"; +}; +``` + +We added a default `ZenohCodec`, which provides default serialization / deserialization for common numerical types, strings, and containers: + +```cpp + // stream of bytes serialization + std::vector data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + Bytes b = Bytes::serialize(data); + assert(b.deserialize>() == data); + + // arithmetic type serialization + double pi = 3.1415926; + Bytes b = Bytes::serialize(pi); + assert(b.deserialize() == pi); + + // Composite types serialization + std::vector v = {0.1f, .2f, 0.3f}; + auto b = Bytes::serialize(v); + assert(b.deserialize() == v); + + std::map> m = { + {"a", {0.5, 0.2}}, + {"b", {-123.45, 0.4}}, + {"abc", {3.1415926, -1.0} } + }; + + b = Bytes::serialize(m); + assert(b.deserialize() == 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: + +```cpp +MyType my_data(...); +MyCodec my_codec(...); +Bytes b = Bytes::serialize(my_data, my_codec); +// or Bytes::serialize(my_data) if MyCodec is a stateless codec with an empty constructor +assert(b.deserialize>(my_codec) == my_data); +// or assert(b.deserialize>() == 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` : + +```cpp +// callback +session.get(keyexpr, "", {on_reply, on_done}, opts); + +// stream handlers interface +auto [send, recv] = reply_fifo_new(16); +session.get(keyexpr, "", std::move(send), opts); + +Reply reply(nullptr); +// blocking +for (recv(reply); reply.check(); recv(reply)) { + auto sample = expect(reply.get()); + std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '" + << sample.get_payload().as_string_view() << "')\n"; +} + +// non-blocking +for (bool call_success = recv(reply); !call_success || reply.check(); call_success = recv(reply)) { + if (!call_success) { + std::cout << "."; + Sleep(1); + continue; + } + auto sample = expect(reply.get()); + std::cout << "\nReceived ('" << sample.get_keyexpr().as_string_view() << "' : '" + << sample.get_payload().as_string_view() << "')"; +} +``` + +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. + +```cpp +// callback +session.get( + keyexpr, "", on_reply, on_done, + {.target = Z_QUERY_TARGET_ALL} +); + +// stream handlers interface +auto replies = session.get( + keyexpr, "", channels::FifoChannel(16), + {.target = QueryTarget::Z_QUERY_TARGET_ALL} +); +// blocking +for (auto res = replies.recv(); std::has_alternative(res); res = replies.recv()) { + const auto& sample = std::get(res).get_ok(); + std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '" + << sample.get_payload().deserialize() << "')\n"; +} +// non-blocking +while (true) { + auto res = replies.try_recv(); + if (std::has_alternative(res)) { + const auto& sample = std::get(res).get_ok(); + std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '" + << sample.get_payload().deserialize() << "')\n"; + } else if (std::get(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 << "."; + std::this_thread::sleep_for(1s); + continue; + } else { // std::get(res) == channels::RecvError::Z_DISCONNECTED + break; // no more replies will arrive + } +} +std::cout << std::endl; + +``` + +The same works for `Subscriber` and `Queryable`: + +```cpp +// callback +auto data_callback = [](const Sample &sample) { + std::cout << ">> [Subscriber] Received ('" + << sample.get_keyexpr().as_string_view() + << "' : '" << sample.get_payload().deserialize() + << "')\n"; +}; + +auto subscriber = session.declare_subscriber( + keyexpr, data_callback, closures::none // or dedicated function to call when subscriber is undeclared +); +std::cout << "Press CTRL-C to quit...\n"; +while (true) { + std::this_thread::sleep_for(1s); +} + + +// stream handlers interface +auto subscriber = session.declare_subscriber(keyexpr, channels::FifoChannel(16)); +const auto& messages = subscriber.handler(); +//blocking +for (auto res = messages.recv(); std::has_alternative(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(res); + std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '" + << sample.get_payload().deserialize() << "')\n"; +} +// non-blocking +while (true) { + auto res = messages.try_recv(); + if (std::has_alternative(res)) { + const auto& sample = std::get(res); + std::cout << "Received ('" << sample.get_keyexpr().as_string_view() << "' : '" + << sample.get_payload().deserialize() << "')\n"; + } else if (std::get(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 << "."; + std::this_thread::sleep_for(1s); + } else { // std::get(res) == channels::RecvError::Z_DISCONNECTED + break; // no more samples will arrive + } +} +std::cout << std::endl; + +``` + +## Attachment + +In version 0.11.0 an attachment could only represent a set of key-value pairs and had a somewhat complicated interface: + +```cpp +// publish message with attachment +options.set_encoding(Z_ENCODING_PREFIX_TEXT_PLAIN); +std::unordered_map attachment_map = { + {"source", "C++"}, + {"index", "0"} +}; +options.set_attachment(attachment_map); +pub.put(s, options); + +// subscriber callback function receiving message with attachment +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; + } + } +}; +``` + +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. + + +```c++ +// publish a message with attachment +auto session = Session::open(std::move(config)); +auto pub = session.declare_publisher(KeyExpr(keyexpr)); +// send some key-value pairs as attachment +// allocate attachment map +std::unordered_map attachment_map = { + {"source", "C++"}, + {"index", "0"} +}; +pub.put( + Bytes::serialize("my_payload"), + {.encoding = Encoding("text/plain"), .attachment = std::move(attachment_map)} +); + + +// subscriber callback function receiving a message with attachment +void data_handler(const Sample &sample) { + std::cout << ">> [Subscriber] Received ('" + << sample.get_keyexpr().as_string_view() + << "' : '" + << sample.get_payload().deserialize() + << "')\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>(); + for (auto&& [key, value]: attachment) { + std::cout << " attachment: " << key << ": '" << value << "'\n"; + } +}; +``` + + +# Optional Parameters + +Handling for optional parameters for Zenoh functions was simplified. There are no more getters/setters and all fields of option structures are public. Also option arguments are automatically set to their default values, and if your compiler has support for designated initializers, it is sufficient to only set the fields that are needed to be different from default ones. + +In version 0.11.0: + +```cpp +GetOptions opts; +opts.set_target(Z_QUERY_TARGET_ALL); +opts.set_value(value); + +... + +session.get(keyexpr, "", {on_reply, on_done}, opts); +``` + +In 1.0.0: + +```cpp +session.get(keyexpr, "", on_reply, on_done, {.target = Z_QUERY_TARGET_ALL, .payload = Bytes::serialize(value)}); +``` \ No newline at end of file diff --git a/content/docs/migration_1.0/C_Pico.md b/content/docs/migration_1.0/C_Pico.md new file mode 100644 index 00000000..e5b57f61 --- /dev/null +++ b/content/docs/migration_1.0/C_Pico.md @@ -0,0 +1,806 @@ +--- +title: "C / Pico" +weight : 6400 +menu: + docs: + parent: migration_1.0 +--- + +## 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 + +```c +z_owned_session_t session = z_open(z_move(config)); +if (!z_check(session)) { + return -1; +} +z_close(z_move(session)); +``` + +- Zenoh 1.0.0 + +```c +z_owned_session_t session; +if (z_open(&session, z_move(config), &opts) < 0) { + 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 + +```c +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); + } +} +``` + +- Zenoh 1.0.0 + +```c +void reply_handler(const z_loaned_reply_t *reply, void *ctx) { + if (z_reply_is_ok(reply)) { + const z_loaned_sample_t *sample = z_reply_ok(reply); + z_owned_string_t replystr; + z_bytes_deserialize_into_string(z_sample_payload(sample), &replystr); + printf(">> Received ('%s')\n", z_string_data(z_loan(replystr))); + } +} +``` + +Certain loaned types can be copied into owned, using the `z_clone` function. +All objects that can be cloned will have a function with the signature `z_xxx_clone`. +Please consult the docs of each type to determine if it can be copied into owned + +```c +void reply_handler(const z_loaned_reply_t *reply, void *ctx) { + some_struct_t *some_ctx = (some_struct_t *)(ctx); + z_clone(some_ctx->query, query); + some_ctx->has_query = true; +} +``` + +In the case of our callback, this allows the data from the loaned type to be used in another thread context. Note that this other thread should not forget to call `z_drop` to avoid a memory leak! + +### View types + +View types are only wrappers to user allocated data, like `z_view_keyexpr_t.` These types can be loaned in the same way as owned types but they don’t need to be dropped explicitly (user is fully responsible for deallocation of wrapped data). + +- Zenoh 0.11.x + +```c +const char *keyexpr = "example/demo/*"; +z_owned_subscriber_t sub = z_declare_subscriber(z_loan(session), z_keyexpr(keyexpr), z_move(callback), NULL); +if (!z_check(sub)) { + printf("Unable to declare subscriber.\n"); + return -1; +} +``` + +- Zenoh 1.0.0 + +```c +const char *keyexpr = "example/demo/*"; +z_owned_subscriber_t sub; +z_view_keyexpr_t ke; +z_view_keyexpr_from_str(&ke, keyexpr); +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 + +```c +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; +} +``` + +- Zenoh 1.0.0 + +```c +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; +} +``` + +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: + +```c +void send_data(const z_loaned_publisher_t* pub, const uint8_t *data, size_t len) { + 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 +} + +void receive_data(const z_loaned_bytes_t* payload) { + z_owned_slice_t slice; + z_bytes_deserialize_into_slice(payload, &slice); + + // 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)) + + // in the end slice should be dropped since it contains a copy of the payload data + z_drop(z_move(slice)); +} +``` + +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: + +```c +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: + +```c +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)); +} + +// an example of sending a data with more complex destructor +// a case of std::vector from c++ stl library + +void delete_vector(void *data, void* context) { + std::vector *v = (std::vector *)context; + delete v; + // in this case data pointer is not used for destruction +} + +void send_move_vector(std::vector *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: + +```c +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)); +} +``` + +## Channel Handlers and Callbacks + +In version 0.11.0 Channel handlers were only supported for `z_get`and `z_owned_queryable_t`: + +```c +// callback +z_owned_closure_reply_t callback = z_closure(reply_handler); +z_get(z_loan(session), z_keyexpr(keyexpr), "", z_move(callback), &opts); + +// Channel handlers interface +// blocking +z_owned_reply_channel_t channel = zc_reply_fifo_new(16); +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)); +} +z_drop(z_move(channel)); + +// non-blocking +z_owned_reply_channel_t channel = zc_reply_non_blocking_fifo_new(16); +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)); +} +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. + +```c +// callback +z_owned_closure_reply_t callback = z_closure(reply_handler); +z_get(z_loan(session), z_keyexpr(keyexpr), "", z_move(callback), &opts); + +// stream handlers interface +z_owned_fifo_handler_reply_t handler; +z_owned_closure_reply_t closure; +z_fifo_channel_reply_new(&closure, &handler, 16); +z_get(z_loan(s), z_loan(keyexpr), "", z_move(closure), z_move(opts)); +z_owned_reply_t reply; + +// 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)); +} + +// 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 + } +} + +``` + +The same now also works for `Subscriber` and `Queryable` : + +```c +// callback +// callback +void data_handler(const z_loaned_sample_t *sample, void *context) { + // do something with sample +} + +z_owned_closure_sample_t callback; +z_closure(&callback, data_handler, NULL, NULL); +z_owned_subscriber_t sub; +if (z_declare_subscriber(&sub, z_loan(session), z_loan(keyexpr), z_move(callback), NULL) < 0) { + printf("Unable to declare subscriber.\n"); + exit(-1); +} + +// stream handlers interface +z_owned_fifo_handler_sample_t handler; +z_owned_closure_sample_t closure; +z_fifo_channel_reply_new(&closure, &handler, 16); +z_owned_subscriber_t sub; +if (z_declare_subscriber(&sub, z_loan(session), z_loan(keyexpr), z_move(closure), NULL) < 0) { + printf("Unable to declare subscriber.\n"); + exit(-1); +} + +z_owned_sample_t sample; +// blocking +while (z_recv(z_loan(handler), &sample) == Z_OK) { + // z_recv will block until there is at least one sample in the Fifo buffer + // it will return an empty sample and is_alive=false once subscriber gets disconnected + + // do something with sample + z_drop(z_move(sample)); +} + +// 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 + } +} +``` + +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: + +```c +// publish message with attachment +char *value = "Some data to publish on Zenoh"; + +z_put_options_t options = z_put_options_default(); +z_owned_bytes_map_t map = z_bytes_map_new(); +z_bytes_map_insert_by_alias(&map, _z_bytes_wrap((uint8_t *)"test", 2), _z_bytes_wrap((uint8_t *)"attachement", 5)); +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; +} + +z_bytes_map_drop(&map); + +// 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; +} + +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); + } + } + 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. + +```c +// 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); +options.attachment = &attachment; + +z_bytes_serialize_from_str(&payload, "payload"); +z_publisher_put(z_loan(pub), z_move(payload), &options); + +// receive sample with attachment + +void data_handler(const z_loaned_sample_t *sample, void *arg) { + z_view_string_t key_string; + z_keyexpr_as_view_string(z_sample_keyexpr(sample), &key_string); + + z_owned_string_t payload_string; + z_bytes_deserialize_into_string(z_sample_payload(sample), &payload_string); + + printf(">> [Subscriber] Received %s ('%.*s': '%.*s')\n", kind_to_str(z_sample_kind(sample)), + (int)z_string_len(z_loan(key_string)), z_string_data(z_loan(key_string)), + (int)z_string_len(z_loan(payload_string)), z_string_data(z_loan(payload_string))); + z_drop(z_move(payload_string)); + + const z_loaned_bytes_t *attachment = z_sample_attachment(sample); + // checks if attachment exists + if (attachment == NULL) { + return; + } + + // 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)); + } +} +``` + +## 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: `";"` + +All predefined constants provided can be found in here [Encoding Variants](https://github.com/eclipse-zenoh/zenoh-pico/blob/10ddde219be41fc0b43bad4d19f571625a27c161/src/api/api.c#L218-L285) + + +- Zenoh 0.11.x + +```c +char *value = "Some data to publish on Zenoh"; +z_put_options_t options = z_put_options_default(); +options.encoding = z_encoding(Z_ENCODING_PREFIX_TEXT_PLAIN, "utf8"); + +if (z_put(z_loan(session), z_keyexpr(ke), (const uint8_t *)value, strlen(value), &options) < 0) { + return -1; +} +``` + +- Zenoh 1.0.0 + +```c +char *value = "Some data to publish on Zenoh"; +z_owned_bytes_t payload; +z_bytes_serialize_from_str(&payload, value); + +z_publisher_put_options_t options; +z_publisher_put_options_default(&options); +z_encoding_from_str(&encoding, "text/plain;utf8"); +options.encoding = z_move(encoding); + +if (z_put(z_loan(session), z_loan(ke), z_move(payload), &options) < 0) { + return -1; +} +``` + +## Timestamps + +The generation of timestamps is now tied to a Zenoh session, with the timestamp inheriting the `ZenohID` of the session. + +- Zenoh 0.11.x + +```c +// Didn't exist +``` + +- Zenoh 1.0.0 + +```c +z_timestamp_t ts; +z_timestamp_new(&ts, z_loan(s)); +options.timestamp = &ts; +z_publisher_put(z_loan(pub), z_move(payload), &options); +``` + +## Error Handling + +In 1.0.0, we unified the return type of Zenoh functions as `z_result_t`. For example, + +- Zenoh 0.11.x +```c +int8_t z_put(struct z_session_t session, + struct z_keyexpr_t keyexpr, + const uint8_t *payload, + size_t len, + const struct z_put_options_t *opts); + +``` + +- Zenoh 1.0.0 + +```c +z_result_t z_put(const struct z_loaned_session_t *session, + const struct z_loaned_keyexpr_t *key_expr, + struct z_owned_bytes_t *payload, + struct z_put_options_t *options); + +``` + +## KeyExpr / String Conversion + +In 1.0.0, we have reworked the conversion between key expressions and strings, illustrated below. + +- Zenoh 0.11.x + +```c +// keyexpr => string +z_owned_str_t keystr = z_keyexpr_to_string(z_loan(z_owned_keyexpr_t)); + +// keyexpr => const char * +z_loan(keystr) + +// const char* => keystr +z_owned_keyexpr_t keyexpr = z_keyexpr_new(const char*) + +``` + +- Zenoh 1.0.0 + +```c +// keyexpr => string +z_view_string_t keystr; +z_keyexpr_as_view_string(z_loan(z_owned_keyexpr_t), &keystr); + +// z_view_string_t => const char* +z_string_data(z_loan(keystr)) + +// const char* => keystr +z_owned_keyexpr_t keyexpr; +z_error_t res = z_keyexpr_from_string(&keyexpr, const char *); + +``` + +NOTE: Based on efficiency considerations, the char pointer of `z_string_data` might not be null-terminated in zenoh-c. To read the string data, we need to pair it with the length `z_string_len`. + +```c +z_view_str_t keystr; +z_keyexpr_as_view_string(z_loan(keyexpr), &keystr); +printf("%.*s", (int)z_string_len(z_loan(keystr)), z_string_data(z_loan(keystr))); +``` + +And to compare the string with `strncmp` instead of `strcmp`. + +```c +z_view_str_t keystr; +z_keyexpr_as_view_string(z_loan(keyexpr), &keystr); +const char* target = "string"; +strncmp(target, z_string_data(z_loan(keystr)), z_string_len(z_loan(keystr))); +``` + +## Accessor Pattern + +In 1.0.0, we have made our API more convenient and consistent to use across languages. We use opaque types to wrap the raw Zenoh data from the Rust library in zenoh-c. With this change, we introduce the accessor pattern to read the field of a struct. + +For instance, to get the attachment of a sample in zenoh-c: + +- 0.11.0 + +```c +typedef struct z_sample_t { + struct z_keyexpr_t keyexpr; + struct z_bytes_t payload; + struct z_encoding_t encoding; + const void *_zc_buf; + enum z_sample_kind_t kind; + struct z_timestamp_t timestamp; + struct z_qos_t qos; + struct z_attachment_t attachment; +} z_sample_t; +``` + +- 1.0.0 + +Opaque type of `z_sample` + +```rust +/// An owned Zenoh sample. +/// +/// This is a read only type that can only be constructed by cloning a `z_loaned_sample_t`. +/// Like all owned types, it should be freed using z_drop or z_sample_drop. +#[derive(Copy, Clone)] +#[repr(C, align(8))] +pub struct z_owned_sample_t { + _0: [u8; 224], +} +/// A loaned Zenoh sample. +#[derive(Copy, Clone)] +#[repr(C, align(8))] +pub struct z_loaned_sample_t { + _0: [u8; 224], +} +``` + +Get attachment + +```c +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 + +```c +ZENOHC_API struct zc_owned_payload_t zc_sample_payload_rcinc(const struct z_sample_t *sample); +``` + +- Zenoh 1.0.0 + +```c +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 + +```c +// size to dedicate to SHM manager +const size_t size = 1024 * 1024; +// construct session id string +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]); +} +idstr[32] = 0; +// create SHM manager +zc_owned_shm_manager_t manager = zc_shm_manager_new(z_loan(s), idstr, size); +``` + +- Zenoh 1.0.0 + +```c +// size to dedicate to SHM provider +const size_t total_size = 1024 * 1024; + +// Difference: SHM provider now respects alignment +z_alloc_alignment_t alignment = {0}; +z_owned_memory_layout_t layout; +z_memory_layout_new(&layout, total_size, alignment); + +// create SHM provider +z_owned_shm_provider_t provider; +z_posix_shm_provider_new(&provider, z_loan(layout)); +``` + +**Buffer allocation** + +- Zenoh 0.11.x + +```c +// buffer size +const size_t alloc_size = 1024; + +// allocate SHM buffer +zc_owned_shmbuf_t shmbuf = zc_shm_alloc(&manager, alloc_size); +if (!z_check(shmbuf)) { + zc_shm_gc(&manager); + shmbuf = zc_shm_alloc(&manager, alloc_size); + if (!z_check(shmbuf)) { + printf("Failed to allocate an SHM buffer, even after GCing\n"); + exit(-1); + } +} +``` + +- Zenoh 1.0.0 + +```c +// buffer size and alignment +const size_t alloc_size = 1024; +// Diffrence: allocation now respects alignment +z_alloc_alignment_t alignment = {0}; + +// allocate SHM buffer +z_buf_layout_alloc_result_t alloc; +// Difference: there is a rich set of policies available to control +// 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); +} +``` diff --git a/content/docs/migration_1.0/Concepts.md b/content/docs/migration_1.0/Concepts.md new file mode 100644 index 00000000..eb04e85b --- /dev/null +++ b/content/docs/migration_1.0/Concepts.md @@ -0,0 +1,160 @@ +--- +title: "Concepts" +weight : 6100 +menu: + docs: + parent: migration_1.0 +--- + +The Zenoh team have been hard at work preparing an official version 1.0.0 of Zenoh! +This major release comes with several API changes, quality of life improvements and developer conveniences. + +We now have a more stable API and intend to keep backward compatibility in future Zenoh revisions. +This guide is here to ease the transition to Zenoh 1.0.0 for our users! + +## Value is gone, long live ZBytes +We have replaced `Value` with `ZBytes` and `Encoding`. +`ZBytes` is the type core to data representation in Zenoh, all API's have been reworked to accept `ZBytes` or something that can be converted into a `ZBytes`. +We have added a number of conversion implementations for language primitives as well as methods to seamlessly allow user defined structs to be serialized into `ZBytes`. +`Sample`'s payloads are now `ZBytes`. `Publisher`, `Queryable` and `Subscriber` now expect `ZBytes` for all their interfaces. The [Attachment](#attachment) API also now accepts `ZBytes`. + + +Each Language bindings will have their own specifics of Serializing and Deserializing, but for the most part it will involve implementing a serialize / deserialize function for your datatype or make use of auto-generated conversions for composite types. + +## Encoding + +`Encoding` has been reworked, moving away from enumerables to now accepting strings. +While Zenoh does not impose any `Encoding` requirement on the user, providing an `Encoding` can offer automatic wire-level optimization for known types. +For the user defined `Encoding`, it can be thought of as optional metadata, carried over by Zenoh in such a way that the end user’s application may perform different operations based on `Encoding`. +We have expanded our list of predefined encoding types from Zenoh 0.11.0 to include variants for numerous IANA standard encodings, including but not limited to `video/x` , `application/x`, `text/x`, `image/x` and `audio/x` encoding families, as well as an encoding family specific to Zenoh defined by the prefix `zenoh/x` . +Users can also define their own encoding scheme that does not need to be based on the predefined IANA variants. Upon declaring a specific encoding, the users encoding scheme will be prefixed with `zenoh/bytes` for distinction. + + +## Attachment + +We have made attachment more flexible across API’s, essentially accepting anything that can be converted to a `ZBytes` as an optional extra to `put` , `delete` , on `Query`'s and Query `reply`'s. +We also allow for composite types to be converted into `ZBytes`, meaning that using the Attachment API as a metadata transport is easier than ever. + +## Query & Queryable + +The `reply` method of a `Queryable` has gained two variants: `reply_del` and `reply_err` to respectively indicate that a deletion should be performed and that an error occurred. +Additionally, the 3 variants behave similarly to `put` and `del`, hence providing improved ergonomics. + +We have added the ability to get the underlying `Handler` of a Queryable as well. + +## Use accessors to get private members +Across language bindings we encapsulate members of structs, and they can’t be accessed directly now. +The only way to access struct values is to use the getter function associated with them. + + +## Pull Subscribers have been removed + +The concept of a pull subscriber no longer exists in Zenoh. +However, when creating a `Subscriber`, it may be the case that developers only care about the latest data and want to discard the oldest data. +The `RingChannel` can be used to get a similar behaviour. [Rust Example](https://github.com/eclipse-zenoh/zenoh/blob/main/examples/examples/z_pull.rs) +This contrasts with the `FIFOChannel`, the default channel type used internally in Subscribers, which drops new messages once its buffer is full. +You can take a look at examples of usage in any language’s examples/z_pull.x + +## Timestamps +Previously we exposed a function to generate timestamps outside of a session. +Due to our efforts to improve the storage replication logic, users will now have to generate timestamps from a session, with the timestamp inheriting the `ZenohID` of the session. + +This will affect user-created plugins and applications that need to generate timestamps in their Storage and sending of data. +⚠️ Note: Timestamps are important for Storage Alignment (a.k.a. Replication). Data stored in Data bases must include a Timestamp to be properly aligned across Data Stores by Zenoh. +The `timestamping` configuration option must also be enabled for this. + +## Plugins + +### Storages +⚠️ Note: The storage-manager will fail to launch if the `timestamping` configuration option is disabled. +From Zenoh 1.0.0 user-applications can load plugins. +A, somehow, implicit assumption that dictated the behaviour of storages is that the Zenoh node loading them **has to add a timestamp to any received publication that did not have one**. This functionality is controlled by the `timestamping` configuration option. +Until Zenoh 1.0.0 this assumption held true as only a router could load storage and the default configuration for a router enables `timestamping`. However, in Zenoh 1.0.0 nodes configured in `client` & `peer` mode can load storage and *their default configuration disables `timestamping`*. + +### Plugin Loading + +We added the ability for user-applications to load compiled plugins written in Rust, regardless of which language bindings you are using! + +Usage of this feature is [Plugin Loading](#plugin-loading) + +⚠️ Note : When loading a plugin, the Plugin must have been built with the same version of the Rust compiler as the bindings loading it, and the `Cargo.lock` of the plugin must be synced with the same commit of Zenoh. +This means that if the language bindings are using `rustc` version `1.75`, the plugin must: +- Be built with the same toolchain version `1.75` +- Be built with the same Zenoh Commit +- The plugin `Cargo.lock` have had its packages synced with the Zenoh `Cargo.lock` + +The reason behind this strict set of requirements is due to Rust making no guarantees regarding data layout in memory. +This means between compiler versions, the representation may change based on optimizations. +More on this topic at here : [Rust:Type-Layout](https://doc.rust-lang.org/reference/type-layout.html#representations) + +## Config Changes + +### Plugin Loading + +Loading plugins is achieved by enabling the `plugins_loading` section in config file, with the members `enabled` set to true, and specifying the `search_dirs` for the plugins. + +Directories are specified as an object with fields `kind` and `value`. +1. If `kind` is `current_exe_parent`, then the parent of the current executable's directory is searched and `value` should be `null`. + In Bash notation, `{ "kind": "current_exe_parent" }` equals `$(dirname $(which zenohd))` while `"."` equals `$PWD`. +2. If `kind` is `"path"`, then `value` is interpreted as a filesystem path, i.e. `{ "kind": "path" , "value": "path/to/plugin/dir"}`. +Simply supplying a string instead of an object is equivalent to this. +If `enabled: true` and `search_dirs` is not specified then `search_dirs` falls back to the default value of: +`".:~/.zenoh/lib:/opt/homebrew/lib:/usr/local/lib:/usr/lib”` + +```jsx + plugins_loading: { + // Enable plugins loading. + enabled: false, + /// Directories where plugins configured by name should be looked for. Plugins configured by __path__ are not subject to lookup. + /// If `enabled: true` and `search_dirs` is not specified then `search_dirs` falls back to the default value: ".:~/.zenoh/lib:/opt/homebrew/lib:/usr/local/lib:/usr/lib" + search_dirs: [{ "kind": "current_exe_parent" }, ".", "~/.zenoh/lib", "/opt/homebrew/lib", "/usr/local/lib", "/usr/lib"], +} +// ... Rest of Config +``` + +### Mode Dependent endpoints + +Configuration now supports a different list of endpoints depending on the mode zenohd is launched with. +The old behaviour of a single List of endpoints is still supported, applying to `router`, `peer` and `client`, however users can now set endpoints per mode: + +```jsx + connect: { + /// The list of endpoints to connect to. + /// Accepts a single list (e.g. endpoints: ["tcp/10.10.10.10:7447", "tcp/11.11.11.11:7447"]) + /// or different lists for router, peer and client + endpoints: { router: ["tcp/10.10.10.10:7447"], peer: ["tcp/11.11.11.11:7447"], client: ["tcp/somewhere1::7447", "udp/somewhere2:7447"] } +}, +``` + +⚠️ Note: in `client` mode, `zenohd` will try connect to each endpoint in order until one is successful, then stop subsequent endpoint connection attempts. `client`'s only connect to a single endpoint. + + +### Scouting + +We have implemented a small change in the configuration syntax concerning the `scouting` section. +Both `gossip` and `multicast`’s `autoconnect` section's have changed to accept lists of either +`"peer"`, `"client"` or `"router"` + +```jsx +// Zenoh 0.11.0 +scouting: { + multicast: { + autoconnect: { router: "", peer: "router|peer" }, + }, + gossip: { + autoconnect: { router: "", peer: "router|peer" }, + }, +}, + +// Zenoh 1.0.0 +scouting: { + multicast: { + autoconnect: { router: [], peer: ["router", "peer"] }, + }, + gossip: { + autoconnect: { router: [], peer: ["router", "peer"] }, + }, +} +``` + +Next step is to dive into the migration examples for your favourite language! diff --git a/content/docs/migration_1.0/Java.md b/content/docs/migration_1.0/Java.md new file mode 100644 index 00000000..eb3f3c44 --- /dev/null +++ b/content/docs/migration_1.0/Java.md @@ -0,0 +1,9 @@ +--- +title: "Java" +weight : 6700 +menu: + docs: + parent: migration_1.0 +--- + +The Java API is still a work in progress. This migration guide will be updated as soon as the new Zenoh-Java API is released! \ No newline at end of file diff --git a/content/docs/migration_1.0/Kotlin.md b/content/docs/migration_1.0/Kotlin.md new file mode 100644 index 00000000..532db0a1 --- /dev/null +++ b/content/docs/migration_1.0/Kotlin.md @@ -0,0 +1,381 @@ +--- +title: "Kotlin" +weight : 6800 +menu: + docs: + parent: migration_1.0 +--- + +# Kotlin API migration guide for 1.0.0 + +The API has been extensively modified with the following goals in mind: +- Enhancing the API to be more idiomatic to Kotlin. +- Match the API rework done through the Rust Zenoh API. +- Abstracting users from the underlying native mechanisms + +## Default arguments + +Throughout the 0.11.0 API we were exposing builders, however we decided to replace all of them with the more Kotlin-idiomatic way of the default arguments. + +For instance in 0.11.0: + +```kotlin +session.put(keyExpr, value) + .congestionControl(CongestionControl.BLOCK) + .priority(Priority.REALTIME) + .res() +``` + +Becomes the following in 1.0.0: +```kotlin +session.put(keyExpr, + value, + congestionControl = CongestionControl.BLOCK, + priority = Priority.REALTIME, +) +``` + +_Notice that the `.res()` function has been removed. Now functions exposed by the API execute imediately, without the need of `.res()`. i.e. When doing `session.put(...)` the put is run immediately._ + +This applies to all the builders that were in 0.11.0. Generally all the parameters on each builder now have their corresponding argument available in the functions. + +## Encoding rework + +The `Encoding` class has been modified. In 0.11.0. it had the signature +```kotlin +class Encoding(val knownEncoding: KnownEncoding, val suffix: String = "") +``` +where `KnownEncoding` was an enum. + +In 0.11.0. an encoding instance would be created as follows: +```kotlin +val encoding = Encoding(knownEncoding = KnownEncoding.TEXT_JSON) +``` + +In 1.0.0. we have implemented the following changes: +- `KnownEncoding` enum is removed, instead we provide static `Encoding` instances containing an ID and a description. +- Custom encodings can be created +- The list of pre-defined encodings has been expanded. + +In 1.0.0. the previous example would instead now become: + +```kotlin +val encoding = Encoding.TEXT_JSON +``` + +Custom encodings can be created by specifying an `id` and `suffix` string: + +```kotlin +val encoding = Encoding(id = 123, suffix = "example suffix") +``` +## Session-managed declarations + +Up until 0.11.0, it was up to the user to keep track of their variable declarations to keep them alive, because once the variable declarations were garbage collected, the declarations were closed. This was because each Kotlin variable declaration is associated with a native Rust instance, and in order to avoid leaking the memory of that Rust instance, it was necessary to free it upon dropping the declaration instance. However, this behavior could be counterintuitive, as users were expecting the declaration to keep running despite losing track of the reference to it. + +In this release we introduce a change in which any session declaration is internally associated to the session from which it was declared. Users may want to keep a reference to the declaration in case they want to undeclare it earlier before the session is closed, otherwise, the declaration is kept alive. + +For instance: + +```kotlin +val subscriber = session.declareSubscriber("A/B/C".intoKeyExpr(), + callback = { sample -> println("Receiving sample on 'A/B/C': ${sample.payload}") }).getOrThrow() + +session.declareSubscriber("A/C/D".intoKeyExpr(), + callback = { sample -> println("Receiving sample on 'A/C/D': ${sample.payload}") }) // No variable is associated to the declared session, on 0.11.0 it would have been instantanely dropped +``` + +Therfore, when receiving a 'hello' message on `A/**` we would still see: + +``` +>> Receiving sample on 'A/B/C': hello +>> Receiving sample on 'A/C/D': hello +``` +since both declarations are still alive. + +Now the question is, for how long? What happens first, either when: +- you call `undeclare()` or `close()` to the declaration +- the session is closed, then all the associated declarations are automatically undeclared. + +## Key Expression rework + +KeyExpr instances are not bound to a native key expression anymore, unless they are declared from a session. It is safe to drop the reference to the key expression instance, but the memory management associated to a key expression will differ: +- If the KeyExpr was not declared from a session, then the garbage collector simply claims back the memory. +- If it was declared from a session, then the session keeps track of it and frees the native memory upon closing the session. + +Declaring a KeyExpr on a session results in better performance, since the session is informed that we intend to use a key expression repeatedly. +We also associate a native key expression to a Kotlin key expression instance, avoiding copies. + +## Config loading + +When opening a session, it's now mandatory to provide a configuration to the session, even for a default config: +```kotlin +val config = Config.default() +Session.open(config).onSuccess { + // ... + +} +``` + +The `Config` class has been modified + +- `Config.default()`: returns the default config +- `Config.fromFile(file: File): Result`: allows to load a config file. +- `Config.fromPath(path: Path): Result`: allows to load a config file from a path. +- `Config.fromJson(json: String): Result`: loads the config from a string literal with json format +- `Config.fromJson5(json5: String): Result`: loads the config from a string literal with json5 format +- `Config.fromYaml(yaml: String): Result`: loads the config from a string literal with yaml format +- `Config.fromJsonElement(jsonElement: JsonElement): Result`: loads the config from a kotlin JsonElement. + +In case of failure loading the config, a `Result.Error` is returned. + +## Reliability + +The `Reliability` config parameter used when declaring a `Subscriber` has been moved. It now must be specified when declaring a `Publisher` or when performing a `Put` or a `Delete` operaton. + +## ZBytes serialization / deserialization & replacement of Value + +We have created a new abstraction with the name of `ZBytes`. This class represents the bytes received through the Zenoh network. This new approach has the following implications: + +- `Attachment` class is replaced by `ZBytes`. +- `Value` is replaced by the combination of `ZBytes` and `Encoding`. +- Replacing `ByteArray` to represent payloads + +With `ZBytes` we have also introduced a Serialization and Deserialization for conveinent conversion between `ZBytes` and Kotlin types. + +### Serialization + +We can serialize primitive types into a `ZBytes` instance, that is, converting the data into bytes processed by the zenoh network: + +#### Primitive types + +The following types support serialization: + + * Numeric: `Byte`, `Short`, `Int`, `Long`, `Float` and `Double`. + * `String` + * `ByteArray` + + For the primitive types, there are three ways to serialize them into a `ZBytes`, for instance let's suppose + we want to serialize an `Int`: + + * using the `into()` syntax: + ```kotlin + val exampleInt: Int = 256 + val zbytes: ZBytes = exampleInt.into() + ``` + + * using the `from()` syntax: + ```kotlin + val exampleInt: Int = 256 + val zbytes: ZBytes = ZBytes.from(exampleInt) + ``` + + * using the serialize syntax: + ```kotlin + val exampleInt: Int = 256 + val zbytes: ZBytes = ZBytes.serialize(exampleInt).getOrThrow() + ``` + This approach works as well for the other aforementioned types. + + Using `into()` or `from()` guarantees successful serialization for implemented types. + Using `serialize` requires a generic parameter, and returns a Result, i.e. it can fail based in the type passed in and contents of the input parameter. + + #### Lists + + Lists are supported, but they must be either: + - List of `Number` : (`Byte`, `Short`, `Int`, `Long`, `Float`, `Double`) + - List of `String` + - List of `ByteArray` + - List of `IntoZBytes` + + The serialize syntax must be used: + ```kotlin + val myList = listOf(1, 2, 5, 8, 13, 21) + val zbytes = ZBytes.serialize>(myList).getOrThrow() + ``` + + #### Maps + + Maps are supported as well, with the restriction that their inner types must supported primitives: + - `Number` + - `String` + - `ByteArray` + - `IntoZBytes` + + ```kotlin + val myMap: Map = mapOf("foo" to 1, "bar" to 2) + val zbytes = ZBytes.serialize>(myMap).getOrThrow() + ``` + + ### Deserialization + + #### Primitive types + + * Numeric: `Byte`, `Short`, `Int`, `Long`, `Float` and `Double` + * `String` + * `ByteArray` + + Example: + + For these primitive types, you can use the functions `to`, that is + - `toByte` + - `toShort` + - `toInt` + - `toLong` + - `toDouble` + - `toString` + - `toByteArray` + + For instance, for an Int: + ```kotlin + val example: Int = 256 + val zbytes: ZBytes = exampleInt.into() + val deserializedInt = zbytes.toInt() + ``` + + Alternatively, the deserialize syntax can be used as well: + ```kotlin + val exampleInt: Int = 256 + val zbytes: ZBytes = exampleInt.into() + val deserializedInt = zbytes.deserialize().getOrThrow() + ``` + + #### Lists + + Lists are supported, but they must be deserialized into inner primitive types: + - List of `Number` (`Byte`, `Short`, `Int`, `Long`, `Float` or `Double`) + - List of `String` + - List of `ByteArray` + + To deserialize into a list, use the deserialize syntax as follows: + ```kotlin + val inputList = listOf("sample1", "sample2", "sample3") + payload = ZBytes.serialize(inputList).getOrThrow() + val outputList = payload.deserialize>().getOrThrow() + ``` + + #### Maps + + Maps are supported as well, with the restriction that their inner types must be one of the following: + - `Number` + - `String` + - `ByteArray` + + ```kotlin + val inputMap = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3") + payload = ZBytes.serialize(inputMap).getOrThrow() + val outputMap = payload.deserialize>().getOrThrow() + check(inputMap == outputMap) + ``` + + ### Custom serialization and deserialization + + #### Serialization + + For a user defined class, the class must implement the `IntoZBytes` interface. + For instance: + + ```kotlin + class Foo(val content: String) : IntoZBytes { + + /*Inherits: IntoZBytes*/ + override fun into(): ZBytes = content.into() + } + ``` + + This way, we can do: + ```kotlin + val foo = Foo("bar") + val serialization = ZBytes.serialize(foo).getOrThrow() + ``` + + Implementing the `IntoZBytes` interface on a class allows serializing lists and maps of that type, for instance: + ```kotlin + val list = listOf(Foo("bar"), Foo("buz"), Foo("fizz")) + val zbytes = ZBytes.serialize>(list) + ``` + + #### Deserialization + + Regarding deserialization for custom objects, for the time being (this API will be expanded to + provide further utilities) you need to manually convert the ZBytes into the type you want. + + ```kotlin + val inputFoo = Foo("example") + payload = ZBytes.serialize(inputFoo).getOrThrow() + val outputFoo = Foo.from(payload) + check(inputFoo == outputFoo) + + // List of Foo. + val inputListFoo = inputList.map { value -> Foo(value) } + payload = ZBytes.serialize>(inputListFoo).getOrThrow() + val outputListFoo = payload.deserialize>().getOrThrow().map { zbytes -> Foo.from(zbytes) } + check(inputListFoo == outputListFoo) + + // Map of Foo. + val inputMapFoo = inputMap.map { (k, v) -> Foo(k) to Foo(v) }.toMap() + payload = ZBytes.serialize>(inputMapFoo).getOrThrow() + val outputMapFoo = payload.deserialize>().getOrThrow() + .map { (key, value) -> Foo.from(key) to Foo.from(value) }.toMap() + check(inputMapFoo == outputMapFoo) + ``` + + ##### Deserialization functions: + + As an alternative, the `deserialize` function admits an argument which by default is an emptyMap, consisting + of a `Map>` map. + + For instance, the previous implementation of our example Foo class. + We could provide directly the deserialization function as follows: + + ```kotlin + fun deserializeFoo(zbytes: ZBytes): Foo { + return Foo(zbytes.toString()) + } + + val foo = Foo("bar") + val zbytes = ZBytes.serialize(foo) + val deserialization = zbytes.deserialize(mapOf(typeOf() to ::deserializeFoo)).getOrThrow() + ``` + +## Reply handling + +Previously on 0.11.0 we were exposing the following classes: + +- `Reply.Success` +- `Reply.Error` + +In 0.11.0 executing a `get` on a session was done in the following way. + +```kotlin +session.get(selector).res() + .onSuccess { receiver -> + runBlocking { + for (reply in receiver!!) { + when (reply) { + is Reply.Success -> {println("Received ('${reply.sample.keyExpr}': '${reply.sample.value}')")} + is Reply.Error -> println("Received (ERROR: '${reply.error}')") + } + } + } + } +``` + +Now, in 1.0.0, the `Reply` instances contain a `result` attribute, which is of type `Result`. + +```kotlin +session.get(selector, channel = Channel()) + .onSuccess { channelReceiver -> + runBlocking { + for (reply in channelReceiver) { + reply.result.onSuccess { sample -> + when (sample.kind) { + SampleKind.PUT -> println("Received ('${sample.keyExpr}': '${sample.payload}')") + SampleKind.DELETE -> println("Received (DELETE '${sample.keyExpr}')") + } + }.onFailure { error -> + println("Received (ERROR: '${error.message}')") + } + } + } + } +``` diff --git a/content/docs/migration_1.0/Python.md b/content/docs/migration_1.0/Python.md new file mode 100644 index 00000000..dacdc905 --- /dev/null +++ b/content/docs/migration_1.0/Python.md @@ -0,0 +1,117 @@ +--- +title: "Python" +weight : 6500 +menu: + docs: + parent: migration_1.0 +--- + +## Highlights + +The library has been fully rewritten to use only Rust. It should make no difference for users, except for a significant performance improvement. + +The API has also been reworked to feel more pythonic, using notably context managers. + +## Context managers and background execution + +You *should* close the zenoh session after use and the recommended way is through context manager: + +```python +import zenoh +with zenoh.open(zenoh.Config()) as session: + # `session.close()` will be called at the end of the block +``` + +Session-managed objects like subscribers or queryables can also be managed using context managers: + +```python +with session.declare_subscriber("my/keyexpr") as subscriber: + # `subscriber.undeclare()` will be called at the end of the block` +``` + +However, these objects can also be used without a context manager, and without calling `undeclare`. In that case, they will run in “background” mode, meaning that their lifetime will be bound to the session’s. + +```python +import zenoh +with zenoh.open(zenoh.Config()) as session: + subscriber = session.declare_subscriber("my/keyepxr") + for sample in subscriber: + ... + # `session.close()` will be called at the end of the block, and it will undeclare the subscriber +``` + +*In previous versions, it was necessary to keep a variable in the scope for declared subscribers/queryables/etc. This restriction no longer exists, as objects not bound to a variable will still run in "background" mode, until the session is closed.* + +## ZBytes, encoding, and (de)serialization + +### Encoding + +`zenoh.Value` has been split into `zenoh.ZBytes` and `zenoh.Encoding`. Put and other operations now require a `ZBytes` payload, and accept an optional `Encoding`; the encoding is no longer automatically deduced from the payload type. + +```python +session.put("my/keyexpr", 42) # default encoding `zenoh/bytes`session.put("my/keyexpr", 42, encoding=zenoh.Encoding.ZENOH_INT64) +``` + +Publishers can be declared with a default encoding, which will be used for each put operation. + +```python +import json +publisher = session.declare_publisher("my/keyepxr", encoding=zenoh.Encoding.APPLICATION_JSON) +publisher.put(json.dumps({"key", "value"})) # default encoding from publisher `application/json` +``` + +### (De)serialization + +Arbitrary types can be serialized to and deserialized from `ZBytes`. Default (de)serializers are provided for builtin types; `list`/`dict` are **no longer** serialized to JSON, they use instead the builtin serializer of Zenoh, which is compatible with other Zenoh bindings. + +```python +payload = zenoh.ZBytes(42) +assert payload.deserialize(int) == 42# `ZBytes.deserialize` accepts generic `list`/`dict` typepayload = zenoh.ZBytes([0.5, 37.1]) +assert payload.deserialize(list[float]) == [0.5, 37.1] +``` + +(De)serializers can be registered for custom types: + +```python +from dataclasses import dataclass +import zenoh +@dataclassclass RGB: + red: int green: int blue: int@zenoh.serializerdef serialize_rgb(rgb: RGB) -> zenoh.ZBytes: + return zenoh.ZBytes(rgb.red | (rgb.green << 8) | (rgb.blue << 16)) +@zenoh.deserializerdef deserialize_rgb(payload: zenoh.ZBytes) -> RGB: + compact = payload.deserialize(int) + return RGB(compact & 255, (compact >> 8) & 255, (compact >> 16) & 255) +color = RGB(61, 67, 97) +assert zenoh.ZBytes(color).deserialize(RGB) == color +# types with a registered serializer can be used directly with `put`session.put("my/keyexpr", color) +``` + +## Handlers + +The library now directly exposes Rust-backed handlers in `zenoh.handlers`. When no handler is provided, `zenoh.handlers.DefaultHandler` is used. + +```python +import zenoh.handlers +subscriber = session.declare_subscriber("my/keyexpr", zenoh.handlers.DefaultHandler()) +# equivalent to `session.declare_subscriber("my/keyexpr")`# builtin handlers provides `try_recv`/`recv` methods and can be iterated sample_or_none = subscriber.handler.try_recv() +sample = subscriber.handler.recv() +for sample in subscriber.handler: + ... +# builtin handlers methods can be accessed directly through subscriber/queryable object sample_or_none = subscriber.try_recv() +sample = subscriber.recv() +for sample in subscriber: + ... +``` + +Callbacks can also be used as handler: + +```python +def handle_sample(sample: zenoh.Sample): + ... +session.declare_subscriber("my/keyexpr", handle_sample) +# A drop callback can be passed using `zenoh.handlers.Callback`def stop(): + ... +session.declare_subscriber("my/keyexpr", zenoh.handlers.Callback(handle_sample, stop)) +``` + +Note that for each callback handler, zenoh will in fact use a builtin handler and spawn a thread iterating the handler and calling the callback. This is needed to avoid GIL-related issues in low-level parts of zenoh, and as a result, leads to a significant performance improvement. \ No newline at end of file diff --git a/content/docs/migration_1.0/Rust.md b/content/docs/migration_1.0/Rust.md new file mode 100644 index 00000000..33974f6a --- /dev/null +++ b/content/docs/migration_1.0/Rust.md @@ -0,0 +1,374 @@ +--- +title: "Rust" +weight : 6200 +menu: + docs: + parent: migration_1.0 +--- + + +## Module reorganization + +We reorganized the module tree, so import paths are not the same as before. The main difference is that everything should be imported via the root path `zenoh::`. Here are some examples, but you can look into `zenoh/src/lib.rs` for the complete list of changes. + +```rust +// common use +use zenoh::config::*; +use zenoh::{Config, Error, Result}; + +// key_expr & selector +use zenoh::key_expr::{ + format::{kedefine, keformat}, + keyexpr, KeyExpr, OwnedKeyExpr, +}; + +// session +use zenoh::session::{init, open, EntityId, Session, SessionInfo}; + +// publisher & subscriber +use zenoh::pubsub::{Publisher, Reliability, Subscriber}; + +// query & queryable & selectors +use zenoh::query::{ + ConsolidationMode, Parameters, Query, QueryConsolidation, QueryTarget, Queryable, Reply, + Selector, +}; + +// ZBytes & encoding +use zenoh::bytes::{ZBytes, Encoding}; + +// sample +use zenoh::sample::{Locality, Sample}; + +// quality of service +use zenoh::qos::{CongestionControl, Priority, QoSBuilderTrait}; +``` + +## The changes to sync and async + +In the previous version of Zenoh, we needed to use different module paths for the synchronous and asynchronous API. + +Now both API are exposed behind `use zenoh::prelude::*`: + +- Zenoh 0.11.x + +```rust +// async +use zenoh::prelude::r#async::*; +// sync +use zenoh::prelude::sync::*; +``` + +- Zenoh 1.0.0 + +```rust +use zenoh::prelude::*; +``` + +Another big difference is that we have removed the need for `res()` in our asynchronous API. +More inline with Rust naming, `wait()` is used for synchronous API, +and `await` is used for the Asynchronous API. +To make the migration easier, there is a deprecation prompt if you use the old API convention. + +- Zenoh 0.11.x + +```rust +// (deprecated) async +let session = zenoh::open(config).res().await.unwrap(); +let publisher = session.declare_publisher(&key_expr).res().await.unwrap(); +put.res().await.unwrap(); +// (deprecated) sync +let session = zenoh::open(config).res().unwrap(); +let publisher = session.declare_publisher(&key_expr).res().unwrap(); +put.res().unwrap(); +``` + +- Zenoh 1.0.0 + +```rust +// async +// Difference 1: No more res() +let session = zenoh::open(config).await.unwrap(); +let publisher = session.declare_publisher(&key_expr).await.unwrap(); +publisher.put(buf).await.unwrap(); +// sync +// Difference 2: use wait() for synchronous API +let session = zenoh::open(config).wait().unwrap(); +let publisher = session.declare_publisher(&key_expr).wait().unwrap(); +publisher.put(buf).wait().unwrap(); +``` + +## Value is gone, long live ZBytes + +We have replaced `Value` with `ZBytes` and `Encoding` , and added a number of conversion implementations such that user structs can be serialized into `ZBytes`, sent via Zenoh, and de-serialized from `ZBytes` with ease. + +This is facilitated through the `ZSerde` struct, and implementations of the traits +`zenoh::bytes::Deserialize` and `zenoh::bytes::Serialize`. + +We provide implementations of Zenoh’s aforementioned `Deserialize` and `Serialize` traits for primitive Rust types, Rust’s `Vec`, the `Value` type exposed by `Serde`'s various libraries as well as an example of `Protobuf` ’s `prost::Message` type. + +You can look at a full set of examples in `examples/examples/z_bytes.rs`. + +NOTE: ⚠️ `ZSerde` is not the only serializer/deserializer users can make use of, nor a limitation to the types supported by Zenoh. Users are free to use whichever serializer/deserializer they wish! +- Zenoh 0.11.x + +```rust +let sample = subscriber.recv_async().await.unwrap(); +let value: Value = sample.value; +let the_string: String = value.try_into().unwrap(); +``` + +- Zenoh 1.0.0 + +```rust +let sample = subscriber.recv_async().await.unwrap(); +let zbytes: ZBytes = sample.payload(); +let the_string: String = zbytes.deserialize::().unwrap(); +``` + +## Encoding + +`Encoding` has been reworked. +Zenoh does not impose any encoding requirement on the user, nor does it operate on it. +It can be thought of as optional metadata, carried over by Zenoh in such a way that the end user’s application may perform different operations based on encoding. +We have expanded our list of pre-defined encoding types from Zenoh 0.11.0 for user convenience. +The module path and name of the encoding have also changed. + +- Zenoh 0.11.x + +```rust +use zenoh::prelude::KnownEncoding; + +session + .put(&key_expr, payload) + .encoding(KnownEncoding::AppOctetStream) + .res() + .await + .unwrap(); +``` + +- Zenoh 1.0.0 + +```rust +use zenoh::encoding::Encoding; +session + .put(&key_expr, payload) + .encoding(Encoding::APPLICATION_OCTET_STREAM) + .await + .unwrap(); +``` + +Users can also define their own encoding scheme that does not need to be based on the pre-defined variants. + +```rust +let encoding = Encoding::from("pointcloud/LAS"); +``` + +## Attachment + +In Zenoh 0.11.x, the `AttachmentBuilder` was required to create an attachment. +In Zenoh 1.0.0, we have removed `AttachmentBuilder`, and an attachment can be created from anything that implements `Into` + +- Zenoh 0.11.x + +```rust +let mut attachment = AttachmentBuilder::new(); +attachment.insert("key1", "value1"); +attachment.insert("key2", "value2"); +publisher.put(payload) + .with_attachment(attachment.build()) + .res() + .await + .unwrap(); +``` + +- Zenoh 1.0.0 + +```rust +// Difference 1: No AttachmentBuilder anymore +// Accept any type which can be transformed into ZBytes +let mut hashmap = HashMap::new(); +hashmap.insert(String::from("key1"), String::from("value1")); +hashmap.insert(String::from("key2"), String::from("value2")); +let the_attachment = ZBytes::from(&hashmap); +// Difference 2: no with_attachment() +publisher + .put(payload) + .attachment(the_attachment) + .await + .unwrap(); +``` + +## API changes in Query & Queryable + +Query and Queryable have been slightly reworked. + +For the API replying to a `Query` from a `Queryable` declared on a session: +The reply function has been split into 3 separate functions variants depending on the type of reply the user wants to send. + +- Zenoh 0.11.x + +```rust +let reply_ok = Ok(Sample::new(key_expr.clone(), payload.clone())); // Success +query.reply(reply_ok).res().await.unwrap(); +// or +let reply_err = Err(Value::from(payload.clone())); // Failure +query.reply(reply_err).res().await.unwrap(); +``` + +- Zenoh 1.0.0 + +```rust +// No need to send Result +// For sending Succesful Reply to Query +query.reply(key_expr.clone(), payload.clone()).await.unwrap(); // Success +// For sending Error Reply to Query +query.reply_err(payload.clone()).await.unwrap(); // Failure +// For sending Delete reply to Query (Sample Kind = Delete) +query.reply_del(payload.clone()).await.unwrap(); // Delete (Success) +``` + +For how a Get `Query` receives the reply: +use `result()` on the `Reply` to get the `&Sample`, +or `into_result` to take the ownership of the `Sample`. + + `Ok` variant replies, will return `Sample`. + `Err` variant replies, will return `ReplyError` + +- Zenoh 0.11.x + +```rust +while let Ok(reply) = replies.recv_async().await { + match reply.sample { // sample should be Result + Ok(sample) => println!( + ">> Received ('{}': '{}')", + sample.key_expr.as_str(), + sample.value, + ), + Err(value_err) => println!("{}", String::try_from(&value_err).unwrap()), + } +} +``` + +- Zenoh 1.0.0 + +```rust +while let Ok(reply) = replies.recv_async().await { + // Difference 1: using result() to get Result<&Sample, &ReplyError> + match reply.result() { + Ok(sample) => { + println!( + ">> Received ('{}': '{}')", + sample.key_expr().as_str(), + // Difference 2: payload() instead of value + sample.payload().deserialize::().unwrap() + ); + } + // Difference 3: ReplyError instead of Value + Err(err) => { + println!("{}", err.payload().deserialize::().unwrap()); + } + } +} +``` + +We have also added the ability to get underlying Handlers from `Queryables`, so that users have direct acces to the receiver of the data channel. + +```rust +let queryable = session + .declare_queryable(&key_expr) + .await + .unwrap(); + +let handler: &Receiver = queryable.handler(); +// or mutable handler +let mut_handler:&mut Receiver = queryable.handler_mut(); +``` + +## Use accessors to get private members + +We encapsulate members of structs, and they can’t be accessed directly now. +The only way to access Struct values is to use the getter function associated with them. +Let’s take the subscriber as an example here. + +- Zenoh 0.11.x + +```rust +while let Ok(sample) = subscriber.recv_async().await { + println!( + ">> [Subscriber] Received {} ('{}': '{}')", + sample.kind, + sample.key_expr.as_str(), + sample.value + ); +} +``` + +- Zenoh 1.0.0 + +```rust +while let Ok(sample) = subscriber.recv_async().await { + println!( + ">> [Subscriber] Received {} ('{}': '{:?}')", + sample.kind(), + sample.key_expr().as_str(), + sample.payload() // Ignore the deserialization + ); +} +``` + +## Support RingChannel to receive data + +Besides using a callback to receive data, we can also receive the data from a default FIFO channel. However, sometimes we only care about the latest data and want to discard the oldest data. +We can use `RingChannel` to get this behaviour. +You can take a look at the complete code in `examples/examples/z_pull.rs`. + +```rust +let subscriber = session + .declare_subscriber(&key_expr) + .with(RingChannel::new(size)) + .await + .unwrap(); +``` + +⚠️ Note: We **no longer** support **Pull** mode in Zenoh + +To get the same behavior of a Zenoh 0.11.0 `PullSubscriber`, please make use of a `RingChannel` an example of this is illustrated in `z_pull.rs`. + +## Timestamps + +We now tie generating a timestamp to a Zenoh session, with the timestamp inheriting the `ZenohID` of the session. + +Note that a Zenoh session will only be able to generate a timestamp if the `timestamping` configuration option is enabled. + +- Zenoh 0.11.x + +```rust +let timestamp : Timestamp = zenoh::time::new_reception_timestamp(); +``` + +- Zenoh 1.0.0 + +```rust +let session: Session = zenoh::open(); +// If the `timestamping` configuration is disabled, this call will return `None`. +let timestamp: Option = session::new_timestamp(); +``` + +This will affect user-created plugins and applications that need to generate timestamps. + +## Feature Flags + +Removed: + +- `complete_n`: due to a Legacy code cleanup + +## Storages + +Zenoh 1.0.0 introduced the possibility for Zenoh nodes configured in a mode other than `router` to load plugins. + +A, somehow, implicit assumption that dictated the behaviour of storages is that the Zenoh node loading them **has to add a timestamp to any received publication that did not have one**. This functionality is controlled by the `timestamping` configuration option. + +Until Zenoh 1.0.0 this assumption held true as only a router could load storage and the default configuration for a router enables `timestamping`. However, in Zenoh 1.0.0 nodes configured in `client` & `peer` mode can load storage and *their default configuration disables `timestamping`*. + +⚠️ The `storage-manager` will fail to launch if the `timestamping` configuration option is disabled. \ No newline at end of file