Skip to content

Commit e470720

Browse files
committed
st: byte-buffer SerDe seam + zero-copy BytesView
Per PR review on the Schema encode/decode signatures. SerDe seam (Schema<T> + JSON/Avro/protobuf factories): - encode writes into a caller-provided, reusable std::vector<std::byte>& (no per-message allocation) and returns Expected<void>; - decode takes std::span<const std::byte> and returns Expected<T>, so malformed input / an unset schema are error values rather than throws; - Bytes is now std::vector<std::byte>. Client-facing API unchanged: Message<T>::value() still returns T (decode failures are handled inside the SDK), Producer::send(const T&) and the examples are as before; a rare encode error is stashed in the builder and surfaces from send()/sendAsync(). Zero-copy bytes: new BytesView = std::span<const std::byte>. Schema<BytesView> is the zero-copy counterpart of Schema<Bytes> -- Producer<BytesView> publishes the caller's bytes without copying (the caller keeps them valid until the send completes) and Message<BytesView>::value() returns a view into the message buffer. OutgoingMessage carries an optional non-owning view. Verified with clang + gcc:13 (-Wextra -Werror), clang-format-11, and a runtime check that decode returns a view at the same address. Signed-off-by: Matteo Merli <mmerli@apache.org>
1 parent 967a09d commit e470720

6 files changed

Lines changed: 214 additions & 88 deletions

File tree

include/pulsar/st/AvroSchema.h

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
#include <pulsar/st/Schema.h>
2222

2323
#include <cstddef>
24+
#include <exception>
2425
#include <rfl.hpp>
2526
#include <rfl/avro.hpp>
27+
#include <span>
2628
#include <string>
29+
#include <vector>
2730

2831
// avroSchema<T>() is the Avro counterpart of jsonSchema<T>(): reflect-cpp derives
2932
// the SerDe and the Avro schema from T's fields — no per-type serializer. The
@@ -43,9 +46,19 @@ namespace detail {
4346
template <typename T>
4447
struct AvroSerDe {
4548
SchemaInfo info() const { return SchemaInfo(SchemaType::AVRO, "AVRO", rfl::avro::to_schema<T>()); }
46-
std::string encode(const T& value) const { return rfl::avro::write(value); }
47-
T decode(const char* data, std::size_t size) const {
48-
return rfl::avro::read<T>(std::string(data, size)).value();
49+
Expected<void> encode(const T& value, std::vector<std::byte>& out) const {
50+
const std::string s = rfl::avro::write(value);
51+
const auto* p = reinterpret_cast<const std::byte*>(s.data());
52+
out.assign(p, p + s.size());
53+
return {};
54+
}
55+
Expected<T> decode(std::span<const std::byte> data) const {
56+
try {
57+
return rfl::avro::read<T>(std::string(reinterpret_cast<const char*>(data.data()), data.size()))
58+
.value();
59+
} catch (const std::exception& e) {
60+
return unexpected(pulsar::ResultInvalidMessage, e.what());
61+
}
4962
}
5063
};
5164
} // namespace detail
@@ -63,9 +76,8 @@ struct AvroSerDe {
6376
*
6477
* @tparam T the struct type to serialize as Avro; its fields must be reflectable
6578
* by reflect-cpp.
66-
* @return a `Schema<T>` whose `encode`/`decode` use Avro.
67-
* @throws std::runtime_error (from reflect-cpp) at decode time if the input bytes
68-
* are not a valid Avro encoding for `T`.
79+
* @return a `Schema<T>` whose `encode`/`decode` use Avro. `decode` reports input
80+
* that is not a valid Avro encoding for `T` as an `Error` rather than throwing.
6981
*/
7082
template <typename T>
7183
Schema<T> avroSchema() {

include/pulsar/st/JsonSchema.h

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
#include <pulsar/st/Schema.h>
2222

2323
#include <cstddef>
24+
#include <exception>
2425
#include <rfl.hpp>
2526
#include <rfl/json.hpp>
27+
#include <span>
2628
#include <string>
29+
#include <vector>
2730

2831
// jsonSchema<T>() derives BOTH the JSON SerDe and the declared schema from T's
2932
// fields via reflect-cpp (https://github.com/getml/reflect-cpp) — no per-type
@@ -40,9 +43,19 @@ namespace detail {
4043
template <typename T>
4144
struct JsonSerDe {
4245
SchemaInfo info() const { return SchemaInfo(SchemaType::JSON, "JSON", rfl::json::to_schema<T>()); }
43-
std::string encode(const T& value) const { return rfl::json::write(value); }
44-
T decode(const char* data, std::size_t size) const {
45-
return rfl::json::read<T>(std::string(data, size)).value();
46+
Expected<void> encode(const T& value, std::vector<std::byte>& out) const {
47+
const std::string s = rfl::json::write(value);
48+
const auto* p = reinterpret_cast<const std::byte*>(s.data());
49+
out.assign(p, p + s.size());
50+
return {};
51+
}
52+
Expected<T> decode(std::span<const std::byte> data) const {
53+
try {
54+
return rfl::json::read<T>(std::string(reinterpret_cast<const char*>(data.data()), data.size()))
55+
.value();
56+
} catch (const std::exception& e) {
57+
return unexpected(pulsar::ResultInvalidMessage, e.what());
58+
}
4659
}
4760
};
4861
} // namespace detail
@@ -63,9 +76,8 @@ struct JsonSerDe {
6376
*
6477
* @tparam T the struct type to serialize as JSON; its fields must be reflectable
6578
* by reflect-cpp.
66-
* @return a `Schema<T>` whose `encode`/`decode` use JSON.
67-
* @throws std::runtime_error (from reflect-cpp) at decode time if the input bytes
68-
* are not valid JSON for `T`.
79+
* @return a `Schema<T>` whose `encode`/`decode` use JSON. `decode` reports input
80+
* that is not valid JSON for `T` as an `Error` rather than throwing.
6981
*/
7082
template <typename T>
7183
Schema<T> jsonSchema() {

include/pulsar/st/Message.h

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <cstddef>
2828
#include <cstdint>
2929
#include <optional>
30+
#include <span>
3031
#include <string>
3132
#include <vector>
3233

@@ -61,12 +62,17 @@ class Message {
6162
/**
6263
* Decode the payload through `Schema<T>` and return the typed value.
6364
*
64-
* Decoding happens on every call (the result is not cached). May throw if the
65-
* payload bytes are malformed for the schema.
65+
* Decoding happens on every call (the result is not cached). The SDK handles a
66+
* payload that cannot be decoded internally — such a message is not delivered to
67+
* the application — so this does not surface decode failures to the caller. The
68+
* raw bytes remain available via `data()` / `size()`.
6669
*
6770
* @return the decoded value of type `T`.
6871
*/
69-
T value() const { return schema_.decode(core_.data(), core_.size()); }
72+
T value() const {
73+
const auto* bytes = reinterpret_cast<const std::byte*>(core_.data());
74+
return schema_.decode(std::span<const std::byte>(bytes, core_.size())).value();
75+
}
7076

7177
/**
7278
* Pointer to the raw, undecoded payload bytes.

include/pulsar/st/Producer.h

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@
3030
#include <pulsar/st/detail/ProducerCore.h>
3131

3232
#include <chrono>
33+
#include <cstddef>
3334
#include <cstdint>
3435
#include <memory>
3536
#include <optional>
37+
#include <span>
3638
#include <string>
39+
#include <type_traits>
3740
#include <vector>
3841

3942
namespace pulsar::st {
@@ -99,8 +102,14 @@ struct ProducerConfig {
99102
* its fluent setters and hands the result to the producer core for publishing.
100103
*/
101104
struct OutgoingMessage {
102-
/** Encoded message payload (the value serialized through `Schema<T>`). */
103-
std::string payload;
105+
/** Encoded message payload — the value serialized to bytes through `Schema<T>`.
106+
* Published unless `usesView` is set. */
107+
std::vector<std::byte> payload;
108+
/** Non-owning view of already-encoded bytes for zero-copy publishing
109+
* (`Schema<BytesView>`); the caller keeps them valid until the send completes. */
110+
std::span<const std::byte> payloadView;
111+
/** When true, publish `payloadView` directly without copying; otherwise `payload`. */
112+
bool usesView = false;
104113
/** Whether a routing/ordering key is set. `false` (the default) means no key. */
105114
bool hasKey = false;
106115
/** Partition/ordering key; meaningful only when `hasKey` is true. */
@@ -145,11 +154,22 @@ class MessageBuilder {
145154
* Set the message value, encoding it to bytes through this producer's
146155
* `Schema<T>`.
147156
*
157+
* For a zero-copy `Schema<BytesView>` producer the bytes are not copied — the
158+
* view is published directly, so the caller must keep them valid until the send
159+
* completes. A rare encoding failure (e.g. an unset schema) is not reported here,
160+
* so the fluent chain stays unbroken; it surfaces from the terminal `send()` /
161+
* `sendAsync()` instead.
162+
*
148163
* @param v the typed value to publish.
149164
* @return `*this`, for chaining.
150165
*/
151166
MessageBuilder& value(const T& v) {
152-
message_.payload = schema_.encode(v);
167+
if constexpr (std::is_same_v<T, BytesView>) {
168+
message_.payloadView = v;
169+
message_.usesView = true;
170+
} else {
171+
if (auto r = schema_.encode(v, message_.payload); !r) encodeError_ = r.error();
172+
}
153173
return *this;
154174
}
155175
/**
@@ -246,7 +266,14 @@ class MessageBuilder {
246266
* @return a `Future<MessageId>` that completes with the assigned id on success
247267
* or the failure. The future may be ignored for fire-and-forget sends.
248268
*/
249-
Future<MessageId> sendAsync() { return core_.sendAsync(std::move(message_)); }
269+
Future<MessageId> sendAsync() {
270+
if (encodeError_) {
271+
detail::Promise<MessageId> promise;
272+
promise.setError(*encodeError_);
273+
return promise.getFuture();
274+
}
275+
return core_.sendAsync(std::move(message_));
276+
}
250277

251278
private:
252279
friend class Producer<T>;
@@ -260,6 +287,7 @@ class MessageBuilder {
260287
detail::ProducerCore core_;
261288
Schema<T> schema_;
262289
OutgoingMessage message_;
290+
std::optional<Error> encodeError_;
263291
};
264292

265293
/**

include/pulsar/st/ProtobufNativeSchema.h

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
#include <pulsar/st/Schema.h>
2525

2626
#include <cstddef>
27+
#include <span>
2728
#include <string>
2829
#include <type_traits>
30+
#include <vector>
2931

3032
namespace pulsar::st {
3133

@@ -38,11 +40,16 @@ struct ProtobufNativeSerDe {
3840
static_assert(std::is_base_of_v<google::protobuf::Message, T>,
3941
"protobufNativeSchema<T> requires T to be a generated protobuf Message");
4042
SchemaInfo info() const { return pulsar::createProtobufNativeSchema(T::descriptor()); }
41-
std::string encode(const T& value) const { return value.SerializeAsString(); }
42-
T decode(const char* data, std::size_t size) const {
43+
Expected<void> encode(const T& value, std::vector<std::byte>& out) const {
44+
out.resize(value.ByteSizeLong());
45+
if (!value.SerializeToArray(out.data(), static_cast<int>(out.size())))
46+
return unexpected(pulsar::ResultInvalidMessage, "failed to serialize protobuf message");
47+
return {};
48+
}
49+
Expected<T> decode(std::span<const std::byte> data) const {
4350
T message;
44-
message.ParseFromArray(data, static_cast<int>(size));
45-
return message;
51+
if (message.ParseFromArray(data.data(), static_cast<int>(data.size()))) return message;
52+
return unexpected(pulsar::ResultInvalidMessage, "failed to parse protobuf message");
4653
}
4754
};
4855
} // namespace detail

0 commit comments

Comments
 (0)