This is an automated email from the ASF dual-hosted git repository.
paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-nanoarrow.git
The following commit(s) were added to refs/heads/main by this push:
new 3e02822 refactor: Improve syntax for implementing `ArrowArrayStream`
from C++ (#336)
3e02822 is described below
commit 3e02822e232c10abe97f126165c4891f02706b4a
Author: Dewey Dunnington <[email protected]>
AuthorDate: Tue Dec 19 12:40:07 2023 -0400
refactor: Improve syntax for implementing `ArrowArrayStream` from C++ (#336)
An early commit implemented `nanoarrow::EmptyArrayStream` and
`nanoarrow::VectorArrayStream` in the nanoarrow.hpp C++ helpers. The
intention at the time was to make it "easy"/idiomatic to use a C++
object to back an `ArrowArrayStream`; however, it was in practice
difficult to actually make work (I tried briefly and gave up when
writing a [dummy ADBC
driver](https://github.com/voltrondata-labs/blog-posts-code/blob/main/2023-08-nanoarrow-adbc/simple_csv_reader.cc#L52-L177)
for [this blog
post](https://voltrondata.com/resources/nanoarrow-lightweight-embeddable-arrow-implementation-data-pipelines).
This PR deprecates the original syntax and migrates it to the following.
Implementation:
```cpp
class StreamImpl {
public:
// Public methods (e.g., constructor) used from C++ to initialize
relevant data
// Idiomatic exporter to move data + lifecycle responsibility to an
instance
// managed by the ArrowArrayStream callbacks
void ToArrayStream(struct ArrowArrayStream* out) {
ArrayStreamFactory<StreamImpl>::InitArrayStream(new StreamImpl(...),
out);
}
private:
// Make relevant methods available to the ArrayStreamFactory
friend class ArrayStreamFactory<StreamImpl>;
// Method implementations (called from C, not normally interacted with
from C++)
int GetSchema(struct ArrowSchema* schema) { return ENOTSUP; }
int GetNext(struct ArrowArray* array) { return ENOTSUP; }
const char* GetLastError() { nullptr; }
};
```
Usage:
```cpp
// Call constructor and/or public methods to initialize relevant data
StreamImpl impl;
// Export to ArrowArrayStream after data are finalized
UniqueArrayStream stream;
impl.ToArrayStream(stream.get());
```
I'm open to suggestions on how to make that better! It might be that the
`ToArrayStream()` bit is confusing (i.e., just use
`ArrayStreamFactory<>::InitArrayStream(new StreamImpl())` directory) but
it also seemed better to keep the lines where a raw pointer was floating
around to be entirely contained within the `StreamImpl` class.
It also fixes an issue with the `XXX_pointer()` functions, which because
of the way they were declared, required that `nanoarrow.hpp` be
confusingly included *after* `nanoarrow_ipc.hpp`. The correct way to do
this (I think) was to declare the template and add implementations
(rather than use overloads).
---
.../nanoarrow_ipc/src/nanoarrow/nanoarrow_ipc.hpp | 28 ++-
src/nanoarrow/nanoarrow.hpp | 278 ++++++++++++++++-----
src/nanoarrow/nanoarrow_hpp_test.cc | 25 +-
3 files changed, 242 insertions(+), 89 deletions(-)
diff --git a/extensions/nanoarrow_ipc/src/nanoarrow/nanoarrow_ipc.hpp
b/extensions/nanoarrow_ipc/src/nanoarrow/nanoarrow_ipc.hpp
index 505f827..e52fddb 100644
--- a/extensions/nanoarrow_ipc/src/nanoarrow/nanoarrow_ipc.hpp
+++ b/extensions/nanoarrow_ipc/src/nanoarrow/nanoarrow_ipc.hpp
@@ -15,40 +15,46 @@
// specific language governing permissions and limitations
// under the License.
-#include "nanoarrow_ipc.h"
-
#ifndef NANOARROW_IPC_HPP_INCLUDED
#define NANOARROW_IPC_HPP_INCLUDED
+#include "nanoarrow.hpp"
+#include "nanoarrow_ipc.h"
+
namespace nanoarrow {
namespace internal {
-static inline void init_pointer(struct ArrowIpcDecoder* data) {
+template <>
+inline void init_pointer(struct ArrowIpcDecoder* data) {
data->private_data = nullptr;
}
-static inline void move_pointer(struct ArrowIpcDecoder* src,
- struct ArrowIpcDecoder* dst) {
+template <>
+inline void move_pointer(struct ArrowIpcDecoder* src, struct ArrowIpcDecoder*
dst) {
memcpy(dst, src, sizeof(struct ArrowIpcDecoder));
src->private_data = nullptr;
}
-static inline void release_pointer(struct ArrowIpcDecoder* data) {
+template <>
+inline void release_pointer(struct ArrowIpcDecoder* data) {
ArrowIpcDecoderReset(data);
}
-static inline void init_pointer(struct ArrowIpcInputStream* data) {
+template <>
+inline void init_pointer(struct ArrowIpcInputStream* data) {
data->release = nullptr;
}
-static inline void move_pointer(struct ArrowIpcInputStream* src,
- struct ArrowIpcInputStream* dst) {
+template <>
+inline void move_pointer(struct ArrowIpcInputStream* src,
+ struct ArrowIpcInputStream* dst) {
memcpy(dst, src, sizeof(struct ArrowIpcInputStream));
src->release = nullptr;
}
-static inline void release_pointer(struct ArrowIpcInputStream* data) {
+template <>
+inline void release_pointer(struct ArrowIpcInputStream* data) {
if (data->release != nullptr) {
data->release(data);
}
@@ -57,8 +63,6 @@ static inline void release_pointer(struct
ArrowIpcInputStream* data) {
} // namespace internal
} // namespace nanoarrow
-#include "nanoarrow.hpp"
-
namespace nanoarrow {
namespace ipc {
diff --git a/src/nanoarrow/nanoarrow.hpp b/src/nanoarrow/nanoarrow.hpp
index 2638769..15914ce 100644
--- a/src/nanoarrow/nanoarrow.hpp
+++ b/src/nanoarrow/nanoarrow.hpp
@@ -88,70 +88,108 @@ namespace internal {
///
/// @{
-static inline void init_pointer(struct ArrowSchema* data) { data->release =
nullptr; }
+template <typename T>
+static inline void init_pointer(T* data);
+
+template <typename T>
+static inline void move_pointer(T* src, T* dst);
+
+template <typename T>
+static inline void release_pointer(T* data);
+
+template <>
+inline void init_pointer(struct ArrowSchema* data) {
+ data->release = nullptr;
+}
-static inline void move_pointer(struct ArrowSchema* src, struct ArrowSchema*
dst) {
+template <>
+inline void move_pointer(struct ArrowSchema* src, struct ArrowSchema* dst) {
ArrowSchemaMove(src, dst);
}
-static inline void release_pointer(struct ArrowSchema* data) {
+template <>
+inline void release_pointer(struct ArrowSchema* data) {
if (data->release != nullptr) {
data->release(data);
}
}
-static inline void init_pointer(struct ArrowArray* data) { data->release =
nullptr; }
+template <>
+inline void init_pointer(struct ArrowArray* data) {
+ data->release = nullptr;
+}
-static inline void move_pointer(struct ArrowArray* src, struct ArrowArray*
dst) {
+template <>
+inline void move_pointer(struct ArrowArray* src, struct ArrowArray* dst) {
ArrowArrayMove(src, dst);
}
-static inline void release_pointer(struct ArrowArray* data) {
+template <>
+inline void release_pointer(struct ArrowArray* data) {
if (data->release != nullptr) {
data->release(data);
}
}
-static inline void init_pointer(struct ArrowArrayStream* data) {
+template <>
+inline void init_pointer(struct ArrowArrayStream* data) {
data->release = nullptr;
}
-static inline void move_pointer(struct ArrowArrayStream* src,
- struct ArrowArrayStream* dst) {
+template <>
+inline void move_pointer(struct ArrowArrayStream* src, struct
ArrowArrayStream* dst) {
ArrowArrayStreamMove(src, dst);
}
-static inline void release_pointer(ArrowArrayStream* data) {
+template <>
+inline void release_pointer(ArrowArrayStream* data) {
if (data->release != nullptr) {
data->release(data);
}
}
-static inline void init_pointer(struct ArrowBuffer* data) {
ArrowBufferInit(data); }
+template <>
+inline void init_pointer(struct ArrowBuffer* data) {
+ ArrowBufferInit(data);
+}
-static inline void move_pointer(struct ArrowBuffer* src, struct ArrowBuffer*
dst) {
+template <>
+inline void move_pointer(struct ArrowBuffer* src, struct ArrowBuffer* dst) {
ArrowBufferMove(src, dst);
}
-static inline void release_pointer(struct ArrowBuffer* data) {
ArrowBufferReset(data); }
+template <>
+inline void release_pointer(struct ArrowBuffer* data) {
+ ArrowBufferReset(data);
+}
-static inline void init_pointer(struct ArrowBitmap* data) {
ArrowBitmapInit(data); }
+template <>
+inline void init_pointer(struct ArrowBitmap* data) {
+ ArrowBitmapInit(data);
+}
-static inline void move_pointer(struct ArrowBitmap* src, struct ArrowBitmap*
dst) {
+template <>
+inline void move_pointer(struct ArrowBitmap* src, struct ArrowBitmap* dst) {
ArrowBitmapMove(src, dst);
}
-static inline void release_pointer(struct ArrowBitmap* data) {
ArrowBitmapReset(data); }
+template <>
+inline void release_pointer(struct ArrowBitmap* data) {
+ ArrowBitmapReset(data);
+}
-static inline void init_pointer(struct ArrowArrayView* data) {
+template <>
+inline void init_pointer(struct ArrowArrayView* data) {
ArrowArrayViewInitFromType(data, NANOARROW_TYPE_UNINITIALIZED);
}
-static inline void move_pointer(struct ArrowArrayView* src, struct
ArrowArrayView* dst) {
+template <>
+inline void move_pointer(struct ArrowArrayView* src, struct ArrowArrayView*
dst) {
ArrowArrayViewMove(src, dst);
}
-static inline void release_pointer(struct ArrowArrayView* data) {
+template <>
+inline void release_pointer(struct ArrowArrayView* data) {
ArrowArrayViewReset(data);
}
@@ -231,28 +269,124 @@ using UniqueArrayView = internal::Unique<struct
ArrowArrayView>;
/// \defgroup nanoarrow_hpp-array-stream ArrayStream helpers
///
-/// These classes provide simple struct ArrowArrayStream implementations that
+/// These classes provide simple ArrowArrayStream implementations that
/// can be extended to help simplify the process of creating a valid
/// ArrowArrayStream implementation or used as-is for testing.
///
/// @{
+/// @brief Export an ArrowArrayStream from a standard C++ class
+/// @tparam T A class with methods `int GetSchema(ArrowSchema*)`, `int
+/// GetNext(ArrowArray*)`, and `const char* GetLastError()`
+///
+/// This class allows a standard C++ class to be exported to a generic
ArrowArrayStream
+/// consumer by mapping C callback invocations to method calls on an instance
of the
+/// object whose lifecycle is owned by the ArrowArrayStream. See
VectorArrayStream for
+/// minimal useful example of this pattern.
+///
+/// The methods must be accessible to the ArrayStreamFactory, either as public
methods or
+/// by declaring ArrayStreamFactory<ImplClass> a friend. Implementors are
encouraged (but
+/// not required) to implement a ToArrayStream(ArrowArrayStream*) that creates
a new
+/// instance owned by the ArrowArrayStream and moves the relevant data to that
instance.
+///
+/// An example implementation might be:
+///
+/// \code
+/// class StreamImpl {
+/// public:
+/// // Public methods (e.g., constructor) used from C++ to initialize
relevant data
+///
+/// // Idiomatic exporter to move data + lifecycle responsibility to an
instance
+/// // managed by the ArrowArrayStream callbacks
+/// void ToArrayStream(struct ArrowArrayStream* out) {
+/// ArrayStreamFactory<StreamImpl>::InitArrayStream(new StreamImpl(...),
out);
+/// }
+///
+/// private:
+/// // Make relevant methods available to the ArrayStreamFactory
+/// friend class ArrayStreamFactory<StreamImpl>;
+///
+/// // Method implementations (called from C, not normally interacted with
from C++)
+/// int GetSchema(struct ArrowSchema* schema) { return ENOTSUP; }
+/// int GetNext(struct ArrowArray* array) { return ENOTSUP; }
+/// const char* GetLastError() { nullptr; }
+/// };
+/// \endcode
+///
+/// An example usage might be:
+///
+/// \code
+/// // Call constructor and/or public methods to initialize relevant data
+/// StreamImpl impl;
+///
+/// // Export to ArrowArrayStream after data are finalized
+/// UniqueArrayStream stream;
+/// impl.ToArrayStream(stream.get());
+/// \endcode
+template <typename T>
+class ArrayStreamFactory {
+ public:
+ /// \brief Take ownership of instance and populate callbacks of out
+ static void InitArrayStream(T* instance, struct ArrowArrayStream* out) {
+ out->get_schema = &get_schema_wrapper;
+ out->get_next = &get_next_wrapper;
+ out->get_last_error = &get_last_error_wrapper;
+ out->release = &release_wrapper;
+ out->private_data = instance;
+ }
+
+ private:
+ static int get_schema_wrapper(struct ArrowArrayStream* stream,
+ struct ArrowSchema* schema) {
+ return reinterpret_cast<T*>(stream->private_data)->GetSchema(schema);
+ }
+
+ static int get_next_wrapper(struct ArrowArrayStream* stream, struct
ArrowArray* array) {
+ return reinterpret_cast<T*>(stream->private_data)->GetNext(array);
+ }
+
+ static const char* get_last_error_wrapper(struct ArrowArrayStream* stream) {
+ return reinterpret_cast<T*>(stream->private_data)->GetLastError();
+ }
+
+ static void release_wrapper(struct ArrowArrayStream* stream) {
+ delete reinterpret_cast<T*>(stream->private_data);
+ stream->release = nullptr;
+ stream->private_data = nullptr;
+ }
+};
+
/// \brief An empty array stream
///
-/// This class can be constructed from an enum ArrowType or
-/// struct ArrowSchema and implements a default get_next() method that
-/// always marks the output ArrowArray as released. This class can
-/// be extended with an implementation of get_next() for a custom
-/// source.
+/// This class can be constructed from an struct ArrowSchema and implements a
default
+/// get_next() method that always marks the output ArrowArray as released.
+///
+/// DEPRECATED (0.4.0): Early versions of nanoarrow allowed subclasses to
override
+/// get_schema(), get_next(), and get_last_error(). This functionality will be
removed
+/// in a future release: use the pattern documented in ArrayStreamFactory to
create
+/// custom ArrowArrayStream implementations.
class EmptyArrayStream {
public:
+ /// \brief Create an EmptyArrayStream from an ArrowSchema
+ ///
+ /// Takes ownership of schema.
+ EmptyArrayStream(struct ArrowSchema* schema) : schema_(schema) {
+ ArrowErrorInit(&error_);
+ }
+
+ /// \brief Export to ArrowArrayStream
+ void ToArrayStream(struct ArrowArrayStream* out) {
+ EmptyArrayStream* impl = new EmptyArrayStream(schema_.get());
+ ArrayStreamFactory<EmptyArrayStream>::InitArrayStream(impl, out);
+ }
+
/// \brief Create an empty UniqueArrayStream from a struct ArrowSchema
///
- /// This object takes ownership of the schema and marks the source schema
- /// as released.
+ /// DEPRECATED (0.4.0): Use the constructor + ToArrayStream() to export an
+ /// EmptyArrayStream to an ArrowArrayStream consumer.
static UniqueArrayStream MakeUnique(struct ArrowSchema* schema) {
UniqueArrayStream stream;
- (new EmptyArrayStream(schema))->MakeStream(stream.get());
+ EmptyArrayStream(schema).ToArrayStream(stream.get());
return stream;
}
@@ -262,17 +396,7 @@ class EmptyArrayStream {
UniqueSchema schema_;
struct ArrowError error_;
- EmptyArrayStream(struct ArrowSchema* schema) : schema_(schema) {
- error_.message[0] = '\0';
- }
-
- void MakeStream(struct ArrowArrayStream* stream) {
- stream->get_schema = &get_schema_wrapper;
- stream->get_next = &get_next_wrapper;
- stream->get_last_error = &get_last_error_wrapper;
- stream->release = &release_wrapper;
- stream->private_data = this;
- }
+ void MakeStream(struct ArrowArrayStream* stream) { ToArrayStream(stream); }
virtual int get_schema(struct ArrowSchema* schema) {
return ArrowSchemaDeepCopy(schema_.get(), schema);
@@ -286,54 +410,72 @@ class EmptyArrayStream {
virtual const char* get_last_error() { return error_.message; }
private:
- static int get_schema_wrapper(struct ArrowArrayStream* stream,
- struct ArrowSchema* schema) {
- return
reinterpret_cast<EmptyArrayStream*>(stream->private_data)->get_schema(schema);
- }
+ friend class ArrayStreamFactory<EmptyArrayStream>;
- static int get_next_wrapper(struct ArrowArrayStream* stream, struct
ArrowArray* array) {
- return
reinterpret_cast<EmptyArrayStream*>(stream->private_data)->get_next(array);
- }
+ int GetSchema(struct ArrowSchema* schema) { return get_schema(schema); }
- static const char* get_last_error_wrapper(struct ArrowArrayStream* stream) {
- return
reinterpret_cast<EmptyArrayStream*>(stream->private_data)->get_last_error();
- }
+ int GetNext(struct ArrowArray* array) { return get_next(array); }
- static void release_wrapper(struct ArrowArrayStream* stream) {
- delete reinterpret_cast<EmptyArrayStream*>(stream->private_data);
- stream->release = nullptr;
- stream->private_data = nullptr;
- }
+ const char* GetLastError() { return get_last_error(); }
};
-/// \brief Implementation of an ArrowArrayStream backed by a vector of
ArrowArray objects
-class VectorArrayStream : public EmptyArrayStream {
+/// \brief Implementation of an ArrowArrayStream backed by a vector of
UniqueArray objects
+class VectorArrayStream {
public:
+ /// \brief Create a VectorArrayStream from an ArrowSchema + vector of
UniqueArray
+ ///
+ /// Takes ownership of schema and moves arrays if possible.
+ VectorArrayStream(struct ArrowSchema* schema, std::vector<UniqueArray>
arrays)
+ : offset_(0), schema_(schema), arrays_(std::move(arrays)) {}
+
+ /// \brief Create a one-shot VectorArrayStream from an ArrowSchema +
ArrowArray
+ ///
+ /// Takes ownership of schema and array.
+ VectorArrayStream(struct ArrowSchema* schema, struct ArrowArray* array)
+ : offset_(0), schema_(schema) {
+ arrays_.emplace_back(array);
+ }
+
+ /// \brief Export to ArrowArrayStream
+ void ToArrayStream(struct ArrowArrayStream* out) {
+ VectorArrayStream* impl = new VectorArrayStream(schema_.get(),
std::move(arrays_));
+ ArrayStreamFactory<VectorArrayStream>::InitArrayStream(impl, out);
+ }
+
/// \brief Create a UniqueArrowArrayStream from an existing array
///
- /// Takes ownership of the schema and the array.
+ /// DEPRECATED (0.4.0): Use the constructors + ToArrayStream() to export a
+ /// VectorArrayStream to an ArrowArrayStream consumer.
static UniqueArrayStream MakeUnique(struct ArrowSchema* schema,
struct ArrowArray* array) {
- std::vector<UniqueArray> arrays;
- arrays.emplace_back(array);
- return MakeUnique(schema, std::move(arrays));
+ UniqueArrayStream stream;
+ VectorArrayStream(schema, array).ToArrayStream(stream.get());
+ return stream;
}
/// \brief Create a UniqueArrowArrayStream from existing arrays
///
- /// This object takes ownership of the schema and arrays.
+ /// DEPRECATED (0.4.0): Use the constructor + ToArrayStream() to export a
+ /// VectorArrayStream to an ArrowArrayStream consumer.
static UniqueArrayStream MakeUnique(struct ArrowSchema* schema,
std::vector<UniqueArray> arrays) {
UniqueArrayStream stream;
- (new VectorArrayStream(schema,
std::move(arrays)))->MakeStream(stream.get());
+ VectorArrayStream(schema, std::move(arrays)).ToArrayStream(stream.get());
return stream;
}
- protected:
- VectorArrayStream(struct ArrowSchema* schema, std::vector<UniqueArray>
arrays)
- : EmptyArrayStream(schema), arrays_(std::move(arrays)), offset_(0) {}
+ private:
+ int64_t offset_;
+ UniqueSchema schema_;
+ std::vector<UniqueArray> arrays_;
+
+ friend class ArrayStreamFactory<VectorArrayStream>;
- int get_next(struct ArrowArray* array) {
+ int GetSchema(struct ArrowSchema* schema) {
+ return ArrowSchemaDeepCopy(schema_.get(), schema);
+ }
+
+ int GetNext(struct ArrowArray* array) {
if (offset_ < static_cast<int64_t>(arrays_.size())) {
arrays_[offset_++].move(array);
} else {
@@ -343,9 +485,7 @@ class VectorArrayStream : public EmptyArrayStream {
return NANOARROW_OK;
}
- private:
- std::vector<UniqueArray> arrays_;
- int64_t offset_;
+ const char* GetLastError() { return ""; }
};
/// @}
diff --git a/src/nanoarrow/nanoarrow_hpp_test.cc
b/src/nanoarrow/nanoarrow_hpp_test.cc
index 5eef4d5..fd6733c 100644
--- a/src/nanoarrow/nanoarrow_hpp_test.cc
+++ b/src/nanoarrow/nanoarrow_hpp_test.cc
@@ -191,7 +191,9 @@ TEST(NanoarrowHppTest, NanoarrowHppEmptyArrayStreamTest) {
nanoarrow::UniqueSchema schema_in;
EXPECT_EQ(ArrowSchemaInitFromType(schema_in.get(), NANOARROW_TYPE_INT32),
NANOARROW_OK);
- auto array_stream = nanoarrow::EmptyArrayStream::MakeUnique(schema_in.get());
+
+ nanoarrow::UniqueArrayStream array_stream;
+
nanoarrow::EmptyArrayStream(schema_in.get()).ToArrayStream(array_stream.get());
EXPECT_EQ(array_stream->get_schema(array_stream.get(), schema.get()),
NANOARROW_OK);
EXPECT_STREQ(schema->format, "i");
@@ -201,10 +203,6 @@ TEST(NanoarrowHppTest, NanoarrowHppEmptyArrayStreamTest) {
}
TEST(NanoarrowHppTest, NanoarrowHppVectorArrayStreamTest) {
- nanoarrow::UniqueSchema schema;
- nanoarrow::UniqueArray array;
- nanoarrow::UniqueArrayView array_view;
-
nanoarrow::UniqueArray array_in;
EXPECT_EQ(ArrowArrayInitFromType(array_in.get(), NANOARROW_TYPE_INT32),
NANOARROW_OK);
EXPECT_EQ(ArrowArrayStartAppending(array_in.get()), NANOARROW_OK);
@@ -214,15 +212,26 @@ TEST(NanoarrowHppTest, NanoarrowHppVectorArrayStreamTest)
{
nanoarrow::UniqueSchema schema_in;
EXPECT_EQ(ArrowSchemaInitFromType(schema_in.get(), NANOARROW_TYPE_INT32),
NANOARROW_OK);
- auto array_stream =
- nanoarrow::VectorArrayStream::MakeUnique(schema_in.get(),
array_in.get());
+ nanoarrow::UniqueArrayStream array_stream;
+ nanoarrow::VectorArrayStream(schema_in.get(), array_in.get())
+ .ToArrayStream(array_stream.get());
+
+ nanoarrow::UniqueSchema schema;
+ ASSERT_EQ(array_stream->get_schema(array_stream.get(), schema.get()),
NANOARROW_OK);
+
+ nanoarrow::UniqueArrayView array_view;
+ ASSERT_EQ(ArrowArrayViewInitFromSchema(array_view.get(), schema.get(),
nullptr),
+ NANOARROW_OK);
+ nanoarrow::UniqueArray array;
EXPECT_EQ(array_stream->get_next(array_stream.get(), array.get()),
NANOARROW_OK);
- ArrowArrayViewInitFromType(array_view.get(), NANOARROW_TYPE_INT32);
+
ASSERT_EQ(ArrowArrayViewSetArray(array_view.get(), array.get(), nullptr),
NANOARROW_OK);
EXPECT_EQ(ArrowArrayViewGetIntUnsafe(array_view.get(), 0), 1234);
array.reset();
EXPECT_EQ(array_stream->get_next(array_stream.get(), array.get()),
NANOARROW_OK);
EXPECT_EQ(array->release, nullptr);
+
+ EXPECT_STREQ(array_stream->get_last_error(array_stream.get()), "");
}