All of lore.kernel.org
 help / color / mirror / Atom feed
* [libgpiod v2][PATCH v5] bindings: cxx: implement C++ bindings for libgpiod v2.0
@ 2022-03-23 14:22 Bartosz Golaszewski
  2022-03-27 12:21 ` Kent Gibson
  0 siblings, 1 reply; 6+ messages in thread
From: Bartosz Golaszewski @ 2022-03-23 14:22 UTC (permalink / raw)
  To: Kent Gibson, Linus Walleij, Andy Shevchenko
  Cc: linux-gpio, Bartosz Golaszewski

This rewrites the C++ bindings for libgpiod in order to work with v2.0
version of the C API. The C++ standard use is C++17 which is well
supported in GCC. The documentation covers the entire API so for details
please refer to it, the tests and example programs.

Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
---
v4 -> v5:

While this is technically the fifth iteration of C++ bindings I'm posting, the
change are so many, it doesn't make sense to list them here - especially since
the C API changed in the meantime too. This time the tests have been rewritten
as well so the bindings can actually be tested using gpio-sim.

 Doxyfile.in                                 |   4 +-
 bindings/cxx/Makefile.am                    |  23 +-
 bindings/cxx/chip-info.cpp                  |  74 ++
 bindings/cxx/chip.cpp                       | 213 +++--
 bindings/cxx/edge-event-buffer.cpp          | 115 +++
 bindings/cxx/edge-event.cpp                 | 135 +++
 bindings/cxx/examples/Makefile.am           |  12 +-
 bindings/cxx/examples/gpiodetectcxx.cpp     |  10 +-
 bindings/cxx/examples/gpiofindcxx.cpp       |   2 +-
 bindings/cxx/examples/gpiogetcxx.cpp        |  19 +-
 bindings/cxx/examples/gpioinfocxx.cpp       |  64 +-
 bindings/cxx/examples/gpiomoncxx.cpp        |  53 +-
 bindings/cxx/examples/gpiosetcxx.cpp        |  33 +-
 bindings/cxx/exception.cpp                  | 119 +++
 bindings/cxx/gpiod.hpp                      | 944 +-------------------
 bindings/cxx/gpiodcxx/Makefile.am           |  18 +
 bindings/cxx/gpiodcxx/chip-info.hpp         | 105 +++
 bindings/cxx/gpiodcxx/chip.hpp              | 179 ++++
 bindings/cxx/gpiodcxx/edge-event-buffer.hpp | 129 +++
 bindings/cxx/gpiodcxx/edge-event.hpp        | 137 +++
 bindings/cxx/gpiodcxx/exception.hpp         | 158 ++++
 bindings/cxx/gpiodcxx/info-event.hpp        | 123 +++
 bindings/cxx/gpiodcxx/line-config.hpp       | 564 ++++++++++++
 bindings/cxx/gpiodcxx/line-info.hpp         | 176 ++++
 bindings/cxx/gpiodcxx/line-request.hpp      | 221 +++++
 bindings/cxx/gpiodcxx/line.hpp              | 274 ++++++
 bindings/cxx/gpiodcxx/misc.hpp              |  44 +
 bindings/cxx/gpiodcxx/request-config.hpp    | 163 ++++
 bindings/cxx/gpiodcxx/timestamp.hpp         | 122 +++
 bindings/cxx/info-event.cpp                 | 102 +++
 bindings/cxx/internal.cpp                   |  28 +
 bindings/cxx/internal.hpp                   | 208 ++++-
 bindings/cxx/iter.cpp                       |  60 --
 bindings/cxx/line-config.cpp                | 685 ++++++++++++++
 bindings/cxx/line-info.cpp                  | 189 ++++
 bindings/cxx/line-request.cpp               | 224 +++++
 bindings/cxx/line.cpp                       | 331 ++-----
 bindings/cxx/line_bulk.cpp                  | 366 --------
 bindings/cxx/misc.cpp                       |  20 +
 bindings/cxx/request-config.cpp             | 174 ++++
 bindings/cxx/tests/Makefile.am              |  27 +-
 bindings/cxx/tests/check-kernel.cpp         |  48 +
 bindings/cxx/tests/gpio-mockup.cpp          | 153 ----
 bindings/cxx/tests/gpio-mockup.hpp          |  94 --
 bindings/cxx/tests/gpiod-cxx-test.cpp       |  55 --
 bindings/cxx/tests/gpiosim.cpp              | 264 ++++++
 bindings/cxx/tests/gpiosim.hpp              |  69 ++
 bindings/cxx/tests/helpers.cpp              |  37 +
 bindings/cxx/tests/helpers.hpp              |  36 +
 bindings/cxx/tests/tests-chip-info.cpp      |  91 ++
 bindings/cxx/tests/tests-chip.cpp           | 219 +++--
 bindings/cxx/tests/tests-edge-event.cpp     | 417 +++++++++
 bindings/cxx/tests/tests-event.cpp          | 280 ------
 bindings/cxx/tests/tests-info-event.cpp     | 198 ++++
 bindings/cxx/tests/tests-iter.cpp           |  21 -
 bindings/cxx/tests/tests-line-config.cpp    | 270 ++++++
 bindings/cxx/tests/tests-line-info.cpp      | 140 +++
 bindings/cxx/tests/tests-line-request.cpp   | 494 ++++++++++
 bindings/cxx/tests/tests-line.cpp           | 467 ----------
 bindings/cxx/tests/tests-misc.cpp           |  78 ++
 bindings/cxx/tests/tests-request-config.cpp | 155 ++++
 configure.ac                                |   1 +
 62 files changed, 7270 insertions(+), 2964 deletions(-)
 create mode 100644 bindings/cxx/chip-info.cpp
 create mode 100644 bindings/cxx/edge-event-buffer.cpp
 create mode 100644 bindings/cxx/edge-event.cpp
 create mode 100644 bindings/cxx/exception.cpp
 create mode 100644 bindings/cxx/gpiodcxx/Makefile.am
 create mode 100644 bindings/cxx/gpiodcxx/chip-info.hpp
 create mode 100644 bindings/cxx/gpiodcxx/chip.hpp
 create mode 100644 bindings/cxx/gpiodcxx/edge-event-buffer.hpp
 create mode 100644 bindings/cxx/gpiodcxx/edge-event.hpp
 create mode 100644 bindings/cxx/gpiodcxx/exception.hpp
 create mode 100644 bindings/cxx/gpiodcxx/info-event.hpp
 create mode 100644 bindings/cxx/gpiodcxx/line-config.hpp
 create mode 100644 bindings/cxx/gpiodcxx/line-info.hpp
 create mode 100644 bindings/cxx/gpiodcxx/line-request.hpp
 create mode 100644 bindings/cxx/gpiodcxx/line.hpp
 create mode 100644 bindings/cxx/gpiodcxx/misc.hpp
 create mode 100644 bindings/cxx/gpiodcxx/request-config.hpp
 create mode 100644 bindings/cxx/gpiodcxx/timestamp.hpp
 create mode 100644 bindings/cxx/info-event.cpp
 create mode 100644 bindings/cxx/internal.cpp
 delete mode 100644 bindings/cxx/iter.cpp
 create mode 100644 bindings/cxx/line-config.cpp
 create mode 100644 bindings/cxx/line-info.cpp
 create mode 100644 bindings/cxx/line-request.cpp
 delete mode 100644 bindings/cxx/line_bulk.cpp
 create mode 100644 bindings/cxx/misc.cpp
 create mode 100644 bindings/cxx/request-config.cpp
 create mode 100644 bindings/cxx/tests/check-kernel.cpp
 delete mode 100644 bindings/cxx/tests/gpio-mockup.cpp
 delete mode 100644 bindings/cxx/tests/gpio-mockup.hpp
 delete mode 100644 bindings/cxx/tests/gpiod-cxx-test.cpp
 create mode 100644 bindings/cxx/tests/gpiosim.cpp
 create mode 100644 bindings/cxx/tests/gpiosim.hpp
 create mode 100644 bindings/cxx/tests/helpers.cpp
 create mode 100644 bindings/cxx/tests/helpers.hpp
 create mode 100644 bindings/cxx/tests/tests-chip-info.cpp
 create mode 100644 bindings/cxx/tests/tests-edge-event.cpp
 delete mode 100644 bindings/cxx/tests/tests-event.cpp
 create mode 100644 bindings/cxx/tests/tests-info-event.cpp
 delete mode 100644 bindings/cxx/tests/tests-iter.cpp
 create mode 100644 bindings/cxx/tests/tests-line-config.cpp
 create mode 100644 bindings/cxx/tests/tests-line-info.cpp
 create mode 100644 bindings/cxx/tests/tests-line-request.cpp
 delete mode 100644 bindings/cxx/tests/tests-line.cpp
 create mode 100644 bindings/cxx/tests/tests-misc.cpp
 create mode 100644 bindings/cxx/tests/tests-request-config.cpp

diff --git a/Doxyfile.in b/Doxyfile.in
index 0ff735d..9c85e21 100644
--- a/Doxyfile.in
+++ b/Doxyfile.in
@@ -44,7 +44,9 @@ WARNINGS               = YES
 WARN_IF_UNDOCUMENTED   = YES
 WARN_FORMAT            =
 WARN_LOGFILE           =
-INPUT                  = @top_srcdir@/include/gpiod.h @top_srcdir@/bindings/cxx/gpiod.hpp
+INPUT                  = @top_srcdir@/include/gpiod.h \
+                         @top_srcdir@/bindings/cxx/gpiod.hpp \
+                         @top_srcdir@/bindings/cxx/gpiodcxx/
 SOURCE_BROWSER         = YES
 INLINE_SOURCES         = NO
 REFERENCED_BY_RELATION = YES
diff --git a/bindings/cxx/Makefile.am b/bindings/cxx/Makefile.am
index d9fa577..29d4f2d 100644
--- a/bindings/cxx/Makefile.am
+++ b/bindings/cxx/Makefile.am
@@ -2,18 +2,35 @@
 # SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
 
 lib_LTLIBRARIES = libgpiodcxx.la
-libgpiodcxx_la_SOURCES = chip.cpp internal.h iter.cpp line.cpp line_bulk.cpp
-libgpiodcxx_la_CPPFLAGS = -Wall -Wextra -g -std=gnu++11
+libgpiodcxx_la_SOURCES =	\
+	chip.cpp		\
+	chip-info.cpp		\
+	edge-event-buffer.cpp	\
+	edge-event.cpp		\
+	exception.cpp		\
+	info-event.cpp		\
+	internal.cpp		\
+	internal.hpp		\
+	line.cpp		\
+	line-config.cpp		\
+	line-info.cpp		\
+	line-request.cpp	\
+	misc.cpp		\
+	request-config.cpp
+
+libgpiodcxx_la_CPPFLAGS = -Wall -Wextra -g -std=gnu++17
 libgpiodcxx_la_CPPFLAGS += -fvisibility=hidden -I$(top_srcdir)/include/
+libgpiodcxx_la_CPPFLAGS += $(PROFILING_CFLAGS)
 libgpiodcxx_la_LDFLAGS = -version-info $(subst .,:,$(ABI_CXX_VERSION))
 libgpiodcxx_la_LDFLAGS += -lgpiod -L$(top_builddir)/lib
+libgpiodcxx_la_LDFLAGS += $(PROFILING_LDFLAGS)
 
 include_HEADERS = gpiod.hpp
 
 pkgconfigdir = $(libdir)/pkgconfig
 pkgconfig_DATA = libgpiodcxx.pc
 
-SUBDIRS = .
+SUBDIRS = gpiodcxx .
 
 if WITH_TESTS
 
diff --git a/bindings/cxx/chip-info.cpp b/bindings/cxx/chip-info.cpp
new file mode 100644
index 0000000..bd66758
--- /dev/null
+++ b/bindings/cxx/chip-info.cpp
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+void chip_info::impl::set_info_ptr(chip_info_ptr& new_info)
+{
+	this->info = ::std::move(new_info);
+}
+
+GPIOD_CXX_API chip_info::chip_info(void)
+	: _m_priv(new impl)
+{
+
+}
+
+GPIOD_CXX_API chip_info::chip_info(const chip_info& other)
+	: _m_priv(other._m_priv)
+{
+
+}
+
+GPIOD_CXX_API chip_info::chip_info(chip_info&& other) noexcept
+	: _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API chip_info::~chip_info(void)
+{
+
+}
+
+GPIOD_CXX_API chip_info& chip_info::operator=(const chip_info& other)
+{
+	this->_m_priv = other._m_priv;
+
+	return *this;
+}
+
+GPIOD_CXX_API chip_info& chip_info::operator=(chip_info&& other) noexcept
+{
+	this->_m_priv = ::std::move(other._m_priv);
+
+	return *this;
+}
+
+GPIOD_CXX_API ::std::string chip_info::name(void) const noexcept
+{
+	return ::gpiod_chip_info_get_name(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API ::std::string chip_info::label(void) const noexcept
+{
+	return ::gpiod_chip_info_get_label(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API ::std::size_t chip_info::num_lines(void) const noexcept
+{
+	return ::gpiod_chip_info_get_num_lines(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const chip_info& info)
+{
+	out << "chip_info(name=\"" << info.name() <<
+	       "\", label=\"" << info.label() <<
+	       "\", num_lines=" << info.num_lines() << ")";
+
+	return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/chip.cpp b/bindings/cxx/chip.cpp
index ee6ab6f..b87d750 100644
--- a/bindings/cxx/chip.cpp
+++ b/bindings/cxx/chip.cpp
@@ -1,11 +1,5 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <functional>
-#include <gpiod.hpp>
-#include <map>
-#include <system_error>
-#include <utility>
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
 
 #include "internal.hpp"
 
@@ -13,145 +7,212 @@ namespace gpiod {
 
 namespace {
 
-GPIOD_CXX_API void chip_deleter(::gpiod_chip* chip)
+using chip_deleter = deleter<::gpiod_chip, ::gpiod_chip_close>;
+using chip_ptr = ::std::unique_ptr<::gpiod_chip, chip_deleter>;
+
+chip_ptr open_chip(const ::std::filesystem::path& path)
 {
-	::gpiod_chip_unref(chip);
+	chip_ptr chip(::gpiod_chip_open(path.c_str()));
+	if (!chip)
+		throw_from_errno("unable to open the GPIO device " + path.string());
+
+	return chip;
 }
 
 } /* namespace */
 
-GPIOD_CXX_API bool is_gpiochip_device(const ::std::string& path)
+struct chip::impl
 {
-	return ::gpiod_is_gpiochip_device(path.c_str());
-}
+	impl(const ::std::filesystem::path& path)
+		: chip(open_chip(path))
+	{
+
+	}
+
+	impl(const impl& other) = delete;
+	impl(impl&& other) = delete;
+	impl& operator=(const impl& other) = delete;
+	impl& operator=(impl&& other) = delete;
+
+	void throw_if_closed(void) const
+	{
+		if (!this->chip)
+			throw chip_closed("GPIO chip has been closed");
+	}
 
-GPIOD_CXX_API chip::chip(const ::std::string& path)
-	: _m_chip()
+	chip_ptr chip;
+};
+
+GPIOD_CXX_API chip::chip(const ::std::filesystem::path& path)
+	: _m_priv(new impl(path))
 {
-	this->open(path);
+
 }
 
-GPIOD_CXX_API chip::chip(::gpiod_chip* chip)
-	: _m_chip(chip, chip_deleter)
+GPIOD_CXX_API chip::chip(chip&& other) noexcept
+	: _m_priv(::std::move(other._m_priv))
 {
 
 }
 
-GPIOD_CXX_API chip::chip(const ::std::weak_ptr<::gpiod_chip>& chip_ptr)
-	: _m_chip(chip_ptr)
+GPIOD_CXX_API chip::~chip(void)
 {
 
 }
 
-GPIOD_CXX_API void chip::open(const ::std::string& path)
+GPIOD_CXX_API chip& chip::operator=(chip&& other) noexcept
 {
-	::gpiod_chip *chip = ::gpiod_chip_open(path.c_str());
-	if (!chip)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "cannot open GPIO device " + path);
+	this->_m_priv = ::std::move(other._m_priv);
 
-	this->_m_chip.reset(chip, chip_deleter);
+	return *this;
 }
 
-GPIOD_CXX_API void chip::reset(void) noexcept
+GPIOD_CXX_API chip::operator bool(void) const noexcept
 {
-	this->_m_chip.reset();
+	return this->_m_priv->chip.get() != nullptr;
 }
 
-GPIOD_CXX_API ::std::string chip::name(void) const
+GPIOD_CXX_API void chip::close(void)
 {
-	this->throw_if_noref();
+	this->_m_priv->throw_if_closed();
 
-	return ::std::string(::gpiod_chip_get_name(this->_m_chip.get()));
+	this->_m_priv->chip.reset();
 }
 
-GPIOD_CXX_API ::std::string chip::label(void) const
+GPIOD_CXX_API ::std::filesystem::path chip::path(void) const
 {
-	this->throw_if_noref();
+	this->_m_priv->throw_if_closed();
 
-	return ::std::string(::gpiod_chip_get_label(this->_m_chip.get()));
+	return ::gpiod_chip_get_path(this->_m_priv->chip.get());
 }
 
-GPIOD_CXX_API unsigned int chip::num_lines(void) const
+GPIOD_CXX_API chip_info chip::get_info(void) const
 {
-	this->throw_if_noref();
+	this->_m_priv->throw_if_closed();
+
+	chip_info_ptr info(::gpiod_chip_get_info(this->_m_priv->chip.get()));
+	if (!info)
+		throw_from_errno("failed to retrieve GPIO chip info");
 
-	return ::gpiod_chip_get_num_lines(this->_m_chip.get());
+	chip_info ret;
+
+	ret._m_priv->set_info_ptr(info);
+
+	return ret;
 }
 
-GPIOD_CXX_API line chip::get_line(unsigned int offset) const
+GPIOD_CXX_API line_info chip::get_line_info(line::offset offset) const
 {
-	this->throw_if_noref();
+	this->_m_priv->throw_if_closed();
+
+	line_info_ptr info(::gpiod_chip_get_line_info(this->_m_priv->chip.get(), offset));
+	if (!info)
+		throw_from_errno("unable to retrieve GPIO line info");
 
-	if (offset >= this->num_lines())
-		throw ::std::out_of_range("line offset greater than the number of lines");
+	line_info ret;
 
-	::gpiod_line* line_handle = ::gpiod_chip_get_line(this->_m_chip.get(), offset);
-	if (!line_handle)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "error getting GPIO line from chip");
+	ret._m_priv->set_info_ptr(info);
 
-	return line(line_handle, *this);
+	return ret;
 }
 
-GPIOD_CXX_API int chip::find_line(const ::std::string& name) const
+GPIOD_CXX_API line_info chip::watch_line_info(line::offset offset) const
 {
-	this->throw_if_noref();
+	this->_m_priv->throw_if_closed();
 
-	for (unsigned int offset = 0; offset < this->num_lines(); offset++) {
-		auto line = this->get_line(offset);
+	line_info_ptr info(::gpiod_chip_watch_line_info(this->_m_priv->chip.get(), offset));
+	if (!info)
+		throw_from_errno("unable to start watching GPIO line info changes");
 
-		if (line.name() == name)
-			return offset;
-	}
+	line_info ret;
 
-	return -1;
+	ret._m_priv->set_info_ptr(info);
+
+	return ret;
 }
 
-GPIOD_CXX_API line_bulk chip::get_lines(const ::std::vector<unsigned int>& offsets) const
+GPIOD_CXX_API void chip::unwatch_line_info(line::offset offset) const
 {
-	line_bulk lines;
-
-	for (auto& it: offsets)
-		lines.append(this->get_line(it));
+	this->_m_priv->throw_if_closed();
 
-	return lines;
+	int ret = ::gpiod_chip_unwatch_line_info(this->_m_priv->chip.get(), offset);
+	if (ret)
+		throw_from_errno("unable to unwatch line status changes");
 }
 
-GPIOD_CXX_API line_bulk chip::get_all_lines(void) const
+GPIOD_CXX_API int chip::fd(void) const
 {
-	line_bulk lines;
-
-	for (unsigned int i = 0; i < this->num_lines(); i++)
-		lines.append(this->get_line(i));
+	this->_m_priv->throw_if_closed();
 
-	return lines;
+	return ::gpiod_chip_get_fd(this->_m_priv->chip.get());
 }
 
-GPIOD_CXX_API bool chip::operator==(const chip& rhs) const noexcept
+GPIOD_CXX_API bool chip::wait_info_event(const ::std::chrono::nanoseconds& timeout) const
 {
-	return this->_m_chip.get() == rhs._m_chip.get();
+	this->_m_priv->throw_if_closed();
+
+	int ret = ::gpiod_chip_wait_info_event(this->_m_priv->chip.get(), timeout.count());
+	if (ret < 0)
+		throw_from_errno("error waiting for info events");
+
+	return ret;
 }
 
-GPIOD_CXX_API bool chip::operator!=(const chip& rhs) const noexcept
+GPIOD_CXX_API info_event chip::read_info_event(void) const
 {
-	return this->_m_chip.get() != rhs._m_chip.get();
+	this->_m_priv->throw_if_closed();
+
+	info_event_ptr event(gpiod_chip_read_info_event(this->_m_priv->chip.get()));
+	if (!event)
+		throw_from_errno("error reading the line info event_handle");
+
+	info_event ret;
+	ret._m_priv->set_info_event_ptr(event);
+
+	return ret;
 }
 
-GPIOD_CXX_API chip::operator bool(void) const noexcept
+GPIOD_CXX_API int chip::find_line(const ::std::string& name) const
 {
-	return this->_m_chip.get() != nullptr;
+	this->_m_priv->throw_if_closed();
+
+	int ret = ::gpiod_chip_find_line(this->_m_priv->chip.get(), name.c_str());
+	if (ret < 0) {
+		if (errno == ENOENT)
+			return -1;
+
+		throw_from_errno("error looking up line by name");
+	}
+
+	return ret;
 }
 
-GPIOD_CXX_API bool chip::operator!(void) const noexcept
+GPIOD_CXX_API line_request chip::request_lines(const request_config& req_cfg,
+					       const line_config& line_cfg)
 {
-	return this->_m_chip.get() == nullptr;
+	this->_m_priv->throw_if_closed();
+
+	line_request_ptr request(::gpiod_chip_request_lines(this->_m_priv->chip.get(),
+							    req_cfg._m_priv->config.get(),
+							    line_cfg._m_priv->config.get()));
+	if (!request)
+		throw_from_errno("error requesting GPIO lines");
+
+	line_request ret;
+	ret._m_priv.get()->set_request_ptr(request);
+
+	return ret;
 }
 
-GPIOD_CXX_API void chip::throw_if_noref(void) const
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const chip& chip)
 {
-	if (!this->_m_chip.get())
-		throw ::std::logic_error("object not associated with an open GPIO chip");
+	if (!chip)
+		out << "chip(closed)";
+	else
+		out << "chip(path=" << chip.path() <<
+		       ", info=" << chip.get_info() << ")";
+
+	return out;
 }
 
 } /* namespace gpiod */
diff --git a/bindings/cxx/edge-event-buffer.cpp b/bindings/cxx/edge-event-buffer.cpp
new file mode 100644
index 0000000..1a90ce5
--- /dev/null
+++ b/bindings/cxx/edge-event-buffer.cpp
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <iterator>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+::gpiod_edge_event_buffer* make_edge_event_buffer(unsigned int capacity)
+{
+	::gpiod_edge_event_buffer* buffer = ::gpiod_edge_event_buffer_new(capacity);
+	if (!buffer)
+		throw_from_errno("unable to allocate the edge event buffer");
+
+	return buffer;
+}
+
+} /* namespace */
+
+edge_event_buffer::impl::impl(unsigned int capacity)
+	: buffer(make_edge_event_buffer(capacity)),
+	  events()
+{
+	events.reserve(capacity);
+
+	for (unsigned int i = 0; i < capacity; i++) {
+		events.push_back(edge_event());
+		events.back()._m_priv.reset(new edge_event::impl_external);
+	}
+}
+
+int edge_event_buffer::impl::read_events(const line_request_ptr& request, unsigned int max_events)
+{
+	int ret = ::gpiod_line_request_read_edge_event(request.get(),
+						       this->buffer.get(), max_events);
+	if (ret < 0)
+		throw_from_errno("error reading edge events from file descriptor");
+
+	for (int i = 0; i < ret; i++) {
+		::gpiod_edge_event* event = ::gpiod_edge_event_buffer_get_event(this->buffer.get(), i);
+
+		dynamic_cast<edge_event::impl_external&>(*this->events[i]._m_priv).event = event;
+	}
+
+	return ret;
+}
+
+GPIOD_CXX_API edge_event_buffer::edge_event_buffer(::std::size_t capacity)
+	: _m_priv(new impl(capacity))
+{
+
+}
+
+GPIOD_CXX_API edge_event_buffer::edge_event_buffer(edge_event_buffer&& other) noexcept
+	: _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API edge_event_buffer::~edge_event_buffer(void)
+{
+
+}
+
+GPIOD_CXX_API edge_event_buffer& edge_event_buffer::operator=(edge_event_buffer&& other) noexcept
+{
+	this->_m_priv = ::std::move(other._m_priv);
+
+	return *this;
+}
+
+GPIOD_CXX_API const edge_event& edge_event_buffer::get_event(unsigned int index) const
+{
+	return this->_m_priv->events.at(index);
+}
+
+GPIOD_CXX_API ::std::size_t edge_event_buffer::num_events(void) const
+{
+	return ::gpiod_edge_event_buffer_get_num_events(this->_m_priv->buffer.get());
+}
+
+GPIOD_CXX_API ::std::size_t edge_event_buffer::capacity(void) const noexcept
+{
+	return ::gpiod_edge_event_buffer_get_capacity(this->_m_priv->buffer.get());
+}
+
+GPIOD_CXX_API edge_event_buffer::const_iterator edge_event_buffer::begin(void) const noexcept
+{
+	return this->_m_priv->events.begin();
+}
+
+GPIOD_CXX_API edge_event_buffer::const_iterator edge_event_buffer::end(void) const noexcept
+{
+	return this->_m_priv->events.begin() + this->num_events();
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const edge_event_buffer& buf)
+{
+	out << "edge_event_buffer(num_events=" << buf.num_events() <<
+	       ", capacity=" << buf.capacity() <<
+	       ", events=[";
+
+	::std::copy(buf.begin(), ::std::prev(buf.end()),
+		    ::std::ostream_iterator<edge_event>(out, ", "));
+	out << *(::std::prev(buf.end()));
+
+	out << "])";
+
+	return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/edge-event.cpp b/bindings/cxx/edge-event.cpp
new file mode 100644
index 0000000..282f4bc
--- /dev/null
+++ b/bindings/cxx/edge-event.cpp
@@ -0,0 +1,135 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <map>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+const ::std::map<int, edge_event::event_type> event_type_mapping = {
+	{ GPIOD_EDGE_EVENT_RISING_EDGE,		edge_event::event_type::RISING_EDGE },
+	{ GPIOD_EDGE_EVENT_FALLING_EDGE,	edge_event::event_type::FALLING_EDGE }
+};
+
+const ::std::map<edge_event::event_type, ::std::string> event_type_names = {
+	{ edge_event::event_type::RISING_EDGE,		"RISING_EDGE" },
+	{ edge_event::event_type::FALLING_EDGE,		"FALLING_EDGE" }
+};
+
+} /* namespace */
+
+::gpiod_edge_event* edge_event::impl_managed::get_event_ptr(void) const noexcept
+{
+	return this->event.get();
+}
+
+::std::shared_ptr<edge_event::impl>
+edge_event::impl_managed::copy(const ::std::shared_ptr<impl>& self) const
+{
+	return self;
+}
+
+edge_event::impl_external::impl_external(void)
+	: impl(),
+	  event(nullptr)
+{
+
+}
+
+::gpiod_edge_event* edge_event::impl_external::get_event_ptr(void) const noexcept
+{
+	return this->event;
+}
+
+::std::shared_ptr<edge_event::impl>
+edge_event::impl_external::copy(const ::std::shared_ptr<impl>& self GPIOD_CXX_UNUSED) const
+{
+	::std::shared_ptr<impl> ret(new impl_managed);
+	impl_managed& managed = dynamic_cast<impl_managed&>(*ret);
+
+	managed.event.reset(::gpiod_edge_event_copy(this->event));
+	if (!managed.event)
+		throw_from_errno("unable to copy the edge event object");
+
+	return ret;
+}
+
+edge_event::edge_event(void)
+	: _m_priv()
+{
+
+}
+
+GPIOD_CXX_API edge_event::edge_event(const edge_event& other)
+	: _m_priv(other._m_priv->copy(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API edge_event::edge_event(edge_event&& other) noexcept
+	: _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API edge_event::~edge_event(void)
+{
+
+}
+
+GPIOD_CXX_API edge_event& edge_event::operator=(const edge_event& other)
+{
+	this->_m_priv = other._m_priv->copy(other._m_priv);
+
+	return *this;
+}
+
+GPIOD_CXX_API edge_event& edge_event::operator=(edge_event&& other) noexcept
+{
+	this->_m_priv = ::std::move(other._m_priv);
+
+	return *this;
+}
+
+GPIOD_CXX_API edge_event::event_type edge_event::type(void) const
+{
+	int evtype = ::gpiod_edge_event_get_event_type(this->_m_priv->get_event_ptr());
+
+	return map_int_to_enum(evtype, event_type_mapping);
+}
+
+GPIOD_CXX_API timestamp edge_event::timestamp_ns(void) const noexcept
+{
+	return ::gpiod_edge_event_get_timestamp_ns(this->_m_priv->get_event_ptr());
+}
+
+GPIOD_CXX_API line::offset edge_event::line_offset(void) const noexcept
+{
+	return ::gpiod_edge_event_get_line_offset(this->_m_priv->get_event_ptr());
+}
+
+GPIOD_CXX_API unsigned long edge_event::global_seqno(void) const noexcept
+{
+	return ::gpiod_edge_event_get_global_seqno(this->_m_priv->get_event_ptr());
+}
+
+GPIOD_CXX_API unsigned long edge_event::line_seqno(void) const noexcept
+{
+	return ::gpiod_edge_event_get_line_seqno(this->_m_priv->get_event_ptr());
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const edge_event& event)
+{
+	out << "edge_event(type='" << event_type_names.at(event.type()) <<
+	       "', timestamp=" << event.timestamp_ns() <<
+	       ", line_offset=" << event.line_offset() <<
+	       ", global_seqno=" << event.global_seqno() <<
+	       ", line_seqno=" << event.line_seqno() << ")";
+
+	return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/examples/Makefile.am b/bindings/cxx/examples/Makefile.am
index 748b581..7e29616 100644
--- a/bindings/cxx/examples/Makefile.am
+++ b/bindings/cxx/examples/Makefile.am
@@ -5,12 +5,12 @@ AM_CPPFLAGS = -I$(top_srcdir)/bindings/cxx/ -I$(top_srcdir)/include
 AM_CPPFLAGS += -Wall -Wextra -g -std=gnu++17
 AM_LDFLAGS = -lgpiodcxx -L$(top_builddir)/bindings/cxx/ -lstdc++fs
 
-noinst_PROGRAMS =				\
-		gpiodetectcxx			\
-		gpiofindcxx			\
-		gpiogetcxx			\
-		gpioinfocxx			\
-		gpiomoncxx			\
+noinst_PROGRAMS = \
+		gpiodetectcxx \
+		gpiofindcxx \
+		gpiogetcxx \
+		gpioinfocxx \
+		gpiomoncxx \
 		gpiosetcxx
 
 gpiodetectcxx_SOURCES = gpiodetectcxx.cpp
diff --git a/bindings/cxx/examples/gpiodetectcxx.cpp b/bindings/cxx/examples/gpiodetectcxx.cpp
index 872cd96..7dbb0e0 100644
--- a/bindings/cxx/examples/gpiodetectcxx.cpp
+++ b/bindings/cxx/examples/gpiodetectcxx.cpp
@@ -3,10 +3,9 @@
 
 /* C++ reimplementation of the gpiodetect tool. */
 
-#include <gpiod.hpp>
-
 #include <cstdlib>
 #include <filesystem>
+#include <gpiod.hpp>
 #include <iostream>
 
 int main(int argc, char **argv)
@@ -19,10 +18,11 @@ int main(int argc, char **argv)
 	for (const auto& entry: ::std::filesystem::directory_iterator("/dev/")) {
 		if (::gpiod::is_gpiochip_device(entry.path())) {
 			::gpiod::chip chip(entry.path());
+			auto info = chip.get_info();
 
-			::std::cout << chip.name() << " ["
-				    << chip.label() << "] ("
-				    << chip.num_lines() << " lines)" << ::std::endl;
+			::std::cout << info.name() << " [" <<
+				       info.label() << "] (" <<
+				       info.num_lines() << " lines)" << ::std::endl;
 		}
 	}
 
diff --git a/bindings/cxx/examples/gpiofindcxx.cpp b/bindings/cxx/examples/gpiofindcxx.cpp
index ec4d79b..a0f7fbf 100644
--- a/bindings/cxx/examples/gpiofindcxx.cpp
+++ b/bindings/cxx/examples/gpiofindcxx.cpp
@@ -22,7 +22,7 @@ int main(int argc, char **argv)
 
 			auto offset = chip.find_line(argv[1]);
 			if (offset >= 0) {
-				::std::cout << chip.name() << " " << offset << ::std::endl;
+				::std::cout << chip.get_info().name() << " " << offset << ::std::endl;
 				return EXIT_SUCCESS;
 			}
 		}
diff --git a/bindings/cxx/examples/gpiogetcxx.cpp b/bindings/cxx/examples/gpiogetcxx.cpp
index 94b3dac..0136f5f 100644
--- a/bindings/cxx/examples/gpiogetcxx.cpp
+++ b/bindings/cxx/examples/gpiogetcxx.cpp
@@ -15,24 +15,23 @@ int main(int argc, char **argv)
 		return EXIT_FAILURE;
 	}
 
-	::std::vector<unsigned int> offsets;
+	::gpiod::line::offsets offsets;
 
 	for (int i = 2; i < argc; i++)
 		offsets.push_back(::std::stoul(argv[i]));
 
 	::gpiod::chip chip(argv[1]);
-	auto lines = chip.get_lines(offsets);
+	auto request = chip.request_lines(
+			::gpiod::request_config({
+				{ ::gpiod::request_config::property::OFFSETS, offsets },
+				{ ::gpiod::request_config::property::CONSUMER, "gpiogetcxx" }
+			}),
+			::gpiod::line_config());
 
-	lines.request({
-		argv[0],
-		::gpiod::line_request::DIRECTION_INPUT,
-		0
-	});
-
-	auto vals = lines.get_values();
+	auto vals = request.get_values();
 
 	for (auto& it: vals)
-		::std::cout << it << ' ';
+		::std::cout << (it == ::gpiod::line::value::ACTIVE ? "1" : "0") << ' ';
 	::std::cout << ::std::endl;
 
 	return EXIT_SUCCESS;
diff --git a/bindings/cxx/examples/gpioinfocxx.cpp b/bindings/cxx/examples/gpioinfocxx.cpp
index 2175adc..3612092 100644
--- a/bindings/cxx/examples/gpioinfocxx.cpp
+++ b/bindings/cxx/examples/gpioinfocxx.cpp
@@ -9,42 +9,52 @@
 #include <filesystem>
 #include <iostream>
 
-int main(int argc, char **argv)
+namespace {
+
+void show_chip(const ::gpiod::chip& chip)
 {
-	if (argc != 1) {
-		::std::cerr << "usage: " << argv[0] << ::std::endl;
-		return EXIT_FAILURE;
-	}
+	auto info = chip.get_info();
 
-	for (const auto& entry: ::std::filesystem::directory_iterator("/dev/")) {
-		if (::gpiod::is_gpiochip_device(entry.path())) {
-			::gpiod::chip chip(entry.path());
+	::std::cout << info.name() << " - " << info.num_lines() << " lines:" << ::std::endl;
+
+	for (unsigned int offset = 0; offset < info.num_lines(); offset++) {
+		auto info = chip.get_line_info(offset);
 
-			::std::cout << chip.name() << " - " << chip.num_lines() << " lines:" << ::std::endl;
+		::std::cout << "\tline ";
+		::std::cout.width(3);
+		::std::cout << info.offset() << ": ";
 
-			for (auto& lit: ::gpiod::line_iter(chip)) {
-				::std::cout << "\tline ";
-				::std::cout.width(3);
-				::std::cout << lit.offset() << ": ";
+		::std::cout.width(12);
+		::std::cout << (info.name().empty() ? "unnamed" : info.name());
+		::std::cout << " ";
 
-				::std::cout.width(12);
-				::std::cout << (lit.name().empty() ? "unnamed" : lit.name());
-				::std::cout << " ";
+		::std::cout.width(12);
+		::std::cout << (info.consumer().empty() ? "unused" : info.consumer());
+		::std::cout << " ";
 
-				::std::cout.width(12);
-				::std::cout << (lit.consumer().empty() ? "unused" : lit.consumer());
-				::std::cout << " ";
+		::std::cout.width(8);
+		::std::cout << (info.direction() == ::gpiod::line::direction::INPUT ? "input" : "output");
+		::std::cout << " ";
 
-				::std::cout.width(8);
-				::std::cout << (lit.direction() == ::gpiod::line::DIRECTION_INPUT ? "input" : "output");
-				::std::cout << " ";
+		::std::cout.width(10);
+		::std::cout << (info.active_low() ? "active-low" : "active-high");
 
-				::std::cout.width(10);
-				::std::cout << (lit.is_active_low() ? "active-low" : "active-high");
+		::std::cout << ::std::endl;
+	}
+}
 
-				::std::cout << ::std::endl;
-			}
-		}
+} /* namespace */
+
+int main(int argc, char **argv)
+{
+	if (argc != 1) {
+		::std::cerr << "usage: " << argv[0] << ::std::endl;
+		return EXIT_FAILURE;
+	}
+
+	for (const auto& entry: ::std::filesystem::directory_iterator("/dev/")) {
+		if (::gpiod::is_gpiochip_device(entry.path()))
+			show_chip(::gpiod::chip(entry.path()));
 	}
 
 	return EXIT_SUCCESS;
diff --git a/bindings/cxx/examples/gpiomoncxx.cpp b/bindings/cxx/examples/gpiomoncxx.cpp
index 4d6ac6e..db053dd 100644
--- a/bindings/cxx/examples/gpiomoncxx.cpp
+++ b/bindings/cxx/examples/gpiomoncxx.cpp
@@ -3,29 +3,27 @@
 
 /* Simplified C++ reimplementation of the gpiomon tool. */
 
-#include <gpiod.hpp>
-
+#include <chrono>
 #include <cstdlib>
+#include <gpiod.hpp>
 #include <iostream>
 
 namespace {
 
-void print_event(const ::gpiod::line_event& event)
+void print_event(const ::gpiod::edge_event& event)
 {
-	if (event.event_type == ::gpiod::line_event::RISING_EDGE)
+	if (event.type() == ::gpiod::edge_event::event_type::RISING_EDGE)
 		::std::cout << " RISING EDGE";
-	else if (event.event_type == ::gpiod::line_event::FALLING_EDGE)
-		::std::cout << "FALLING EDGE";
 	else
-		throw ::std::logic_error("invalid event type");
+		::std::cout << "FALLING EDGE";
 
 	::std::cout << " ";
 
-	::std::cout << ::std::chrono::duration_cast<::std::chrono::seconds>(event.timestamp).count();
+	::std::cout << event.timestamp_ns() / 1000000000;
 	::std::cout << ".";
-	::std::cout << event.timestamp.count() % 1000000000;
+	::std::cout << event.timestamp_ns() % 1000000000;
 
-	::std::cout << " line: " << event.source.offset();
+	::std::cout << " line: " << event.line_offset();
 
 	::std::cout << ::std::endl;
 }
@@ -39,25 +37,36 @@ int main(int argc, char **argv)
 		return EXIT_FAILURE;
 	}
 
-	::std::vector<unsigned int> offsets;
+	::gpiod::line::offsets offsets;
 	offsets.reserve(argc);
 	for (int i = 2; i < argc; i++)
 		offsets.push_back(::std::stoul(argv[i]));
 
 	::gpiod::chip chip(argv[1]);
-	auto lines = chip.get_lines(offsets);
-
-	lines.request({
-		argv[0],
-		::gpiod::line_request::EVENT_BOTH_EDGES,
-		0,
-	});
+	auto request = chip.request_lines(
+			::gpiod::request_config({
+				{ ::gpiod::request_config::property::OFFSETS, offsets},
+				{ ::gpiod::request_config::property::CONSUMER, "gpiomoncxx"},
+			}),
+			::gpiod::line_config({
+				{
+					::gpiod::line_config::property::DIRECTION,
+					::gpiod::line::direction::INPUT
+				},
+				{
+					::gpiod::line_config::property::EDGE,
+					::gpiod::line::edge::BOTH
+				}
+			}));
+
+	::gpiod::edge_event_buffer buffer;
 
 	for (;;) {
-		auto events = lines.event_wait(::std::chrono::seconds(1));
-		if (events) {
-			for (auto& it: events)
-				print_event(it.event_read());
+		if (request.wait_edge_event(::std::chrono::seconds(5))) {
+			request.read_edge_event(buffer);
+
+			for (const auto& event: buffer)
+				print_event(event);
 		}
 	}
 
diff --git a/bindings/cxx/examples/gpiosetcxx.cpp b/bindings/cxx/examples/gpiosetcxx.cpp
index 71b27a9..838d801 100644
--- a/bindings/cxx/examples/gpiosetcxx.cpp
+++ b/bindings/cxx/examples/gpiosetcxx.cpp
@@ -11,12 +11,13 @@
 int main(int argc, char **argv)
 {
 	if (argc < 3) {
-		::std::cerr << "usage: " << argv[0] << " <chip> <line_offset0>=<value0> ..." << ::std::endl;
+		::std::cerr << "usage: " << argv[0] <<
+			       " <chip> <line_offset0>=<value0> ..." << ::std::endl;
 		return EXIT_FAILURE;
 	}
 
-	::std::vector<unsigned int> offsets;
-	::std::vector<int> values;
+	::gpiod::line::offsets offsets;
+	::gpiod::line::values values;
 
 	for (int i = 2; i < argc; i++) {
 		::std::string arg(argv[i]);
@@ -27,20 +28,28 @@ int main(int argc, char **argv)
 		::std::string value(arg.substr(pos + 1, ::std::string::npos));
 
 		if (offset.empty() || value.empty())
-			throw ::std::invalid_argument("invalid argument: " + ::std::string(argv[i]));
+			throw ::std::invalid_argument("invalid offset=value mapping: " +
+						      ::std::string(argv[i]));
 
 		offsets.push_back(::std::stoul(offset));
-		values.push_back(::std::stoul(value));
+		values.push_back(::std::stoul(value) ? ::gpiod::line::value::ACTIVE :
+						       ::gpiod::line::value::INACTIVE);
 	}
 
 	::gpiod::chip chip(argv[1]);
-	auto lines = chip.get_lines(offsets);
-
-	lines.request({
-		argv[0],
-		::gpiod::line_request::DIRECTION_OUTPUT,
-		0
-	}, values);
+	auto request = chip.request_lines(
+			::gpiod::request_config({
+				{ ::gpiod::request_config::property::OFFSETS, offsets },
+				{ ::gpiod::request_config::property::CONSUMER, "gpiogetcxx" }
+			}),
+			::gpiod::line_config({
+				{
+					::gpiod::line_config::property::DIRECTION,
+					::gpiod::line::direction::OUTPUT
+				}
+			}));
+
+	request.set_values(values);
 
 	::std::cin.get();
 
diff --git a/bindings/cxx/exception.cpp b/bindings/cxx/exception.cpp
new file mode 100644
index 0000000..6a015a0
--- /dev/null
+++ b/bindings/cxx/exception.cpp
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+GPIOD_CXX_API chip_closed::chip_closed(const ::std::string& what)
+	: ::std::logic_error(what)
+{
+
+}
+
+GPIOD_CXX_API chip_closed::chip_closed(const chip_closed& other) noexcept
+	: ::std::logic_error(other)
+{
+
+}
+
+GPIOD_CXX_API chip_closed::chip_closed(chip_closed&& other) noexcept
+	: ::std::logic_error(other)
+{
+
+}
+
+GPIOD_CXX_API chip_closed& chip_closed::operator=(const chip_closed& other) noexcept
+{
+	::std::logic_error::operator=(other);
+
+	return *this;
+}
+
+GPIOD_CXX_API chip_closed& chip_closed::operator=(chip_closed&& other) noexcept
+{
+	::std::logic_error::operator=(other);
+
+	return *this;
+}
+
+GPIOD_CXX_API chip_closed::~chip_closed(void)
+{
+
+}
+
+GPIOD_CXX_API request_released::request_released(const ::std::string& what)
+	: ::std::logic_error(what)
+{
+
+}
+
+GPIOD_CXX_API request_released::request_released(const request_released& other) noexcept
+	: ::std::logic_error(other)
+{
+
+}
+
+GPIOD_CXX_API request_released::request_released(request_released&& other) noexcept
+	: ::std::logic_error(other)
+{
+
+}
+
+GPIOD_CXX_API request_released& request_released::operator=(const request_released& other) noexcept
+{
+	::std::logic_error::operator=(other);
+
+	return *this;
+}
+
+GPIOD_CXX_API request_released& request_released::operator=(request_released&& other) noexcept
+{
+	::std::logic_error::operator=(other);
+
+	return *this;
+}
+
+GPIOD_CXX_API request_released::~request_released(void)
+{
+
+}
+
+GPIOD_CXX_API bad_mapping::bad_mapping(const ::std::string& what)
+	: ::std::runtime_error(what)
+{
+
+}
+
+GPIOD_CXX_API bad_mapping::bad_mapping(const bad_mapping& other) noexcept
+	: ::std::runtime_error(other)
+{
+
+}
+
+GPIOD_CXX_API bad_mapping::bad_mapping(bad_mapping&& other) noexcept
+	: ::std::runtime_error(other)
+{
+
+}
+
+GPIOD_CXX_API bad_mapping& bad_mapping::operator=(const bad_mapping& other) noexcept
+{
+	::std::runtime_error::operator=(other);
+
+	return *this;
+}
+
+GPIOD_CXX_API bad_mapping& bad_mapping::operator=(bad_mapping&& other) noexcept
+{
+	::std::runtime_error::operator=(other);
+
+	return *this;
+}
+
+GPIOD_CXX_API bad_mapping::~bad_mapping(void)
+{
+
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/gpiod.hpp b/bindings/cxx/gpiod.hpp
index e3ce2fc..dc7e938 100644
--- a/bindings/cxx/gpiod.hpp
+++ b/bindings/cxx/gpiod.hpp
@@ -1,940 +1,46 @@
 /* SPDX-License-Identifier: LGPL-3.0-or-later */
-/* SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com> */
-
-#ifndef __LIBGPIOD_GPIOD_CXX_HPP__
-#define __LIBGPIOD_GPIOD_CXX_HPP__
-
-#include <bitset>
-#include <chrono>
-#include <gpiod.h>
-#include <memory>
-#include <string>
-#include <vector>
-
-namespace gpiod {
-
-class line;
-class line_bulk;
-class line_iter;
-class chip_iter;
-struct line_event;
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
 
 /**
  * @file gpiod.hpp
  */
 
-/**
- * @defgroup gpiod_cxx C++ bindings
- * @{
- */
-
-/**
- * @brief Check if the file pointed to by path is a GPIO chip character device.
- * @param path Path to check.
- * @return True if the file exists and is a GPIO chip character device or a
- *         symbolic link to it.
- */
-bool is_gpiochip_device(const ::std::string& path);
-
-/**
- * @brief Represents a GPIO chip.
- *
- * Internally this class holds a smart pointer to an open GPIO chip descriptor.
- * Multiple objects of this class can reference the same chip. The chip is
- * closed and all resources freed when the last reference is dropped.
- */
-class chip
-{
-public:
-
-	/**
-	 * @brief Default constructor. Creates an empty GPIO chip object.
-	 */
-	chip(void) = default;
-
-	/**
-	 * @brief Constructor. Opens the chip using chip::open.
-	 * @param path Path to the GPIO chip device.
-	 */
-	chip(const ::std::string& path);
-
-	/**
-	 * @brief Copy constructor. References the object held by other.
-	 * @param other Other chip object.
-	 */
-	chip(const chip& other) = default;
-
-	/**
-	 * @brief Move constructor. References the object held by other.
-	 * @param other Other chip object.
-	 */
-	chip(chip&& other) = default;
-
-	/**
-	 * @brief Assignment operator. References the object held by other.
-	 * @param other Other chip object.
-	 * @return Reference to this object.
-	 */
-	chip& operator=(const chip& other) = default;
-
-	/**
-	 * @brief Move assignment operator. References the object held by other.
-	 * @param other Other chip object.
-	 * @return Reference to this object.
-	 */
-	chip& operator=(chip&& other) = default;
-
-	/**
-	 * @brief Destructor. Unreferences the internal chip object.
-	 */
-	~chip(void) = default;
-
-	/**
-	 * @brief Open a GPIO chip.
-	 * @param path Path to the GPIO chip device.
-	 *
-	 * If the object already holds a reference to an open chip, it will be
-	 * closed and the reference reset.
-	 */
-	void open(const ::std::string &path);
-
-	/**
-	 * @brief Reset the internal smart pointer owned by this object.
-	 */
-	void reset(void) noexcept;
-
-	/**
-	 * @brief Return the name of the chip held by this object.
-	 * @return Name of the GPIO chip.
-	 */
-	::std::string name(void) const;
-
-	/**
-	 * @brief Return the label of the chip held by this object.
-	 * @return Label of the GPIO chip.
-	 */
-	::std::string label(void) const;
-
-	/**
-	 * @brief Return the number of lines exposed by this chip.
-	 * @return Number of lines.
-	 */
-	unsigned int num_lines(void) const;
-
-	/**
-	 * @brief Get the line exposed by this chip at given offset.
-	 * @param offset Offset of the line.
-	 * @return Line object.
-	 */
-	line get_line(unsigned int offset) const;
-
-	/**
-	 * @brief Map a GPIO line's name to its offset within the chip.
-	 * @param name Name of the GPIO line to map.
-	 * @return Offset of the line within the chip or -1 if a line with
-	 *         given name is not exposed by the chip.
-	 */
-	int find_line(const ::std::string& name) const;
-
-	/**
-	 * @brief Get a set of lines exposed by this chip at given offsets.
-	 * @param offsets Vector of line offsets.
-	 * @return Set of lines held by a line_bulk object.
-	 */
-	line_bulk get_lines(const ::std::vector<unsigned int>& offsets) const;
-
-	/**
-	 * @brief Get all lines exposed by this chip.
-	 * @return All lines exposed by this chip held by a line_bulk object.
-	 */
-	line_bulk get_all_lines(void) const;
-
-	/**
-	 * @brief Equality operator.
-	 * @param rhs Right-hand side of the equation.
-	 * @return True if rhs references the same chip. False otherwise.
-	 */
-	bool operator==(const chip& rhs) const noexcept;
-
-	/**
-	 * @brief Inequality operator.
-	 * @param rhs Right-hand side of the equation.
-	 * @return False if rhs references the same chip. True otherwise.
-	 */
-	bool operator!=(const chip& rhs) const noexcept;
-
-	/**
-	 * @brief Check if this object holds a reference to a GPIO chip.
-	 * @return True if this object references a GPIO chip, false otherwise.
-	 */
-	explicit operator bool(void) const noexcept;
-
-	/**
-	 * @brief Check if this object doesn't hold a reference to a GPIO chip.
-	 * @return False if this object references a GPIO chip, true otherwise.
-	 */
-	bool operator!(void) const noexcept;
-
-private:
-
-	chip(::gpiod_chip* chip);
-	chip(const ::std::weak_ptr<::gpiod_chip>& chip_ptr);
-
-	void throw_if_noref(void) const;
-
-	::std::shared_ptr<::gpiod_chip> _m_chip;
-
-	friend line;
-	friend chip_iter;
-	friend line_iter;
-};
-
-/**
- * @brief Stores the configuration for line requests.
- */
-struct line_request
-{
-	/**
-	 * @brief Request types.
-	 */
-	enum : int {
-		DIRECTION_AS_IS = 1,
-		/**< Request for values, don't change the direction. */
-		DIRECTION_INPUT,
-		/**< Request for reading line values. */
-		DIRECTION_OUTPUT,
-		/**< Request for driving the GPIO lines. */
-		EVENT_FALLING_EDGE,
-		/**< Listen for falling edge events. */
-		EVENT_RISING_EDGE,
-		/**< Listen for rising edge events. */
-		EVENT_BOTH_EDGES,
-		/**< Listen for all types of events. */
-	};
-
-	static const ::std::bitset<32> FLAG_ACTIVE_LOW;
-	/**< Set the active state to 'low' (high is the default). */
-	static const ::std::bitset<32> FLAG_OPEN_SOURCE;
-	/**< The line is an open-source port. */
-	static const ::std::bitset<32> FLAG_OPEN_DRAIN;
-	/**< The line is an open-drain port. */
-	static const ::std::bitset<32> FLAG_BIAS_DISABLED;
-	/**< The line has neither pull-up nor pull-down resistor enabled. */
-	static const ::std::bitset<32> FLAG_BIAS_PULL_DOWN;
-	/**< The line has a configurable pull-down resistor enabled. */
-	static const ::std::bitset<32> FLAG_BIAS_PULL_UP;
-	/**< The line has a configurable pull-up resistor enabled. */
-
-	::std::string consumer;
-	/**< Consumer name to pass to the request. */
-	int request_type;
-	/**< Type of the request. */
-	::std::bitset<32> flags;
-	/**< Additional request flags. */
-};
-
-/**
- * @brief Represents a single GPIO line.
- *
- * Internally this class holds a raw pointer to a GPIO line descriptor and a
- * reference to the parent chip. All line resources are freed when the last
- * reference to the parent chip is dropped.
- */
-class line
-{
-public:
-
-	/**
-	 * @brief Default constructor. Creates an empty line object.
-	 */
-	line(void);
-
-	/**
-	 * @brief Copy constructor.
-	 * @param other Other line object.
-	 */
-	line(const line& other) = default;
-
-	/**
-	 * @brief Move constructor.
-	 * @param other Other line object.
-	 */
-	line(line&& other) = default;
-
-	/**
-	 * @brief Assignment operator.
-	 * @param other Other line object.
-	 * @return Reference to this object.
-	 */
-	line& operator=(const line& other) = default;
-
-	/**
-	 * @brief Move assignment operator.
-	 * @param other Other line object.
-	 * @return Reference to this object.
-	 */
-	line& operator=(line&& other) = default;
-
-	/**
-	 * @brief Destructor.
-	 */
-	~line(void) = default;
-
-	/**
-	 * @brief Get the offset of this line.
-	 * @return Offet of this line.
-	 */
-	unsigned int offset(void) const;
-
-	/**
-	 * @brief Get the name of this line (if any).
-	 * @return Name of this line or an empty string if it is unnamed.
-	 */
-	::std::string name(void) const;
-
-	/**
-	 * @brief Get the consumer of this line (if any).
-	 * @return Name of the consumer of this line or an empty string if it
-	 *         is unused.
-	 */
-	::std::string consumer(void) const;
-
-	/**
-	 * @brief Get current direction of this line.
-	 * @return Current direction setting.
-	 */
-	int direction(void) const;
-
-	/**
-	 * @brief Check if this line's signal is inverted.
-	 * @return True if this line is "active-low", false otherwise.
-	 */
-	bool is_active_low(void) const;
-
-	/**
-	 * @brief Get current bias of this line.
-	 * @return Current bias setting.
-	 */
-	int bias(void) const;
-
-	/**
-	 * @brief Check if this line is used by the kernel or other user space
-	 *        process.
-	 * @return True if this line is in use, false otherwise.
-	 */
-	bool is_used(void) const;
-
-	/**
-	 * @brief Get current drive setting of this line.
-	 * @return Current drive setting.
-	 */
-	int drive(void) const;
-
-	/**
-	 * @brief Request this line.
-	 * @param config Request config (see gpiod::line_request).
-	 * @param default_val Default value - only matters for OUTPUT direction.
-	 */
-	void request(const line_request& config, int default_val = 0) const;
-
-	/**
-	 * @brief Release the line if it was previously requested.
-	 */
-	void release(void) const;
-
-	/**
-	 * @brief Read the line value.
-	 * @return Current value (0 or 1).
-	 */
-	int get_value(void) const;
-
-	/**
-	 * @brief Set the value of this line.
-	 * @param val New value (0 or 1).
-	 */
-	void set_value(int val) const;
-
-	/**
-	 * @brief Set configuration of this line.
-	 * @param direction New direction.
-	 * @param flags Replacement flags.
-	 * @param value New value (0 or 1) - only matters for OUTPUT direction.
-	 */
-	void set_config(int direction, ::std::bitset<32> flags, int value = 0) const;
-
-	/**
-	 * @brief Set configuration flags of this line.
-	 * @param flags Replacement flags.
-	 */
-	void set_flags(::std::bitset<32> flags) const;
-
-	/**
-	 * @brief Change the direction this line to input.
-	 */
-	void set_direction_input() const;
-
-	/**
-	 * @brief Change the direction this lines to output.
-	 * @param value New value (0 or 1).
-	 */
-	void set_direction_output(int value = 0) const;
-
-	/**
-	 * @brief Wait for an event on this line.
-	 * @param timeout Time to wait before returning if no event occurred.
-	 * @return True if an event occurred and can be read, false if the wait
-	 *         timed out.
-	 */
-	bool event_wait(const ::std::chrono::nanoseconds& timeout) const;
-
-	/**
-	 * @brief Read a line event.
-	 * @return Line event object.
-	 */
-	line_event event_read(void) const;
-
-	/**
-	 * @brief Read multiple line events.
-	 * @return Vector of line event objects.
-	 */
-	::std::vector<line_event> event_read_multiple(void) const;
-
-	/**
-	 * @brief Get the event file descriptor associated with this line.
-	 * @return File descriptor number.
-	 */
-	int event_get_fd(void) const;
-
-	/**
-	 * @brief Get the parent chip.
-	 * @return Parent chip of this line.
-	 */
-	const chip get_chip(void) const;
-
-	/**
-	 * @brief Reset the state of this object.
-	 *
-	 * This is useful when the user needs to e.g. keep the line_event object
-	 * but wants to drop the reference to the GPIO chip indirectly held by
-	 * the line being the source of the event.
-	 */
-	void reset(void);
-
-	/**
-	 * @brief Check if two line objects reference the same GPIO line.
-	 * @param rhs Right-hand side of the equation.
-	 * @return True if both objects reference the same line, fale otherwise.
-	 */
-	bool operator==(const line& rhs) const noexcept;
-
-	/**
-	 * @brief Check if two line objects reference different GPIO lines.
-	 * @param rhs Right-hand side of the equation.
-	 * @return False if both objects reference the same line, true otherwise.
-	 */
-	bool operator!=(const line& rhs) const noexcept;
-
-	/**
-	 * @brief Check if this object holds a reference to any GPIO line.
-	 * @return True if this object references a GPIO line, false otherwise.
-	 */
-	explicit operator bool(void) const noexcept;
-
-	/**
-	 * @brief Check if this object doesn't reference any GPIO line.
-	 * @return True if this object doesn't reference any GPIO line, true
-	 *         otherwise.
-	 */
-	bool operator!(void) const noexcept;
-
-	/**
-	 * @brief Possible direction settings.
-	 */
-	enum : int {
-		DIRECTION_INPUT = 1,
-		/**< Line's direction setting is input. */
-		DIRECTION_OUTPUT,
-		/**< Line's direction setting is output. */
-	};
-
-	/**
-	 * @brief Possible drive settings.
-	 */
-	enum : int {
-		DRIVE_PUSH_PULL = 1,
-		/**< Drive setting is unknown. */
-		DRIVE_OPEN_DRAIN,
-		/**< Line output is open-drain. */
-		DRIVE_OPEN_SOURCE,
-		/**< Line output is open-source. */
-	};
-
-	/**
-	 * @brief Possible bias settings.
-	 */
-	enum : int {
-		BIAS_UNKNOWN = 1,
-		/**< Line's bias state is unknown. */
-		BIAS_DISABLED,
-		/**< Line's internal bias is disabled. */
-		BIAS_PULL_UP,
-		/**< Line's internal pull-up bias is enabled. */
-		BIAS_PULL_DOWN,
-		/**< Line's internal pull-down bias is enabled. */
-	};
-
-private:
-
-	line(::gpiod_line* line, const chip& owner);
-
-	void throw_if_null(void) const;
-	line_event make_line_event(const ::gpiod_line_event& event) const noexcept;
-
-	::gpiod_line* _m_line;
-	::std::weak_ptr<::gpiod_chip> _m_owner;
-
-	class chip_guard
-	{
-	public:
-		chip_guard(const line& line);
-		~chip_guard(void) = default;
-
-		chip_guard(const chip_guard& other) = delete;
-		chip_guard(chip_guard&& other) = delete;
-		chip_guard& operator=(const chip_guard&& other) = delete;
-		chip_guard& operator=(chip_guard&& other) = delete;
-
-	private:
-		::std::shared_ptr<::gpiod_chip> _m_chip;
-	};
-
-	friend chip;
-	friend line_bulk;
-	friend line_iter;
-};
-
-/**
- * @brief Describes a single GPIO line event.
- */
-struct line_event
-{
-	/**
-	 * @brief Possible event types.
-	 */
-	enum : int {
-		RISING_EDGE = 1,
-		/**< Rising edge event. */
-		FALLING_EDGE,
-		/**< Falling edge event. */
-	};
-
-	::std::chrono::nanoseconds timestamp;
-	/**< Best estimate of time of event occurrence in nanoseconds. */
-	int event_type;
-	/**< Type of the event that occurred. */
-	line source;
-	/**< Line object referencing the GPIO line on which the event occurred. */
-};
+#ifndef __LIBGPIOD_GPIOD_CXX_HPP__
+#define __LIBGPIOD_GPIOD_CXX_HPP__
 
 /**
- * @brief Represents a set of GPIO lines.
+ * @defgroup gpiod_cxx C++ bindings
  *
- * Internally an object of this class stores an array of line objects
- * owned by a single chip.
+ * C++ bindings for libgpiod.
  */
-class line_bulk
-{
-public:
-
-	/**
-	 * @brief Default constructor. Creates an empty line_bulk object.
-	 */
-	line_bulk(void) = default;
-
-	/**
-	 * @brief Construct a line_bulk from a vector of lines.
-	 * @param lines Vector of gpiod::line objects.
-	 * @note All lines must be owned by the same GPIO chip.
-	 */
-	line_bulk(const ::std::vector<line>& lines);
-
-	/**
-	 * @brief Copy constructor.
-	 * @param other Other line_bulk object.
-	 */
-	line_bulk(const line_bulk& other) = default;
-
-	/**
-	 * @brief Move constructor.
-	 * @param other Other line_bulk object.
-	 */
-	line_bulk(line_bulk&& other) = default;
-
-	/**
-	 * @brief Assignment operator.
-	 * @param other Other line_bulk object.
-	 * @return Reference to this object.
-	 */
-	line_bulk& operator=(const line_bulk& other) = default;
-
-	/**
-	 * @brief Move assignment operator.
-	 * @param other Other line_bulk object.
-	 * @return Reference to this object.
-	 */
-	line_bulk& operator=(line_bulk&& other) = default;
-
-	/**
-	 * @brief Destructor.
-	 */
-	~line_bulk(void) = default;
-
-	/**
-	 * @brief Add a line to this line_bulk object.
-	 * @param new_line Line to add.
-	 * @note The new line must be owned by the same chip as all the other
-	 *       lines already held by this line_bulk object.
-	 */
-	void append(const line& new_line);
-
-	/**
-	 * @brief Get the line at given offset.
-	 * @param index Index of the line to get.
-	 * @return Reference to the line object.
-	 * @note This method will throw if index is equal or greater than the
-	 *       number of lines currently held by this bulk.
-	 */
-	line& get(unsigned int index);
-
-	/**
-	 * @brief Get the line at given offset without bounds checking.
-	 * @param index Offset of the line to get.
-	 * @return Reference to the line object.
-	 * @note No bounds checking is performed.
-	 */
-	line& operator[](unsigned int index);
-
-	/**
-	 * @brief Get the number of lines currently held by this object.
-	 * @return Number of elements in this line_bulk.
-	 */
-	unsigned int size(void) const noexcept;
-
-	/**
-	 * @brief Check if this line_bulk doesn't hold any lines.
-	 * @return True if this object is empty, false otherwise.
-	 */
-	bool empty(void) const noexcept;
-
-	/**
-	 * @brief Remove all lines from this object.
-	 */
-	void clear(void);
-
-	/**
-	 * @brief Request all lines held by this object.
-	 * @param config Request config (see gpiod::line_request).
-	 * @param default_vals Vector of default values. Only relevant for
-	 *                     output direction requests.
-	 */
-	void request(const line_request& config,
-		     const ::std::vector<int> default_vals = ::std::vector<int>()) const;
-
-	/**
-	 * @brief Release all lines held by this object.
-	 */
-	void release(void) const;
-
-	/**
-	 * @brief Read values from all lines held by this object.
-	 * @return Vector containing line values the order of which corresponds
-	 *         with the order of lines in the internal array.
-	 */
-	::std::vector<int> get_values(void) const;
-
-	/**
-	 * @brief Set values of all lines held by this object.
-	 * @param values Vector of values to set. Must be the same size as the
-	 *               number of lines held by this line_bulk.
-	 */
-	void set_values(const ::std::vector<int>& values) const;
-
-	/**
-	 * @brief Set configuration of all lines held by this object.
-	 * @param direction New direction.
-	 * @param flags Replacement flags.
-	 * @param values Vector of values to set. Must be the same size as the
-	 *               number of lines held by this line_bulk.
-	 *               Only relevant for output direction requests.
-	 */
-	void set_config(int direction, ::std::bitset<32> flags,
-			const ::std::vector<int> values = ::std::vector<int>()) const;
-
-	/**
-	 * @brief Set configuration flags of all lines held by this object.
-	 * @param flags Replacement flags.
-	 */
-	void set_flags(::std::bitset<32> flags) const;
-
-	/**
-	 * @brief Change the direction all lines held by this object to input.
-	 */
-	void set_direction_input() const;
-
-	/**
-	 * @brief Change the direction all lines held by this object to output.
-	 * @param values Vector of values to set. Must be the same size as the
-	 *               number of lines held by this line_bulk.
-	 */
-	void set_direction_output(const ::std::vector<int>& values) const;
-
-	/**
-	 * @brief Poll the set of lines for line events.
-	 * @param timeout Number of nanoseconds to wait before returning an
-	 *                empty line_bulk.
-	 * @return Returns a line_bulk object containing lines on which events
-	 *         occurred.
-	 */
-	line_bulk event_wait(const ::std::chrono::nanoseconds& timeout) const;
-
-	/**
-	 * @brief Check if this object holds any lines.
-	 * @return True if this line_bulk holds at least one line, false otherwise.
-	 */
-	explicit operator bool(void) const noexcept;
-
-	/**
-	 * @brief Check if this object doesn't hold any lines.
-	 * @return True if this line_bulk is empty, false otherwise.
-	 */
-	bool operator!(void) const noexcept;
-
-	/**
-	 * @brief Max number of lines that this object can hold.
-	 */
-	static const unsigned int MAX_LINES;
-
-	/**
-	 * @brief Iterator for iterating over lines held by line_bulk.
-	 */
-	class iterator
-	{
-	public:
-
-		/**
-		 * @brief Default constructor. Builds an empty iterator object.
-		 */
-		iterator(void) = default;
-
-		/**
-		 * @brief Copy constructor.
-		 * @param other Other line_bulk iterator.
-		 */
-		iterator(const iterator& other) = default;
-
-		/**
-		 * @brief Move constructor.
-		 * @param other Other line_bulk iterator.
-		 */
-		iterator(iterator&& other) = default;
-
-		/**
-		 * @brief Assignment operator.
-		 * @param other Other line_bulk iterator.
-		 * @return Reference to this iterator.
-		 */
-		iterator& operator=(const iterator& other) = default;
-
-		/**
-		 * @brief Move assignment operator.
-		 * @param other Other line_bulk iterator.
-		 * @return Reference to this iterator.
-		 */
-		iterator& operator=(iterator&& other) = default;
-
-		/**
-		 * @brief Destructor.
-		 */
-		~iterator(void) = default;
-
-		/**
-		 * @brief Advance the iterator by one element.
-		 * @return Reference to this iterator.
-		 */
-		iterator& operator++(void);
-
-		/**
-		 * @brief Dereference current element.
-		 * @return Current GPIO line by reference.
-		 */
-		const line& operator*(void) const;
-
-		/**
-		 * @brief Member access operator.
-		 * @return Current GPIO line by pointer.
-		 */
-		const line* operator->(void) const;
-
-		/**
-		 * @brief Check if this operator points to the same element.
-		 * @param rhs Right-hand side of the equation.
-		 * @return True if this iterator points to the same GPIO line,
-		 *         false otherwise.
-		 */
-		bool operator==(const iterator& rhs) const noexcept;
-
-		/**
-		 * @brief Check if this operator doesn't point to the same element.
-		 * @param rhs Right-hand side of the equation.
-		 * @return True if this iterator doesn't point to the same GPIO
-		 *         line, false otherwise.
-		 */
-		bool operator!=(const iterator& rhs) const noexcept;
-
-	private:
-
-		iterator(const ::std::vector<line>::iterator& it);
-
-		::std::vector<line>::iterator _m_iter;
-
-		friend line_bulk;
-	};
-
-	/**
-	 * @brief Returns an iterator to the first line.
-	 * @return A line_bulk iterator.
-	 */
-	iterator begin(void) noexcept;
-
-	/**
-	 * @brief Returns an iterator to the element following the last line.
-	 * @return A line_bulk iterator.
-	 */
-	iterator end(void) noexcept;
-
-private:
-
-	struct line_bulk_deleter
-	{
-		void operator()(::gpiod_line_bulk *bulk);
-	};
-
-	void throw_if_empty(void) const;
-
-	using line_bulk_ptr = ::std::unique_ptr<::gpiod_line_bulk, line_bulk_deleter>;
-
-	line_bulk_ptr make_line_bulk_ptr(void) const;
-	line_bulk_ptr to_line_bulk(void) const;
-
-	::std::vector<line> _m_bulk;
-};
-
-/**
- * @brief Support for range-based loops for line iterators.
- * @param iter A line iterator.
- * @return Iterator unchanged.
- */
-line_iter begin(line_iter iter) noexcept;
 
 /**
- * @brief Support for range-based loops for line iterators.
- * @param iter A line iterator.
- * @return New end iterator.
+ * @cond
  */
-line_iter end(const line_iter& iter) noexcept;
 
-/**
- * @brief Allows to iterate over all lines owned by a GPIO chip.
+/*
+ * We don't make this symbol private because it needs to be accessible by
+ * the declarations in exception.hpp in order to expose the symbols of classes
+ * inheriting from standard exceptions.
  */
-class line_iter
-{
-public:
-
-	/**
-	 * @brief Default constructor. Creates the end iterator.
-	 */
-	line_iter(void) = default;
-
-	/**
-	 * @brief Constructor. Creates the begin iterator.
-	 * @param owner Chip owning the GPIO lines over which we want to iterate.
-	 */
-	line_iter(const chip& owner);
-
-	/**
-	 * @brief Copy constructor.
-	 * @param other Other line iterator.
-	 */
-	line_iter(const line_iter& other) = default;
-
-	/**
-	 * @brief Move constructor.
-	 * @param other Other line iterator.
-	 */
-	line_iter(line_iter&& other) = default;
+#define GPIOD_CXX_API __attribute__((visibility("default")))
 
-	/**
-	 * @brief Assignment operator.
-	 * @param other Other line iterator.
-	 * @return Reference to this line_iter.
-	 */
-	line_iter& operator=(const line_iter& other) = default;
-
-	/**
-	 * @brief Move assignment operator.
-	 * @param other Other line iterator.
-	 * @return Reference to this line_iter.
-	 */
-	line_iter& operator=(line_iter&& other) = default;
-
-	/**
-	 * @brief Destructor.
-	 */
-	~line_iter(void) = default;
-
-	/**
-	 * @brief Advance the iterator by one element.
-	 * @return Reference to this iterator.
-	 */
-	line_iter& operator++(void);
-
-	/**
-	 * @brief Dereference current element.
-	 * @return Current GPIO line by reference.
-	 */
-	const line& operator*(void) const;
-
-	/**
-	 * @brief Member access operator.
-	 * @return Current GPIO line by pointer.
-	 */
-	const line* operator->(void) const;
-
-	/**
-	 * @brief Check if this operator points to the same element.
-	 * @param rhs Right-hand side of the equation.
-	 * @return True if this iterator points to the same line_iter,
-	 *         false otherwise.
-	 */
-	bool operator==(const line_iter& rhs) const noexcept;
-
-	/**
-	 * @brief Check if this operator doesn't point to the same element.
-	 * @param rhs Right-hand side of the equation.
-	 * @return True if this iterator doesn't point to the same line_iter,
-	 *         false otherwise.
-	 */
-	bool operator!=(const line_iter& rhs) const noexcept;
-
-private:
-
-	line _m_current;
-};
+#define __LIBGPIOD_GPIOD_CXX_INSIDE__
+#include "gpiodcxx/chip.hpp"
+#include "gpiodcxx/chip-info.hpp"
+#include "gpiodcxx/edge-event.hpp"
+#include "gpiodcxx/edge-event-buffer.hpp"
+#include "gpiodcxx/exception.hpp"
+#include "gpiodcxx/info-event.hpp"
+#include "gpiodcxx/line.hpp"
+#include "gpiodcxx/line-config.hpp"
+#include "gpiodcxx/line-info.hpp"
+#include "gpiodcxx/line-request.hpp"
+#include "gpiodcxx/request-config.hpp"
+#undef __LIBGPIOD_GPIOD_CXX_INSIDE__
 
 /**
- * @}
+ * @endcond
  */
 
-} /* namespace gpiod */
-
 #endif /* __LIBGPIOD_GPIOD_CXX_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/Makefile.am b/bindings/cxx/gpiodcxx/Makefile.am
new file mode 100644
index 0000000..71532e6
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/Makefile.am
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <brgl@bgdev.pl>
+
+otherincludedir = $(includedir)/gpiodcxx
+otherinclude_HEADERS = \
+	chip.hpp \
+	chip-info.hpp \
+	edge-event-buffer.hpp \
+	edge-event.hpp \
+	exception.hpp \
+	info-event.hpp \
+	line.hpp \
+	line-config.hpp \
+	line-info.hpp \
+	line-request.hpp \
+	misc.hpp \
+	request-config.hpp \
+	timestamp.hpp
diff --git a/bindings/cxx/gpiodcxx/chip-info.hpp b/bindings/cxx/gpiodcxx/chip-info.hpp
new file mode 100644
index 0000000..9313e88
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/chip-info.hpp
@@ -0,0 +1,105 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file chip-info.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_CHIP_INFO_HPP__
+#define __LIBGPIOD_CXX_CHIP_INFO_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <memory>
+#include <ostream>
+
+namespace gpiod {
+
+class chip;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Represents an immutable snapshot of GPIO chip information.
+ */
+class chip_info
+{
+public:
+
+	/**
+	 * @brief Copy constructor.
+	 * @param other Object to copy.
+	 */
+	chip_info(const chip_info& other);
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	chip_info(chip_info&& other) noexcept;
+
+	~chip_info(void);
+
+	/**
+	 * @brief Assignment operator.
+	 * @param other Object to copy.
+	 * @return Reference to self.
+	 */
+	chip_info& operator=(const chip_info& other);
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 * @return Reference to self.
+	 */
+	chip_info& operator=(chip_info&& other) noexcept;
+
+	/**
+	 * @brief Get the name of this GPIO chip.
+	 * @return String containing the chip name.
+	 */
+	::std::string name(void) const noexcept;
+
+	/**
+	 * @brief Get the label of this GPIO chip.
+	 * @return String containing the chip name.
+	 */
+	::std::string label(void) const noexcept;
+
+	/**
+	 * @brief Return the number of lines exposed by this chip.
+	 * @return Number of lines.
+	 */
+	::std::size_t num_lines(void) const noexcept;
+
+private:
+
+	chip_info(void);
+
+	struct impl;
+
+	::std::shared_ptr<impl> _m_priv;
+
+	friend chip;
+};
+
+/**
+ * @brief Stream insertion operator for GPIO chip objects.
+ * @param out Output stream to write to.
+ * @param chip GPIO chip to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const chip_info& chip);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_CHIP_INFO_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/chip.hpp b/bindings/cxx/gpiodcxx/chip.hpp
new file mode 100644
index 0000000..7cc2156
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/chip.hpp
@@ -0,0 +1,179 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file chip.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_CHIP_HPP__
+#define __LIBGPIOD_CXX_CHIP_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <chrono>
+#include <cstddef>
+#include <iostream>
+#include <filesystem>
+#include <memory>
+
+#include "line.hpp"
+
+namespace gpiod {
+
+class chip_info;
+class info_event;
+class line_config;
+class line_info;
+class line_request;
+class request_config;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Represents a GPIO chip.
+ */
+class chip
+{
+public:
+
+	/**
+	 * @brief Instantiates a new chip object by opening the device file
+	 *        indicated by the path argument.
+	 * @param path Path to the device file to open.
+	 */
+	explicit chip(const ::std::filesystem::path& path);
+
+	chip(const chip& other) = delete;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	chip(chip&& other) noexcept;
+
+	~chip(void);
+
+	chip& operator=(const chip& other) = delete;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 * @return Reference to self.
+	 */
+	chip& operator=(chip&& other) noexcept;
+
+	/**
+	 * @brief Check if this object is valid.
+	 * @return True if this object's methods can be used, false otherwise.
+	 *         False usually means the chip was closed. If the user calls
+	 *         any of the methods of this class on an object for which this
+	 *         operator returned false, a logic_error will be thrown.
+	 */
+	explicit operator bool(void) const noexcept;
+
+	/**
+	 * @brief Close the GPIO chip device file and free associated resources.
+	 * @note The chip object can live after calling this method but any of
+	 *       the chip's mutators will throw a logic_error exception.
+	 */
+	void close(void);
+
+	/**
+	 * @brief Get the filesystem path that was used to open this GPIO chip.
+	 * @return Path to the underlying character device file.
+	 */
+	::std::filesystem::path path(void) const;
+
+	/**
+	 * @brief Get information about the chip.
+	 * @return New chip_info object.
+	 */
+	chip_info get_info(void) const;
+
+	/**
+	 * @brief Retrieve the current snapshot of line information for a
+	 *        single line.
+	 * @param offset Offset of the line to get the info for.
+	 * @return New ::gpiod::line_info object.
+	 */
+	line_info get_line_info(line::offset offset) const;
+
+	/**
+	 * @brief Wrapper around ::gpiod::chip::get_line_info that retrieves
+	 *        the line info and starts watching the line for changes.
+	 * @param offset Offset of the line to get the info for.
+	 * @return New ::gpiod::line_info object.
+	 */
+	line_info watch_line_info(line::offset offset) const;
+
+	/**
+	 * @brief Stop watching the line at given offset for info events.
+	 * @param offset Offset of the line to get the info for.
+	 */
+	void unwatch_line_info(line::offset offset) const;
+
+	/**
+	 * @brief Get the file descriptor associated with this chip.
+	 * @return File descriptor number.
+	 */
+	int fd(void) const;
+
+	/**
+	 * @brief Wait for line status events on any of the watched lines
+	 *        exposed by this chip.
+	 * @param timeout Wait time limit in nanoseconds.
+	 * @return True if at least one event is ready to be read. False if the
+	 *         wait timed out.
+	 */
+	bool wait_info_event(const ::std::chrono::nanoseconds& timeout) const;
+
+	/**
+	 * @brief Read a single line status change event from this chip.
+	 * @return New info_event object.
+	 */
+	info_event read_info_event(void) const;
+
+	/**
+	 * @brief Map a GPIO line's name to its offset within the chip.
+	 * @param name Name of the GPIO line to map.
+	 * @return Offset of the line within the chip or -1 if the line with
+	 *         given name is not exposed by this chip.
+	 */
+	int find_line(const ::std::string& name) const;
+
+	/**
+	 * @brief Request a set of lines for exclusive usage.
+	 * @param req_cfg Request config object.
+	 * @param line_cfg Line config object.
+	 * @return New line_request object.
+	 */
+	line_request request_lines(const request_config& req_cfg,
+				   const line_config& line_cfg);
+
+private:
+
+	struct impl;
+
+	::std::unique_ptr<impl> _m_priv;
+};
+
+/**
+ * @brief Stream insertion operator for GPIO chip objects.
+ * @param out Output stream to write to.
+ * @param chip GPIO chip to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const chip& chip);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_CHIP_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/edge-event-buffer.hpp b/bindings/cxx/gpiodcxx/edge-event-buffer.hpp
new file mode 100644
index 0000000..37ac4f5
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/edge-event-buffer.hpp
@@ -0,0 +1,129 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file edge-event-buffer.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_EDGE_EVENT_BUFFER_HPP__
+#define __LIBGPIOD_CXX_EDGE_EVENT_BUFFER_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <cstddef>
+#include <iostream>
+#include <memory>
+#include <vector>
+
+namespace gpiod {
+
+class edge_event;
+class line_request;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Object into which edge events are read for better performance.
+ *
+ * The edge_event_buffer allows reading edge_event objects into an existing
+ * buffer which improves the performance by avoiding needless memory
+ * allocations.
+ */
+class edge_event_buffer
+{
+public:
+
+	/**
+	 * @brief Constant iterator for iterating over edge events stored in
+	 *        the buffer.
+	 */
+	using const_iterator = ::std::vector<edge_event>::const_iterator;
+
+	/**
+	 * @brief Constructor. Creates a new edge event buffer with given
+	 *        capacity.
+	 * @param capacity Capacity of the new buffer.
+	 */
+	explicit edge_event_buffer(::std::size_t capacity = 64);
+
+	edge_event_buffer(const edge_event_buffer& other) = delete;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	edge_event_buffer(edge_event_buffer&& other) noexcept;
+
+	~edge_event_buffer(void);
+
+	edge_event_buffer& operator=(const edge_event_buffer& other) = delete;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 * @return Reference to self.
+	 */
+	edge_event_buffer& operator=(edge_event_buffer&& other) noexcept;
+
+	/**
+	 * @brief Get the constant reference to the edge event at given index.
+	 * @param index Index of the event in the buffer.
+	 * @return Constant reference to the edge event.
+	 */
+	const edge_event& get_event(unsigned int index) const;
+
+	/**
+	 * @brief Get the number of edge events currently stored in the buffer.
+	 * @return Number of edge events in the buffer.
+	 */
+	::std::size_t num_events(void) const;
+
+	/**
+	 * @brief Maximum capacity of the buffer.
+	 * @return Buffer capacity.
+	 */
+	::std::size_t capacity(void) const noexcept;
+
+	/**
+	 * @brief Get a constant iterator to the first edge event currently
+	 *        stored in the buffer.
+	 * @return Constant iterator to the first element.
+	 */
+	const_iterator begin(void) const noexcept;
+
+	/**
+	 * @brief Get a constant iterator to the element after the last edge
+	 *        event in the buffer.
+	 * @return Constant iterator to the element after the last edge event.
+	 */
+	const_iterator end(void) const noexcept;
+
+private:
+
+	struct impl;
+
+	::std::unique_ptr<impl> _m_priv;
+
+	friend line_request;
+};
+
+/**
+ * @brief Stream insertion operator for GPIO edge event buffer objects.
+ * @param out Output stream to write to.
+ * @param buf GPIO edge event buffer object to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const edge_event_buffer& buf);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_EDGE_EVENT_BUFFER_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/edge-event.hpp b/bindings/cxx/gpiodcxx/edge-event.hpp
new file mode 100644
index 0000000..0623880
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/edge-event.hpp
@@ -0,0 +1,137 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file edge-event.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_EDGE_EVENT_HPP__
+#define __LIBGPIOD_CXX_EDGE_EVENT_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <cstdint>
+#include <iostream>
+#include <memory>
+
+#include "timestamp.hpp"
+
+namespace gpiod {
+
+class edge_event_buffer;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Immutable object containing data about a single edge event.
+ */
+class edge_event
+{
+public:
+
+	/**
+	 * @brief Edge event types.
+	 */
+	enum class event_type
+	{
+		RISING_EDGE = 1,
+		/**< This is a rising edge event. */
+		FALLING_EDGE
+		/**< This is falling edge event. */
+	};
+
+	/**
+	 * @brief Copy constructor.
+	 * @param other Object to copy.
+	 */
+	edge_event(const edge_event& other);
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	edge_event(edge_event&& other) noexcept;
+
+	~edge_event(void);
+
+	/**
+	 * @brief Copy assignment operator.
+	 * @param other Object to copy.
+	 * @return Reference to self.
+	 */
+	edge_event& operator=(const edge_event& other);
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 * @return Reference to self.
+	 */
+	edge_event& operator=(edge_event&& other) noexcept;
+
+	/**
+	 * @brief Retrieve the event type.
+	 * @return Event type (rising or falling edge).
+	 */
+	event_type type(void) const;
+
+	/**
+	 * @brief Retrieve the event time-stamp.
+	 * @return Time-stamp in nanoseconds as registered by the kernel using
+	 *         the configured edge event clock.
+	 */
+	timestamp timestamp_ns(void) const noexcept;
+
+	/**
+	 * @brief Read the offset of the line on which this event was
+	 *        registered.
+	 * @return Line offset.
+	 */
+	line::offset line_offset(void) const noexcept;
+
+	/**
+	 * @brief Get the global sequence number of this event.
+	 * @return Sequence number of the event relative to all lines in the
+	 *         associated line request.
+	 */
+	unsigned long global_seqno(void) const noexcept;
+
+	/**
+	 * @brief Get the event sequence number specific to the concerned line.
+	 * @return Sequence number of the event relative to this line within
+	 *         the lifetime of the associated line request.
+	 */
+	unsigned long line_seqno(void) const noexcept;
+
+private:
+
+	edge_event(void);
+
+	struct impl;
+	struct impl_managed;
+	struct impl_external;
+
+	::std::shared_ptr<impl> _m_priv;
+
+	friend edge_event_buffer;
+};
+
+/**
+ * @brief Stream insertion operator for edge events.
+ * @param out Output stream to write to.
+ * @param event Edge event to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const edge_event& event);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_EDGE_EVENT_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/exception.hpp b/bindings/cxx/gpiodcxx/exception.hpp
new file mode 100644
index 0000000..f4d525b
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/exception.hpp
@@ -0,0 +1,158 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file exception.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_EXCEPTION_HPP__
+#define __LIBGPIOD_CXX_EXCEPTION_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <stdexcept>
+#include <string>
+
+namespace gpiod {
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Exception thrown when an already closed chip is used.
+ */
+class GPIOD_CXX_API chip_closed : public ::std::logic_error
+{
+public:
+
+	/**
+	 * @brief Constructor.
+	 * @param what Human readable reason for error.
+	 */
+	explicit chip_closed(const ::std::string& what);
+
+	/**
+	 * @brief Copy constructor.
+	 * @param other Object to copy from.
+	 */
+	chip_closed(const chip_closed& other) noexcept;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	chip_closed(chip_closed&& other) noexcept;
+
+	/**
+	 * @brief Assignment operator.
+	 * @param other Object to copy from.
+	 * @return Reference to self.
+	 */
+	chip_closed& operator=(const chip_closed& other) noexcept;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 * @return Reference to self.
+	 */
+	chip_closed& operator=(chip_closed&& other) noexcept;
+
+	virtual ~chip_closed(void);
+};
+
+/**
+ * @brief Exception thrown when an already released line request is used.
+ */
+class GPIOD_CXX_API request_released : public ::std::logic_error
+{
+public:
+
+	/**
+	 * @brief Constructor.
+	 * @param what Human readable reason for error.
+	 */
+	explicit request_released(const ::std::string& what);
+
+	/**
+	 * @brief Copy constructor.
+	 * @param other Object to copy from.
+	 */
+	request_released(const request_released& other) noexcept;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	request_released(request_released&& other) noexcept;
+
+	/**
+	 * @brief Assignment operator.
+	 * @param other Object to copy from.
+	 * @return Reference to self.
+	 */
+	request_released& operator=(const request_released& other) noexcept;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 * @return Reference to self.
+	 */
+	request_released& operator=(request_released&& other) noexcept;
+
+	virtual ~request_released(void);
+};
+
+/**
+ * @brief Exception thrown when the core C library returns an invalid value
+ *        for any of the line_info properties.
+ */
+class GPIOD_CXX_API bad_mapping : public ::std::runtime_error
+{
+public:
+
+	/**
+	 * @brief Constructor.
+	 * @param what Human readable reason for error.
+	 */
+	explicit bad_mapping(const ::std::string& what);
+
+	/**
+	 * @brief Copy constructor.
+	 * @param other Object to copy from.
+	 */
+	bad_mapping(const bad_mapping& other) noexcept;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	bad_mapping(bad_mapping&& other) noexcept;
+
+	/**
+	 * @brief Assignment operator.
+	 * @param other Object to copy from.
+	 * @return Reference to self.
+	 */
+	bad_mapping& operator=(const bad_mapping& other) noexcept;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 * @return Reference to self.
+	 */
+	bad_mapping& operator=(bad_mapping&& other) noexcept;
+
+	virtual ~bad_mapping(void);
+};
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_EXCEPTION_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/info-event.hpp b/bindings/cxx/gpiodcxx/info-event.hpp
new file mode 100644
index 0000000..107ca57
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/info-event.hpp
@@ -0,0 +1,123 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file gpiod.h
+ */
+
+#ifndef __LIBGPIOD_CXX_INFO_EVENT_HPP__
+#define __LIBGPIOD_CXX_INFO_EVENT_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <cstdint>
+#include <iostream>
+#include <memory>
+
+#include "timestamp.hpp"
+
+namespace gpiod {
+
+class chip;
+class line_info;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Immutable object containing data about a single line info event.
+ */
+class info_event
+{
+public:
+
+	/**
+	 * @brief Types of info events.
+	 */
+	enum class event_type
+	{
+		LINE_REQUESTED = 1,
+		/**< Line has been requested. */
+		LINE_RELEASED,
+		/**< Previously requested line has been released. */
+		LINE_CONFIG_CHANGED
+		/**< Line configuration has changed. */
+	};
+
+	/**
+	 * @brief Copy constructor.
+	 * @param other Object to copy.
+	 */
+	info_event(const info_event& other);
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	info_event(info_event&& other) noexcept;
+
+	~info_event(void);
+
+	/**
+	 * @brief Copy assignment operator.
+	 * @param other Object to copy.
+	 * @return Reference to self.
+	 */
+	info_event& operator=(const info_event& other);
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 * @return Reference to self.
+	 */
+	info_event& operator=(info_event&& other) noexcept;
+
+	/**
+	 * @brief Type of this event.
+	 * @return Event type.
+	 */
+	event_type type(void) const;
+
+	/**
+	 * @brief Timestamp of the event as returned by the kernel.
+	 * @return Timestamp as a 64-bit unsigned integer.
+	 */
+	::std::uint64_t timestamp_ns(void) const noexcept;
+
+	/**
+	 * @brief Get the new line information.
+	 * @return Constant reference to the line info object containing the
+	 *         line data as read at the time of the info event.
+	 */
+	const line_info& get_line_info(void) const noexcept;
+
+private:
+
+	info_event(void);
+
+	struct impl;
+
+	::std::shared_ptr<impl> _m_priv;
+
+	friend chip;
+};
+
+/**
+ * @brief Stream insertion operator for info events.
+ * @param out Output stream to write to.
+ * @param event GPIO line info event to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const info_event& event);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_INFO_EVENT_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/line-config.hpp b/bindings/cxx/gpiodcxx/line-config.hpp
new file mode 100644
index 0000000..6d808bd
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/line-config.hpp
@@ -0,0 +1,564 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file line-config.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_LINE_CONFIG_HPP__
+#define __LIBGPIOD_CXX_LINE_CONFIG_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <any>
+#include <chrono>
+#include <cstddef>
+#include <iostream>
+#include <map>
+#include <memory>
+#include <utility>
+
+namespace gpiod {
+
+class chip;
+class line_request;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Contains a set of line config options used in line requests and
+ *        reconfiguration.
+ */
+class line_config
+{
+public:
+
+	/**
+	 * @brief List of available configuration properties. Used in the
+	 *        constructor, :line_config::set_property_default and
+	 *        :line_config::set_property_override.
+	 */
+	enum class property {
+		DIRECTION = 1,
+		/**< Line direction. */
+		EDGE,
+		/**< Edge detection. */
+		BIAS,
+		/**< Bias. */
+		DRIVE,
+		/**< Drive. */
+		ACTIVE_LOW,
+		/**< Active-low setting. */
+		DEBOUNCE_PERIOD,
+		/**< Debounce period. */
+		EVENT_CLOCK,
+		/**< Event clock. */
+		OUTPUT_VALUE,
+		/**< Output value. */
+		OUTPUT_VALUES,
+		/**< Set of offset-to-value mappings. Only used in the constructor. */
+	};
+
+	/**
+	 * @brief List of configuration properties passed to the constructor.
+	 *        The first member is the property indicator, the second is
+	 *        the value stored as `std::any` that is interpreted by the
+	 *        relevant methods depending on the property value.
+	 */
+	using properties = ::std::map<property, ::std::any>;
+
+	/**
+	 * @brief Stored information about a single configuration override. The
+	 *        first member is the overridden line offset, the second is
+	 *        the property being overridden.
+	 */
+	using override = ::std::pair<line::offset, property>;
+
+	/**
+	 * @brief List of line configuration overrides.
+	 */
+	using override_list = ::std::vector<override>;
+
+	/**
+	 * @brief Constructor.
+	 * @param props List of configuration properties. See
+	 *              :set_property_default for details. Additionally the
+	 *              constructor takes another property type as argument:
+	 *              :property::OUTPUT_VALUES which takes
+	 *              :line::value_mappings as property value. This
+	 *              effectively sets the overrides for output values for
+	 *              the mapped offsets.
+	 */
+	explicit line_config(const properties& props = properties());
+
+	line_config(const line_config& other) = delete;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	line_config(line_config&& other) noexcept;
+
+	~line_config(void);
+
+	line_config& operator=(const line_config& other) = delete;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 * @return Reference to self.
+	 */
+	line_config& operator=(line_config&& other) noexcept;
+
+	/**
+	 * @brief Reset the line config object.
+	 */
+	void reset(void) noexcept;
+
+	/**
+	 * @brief Set the default value of a single configuration property.
+	 * @param prop Property to set.
+	 * @param val Property value. The type must correspond with the
+	 *            property being set: :line::direction for
+	 *            :property::DIRECTION, :line::edge for :property::EDGE,
+	 *            :line::bias for :property::BIAS, :line::drive for
+	 *            :property::DRIVE, `bool` for :property::ACTIVE_LOW,
+	 *            `std::chrono:microseconds` for
+	 *            :property::DEBOUNCE_PERIOD, :line::clock for
+	 *            :property::EVENT_CLOCK and :line::value
+	 *            for :property::OUTPUT_VALUE.
+	 *
+	 */
+	void set_property_default(property prop, const ::std::any& val);
+
+	/**
+	 * @brief Set the override value of a single configuration property.
+	 * @param prop Property to set.
+	 * @param offset Line offset to override.
+	 * @param val Property value. See :set_property_default for details.
+	 */
+	void set_property_offset(property prop, line::offset offset, const ::std::any& val);
+
+	/**
+	 * @brief Set the default direction setting.
+	 * @param direction New direction.
+	 */
+	void set_direction_default(line::direction direction);
+
+	/**
+	 * @brief Set the direction for a single line at given offset.
+	 * @param direction New direction.
+	 * @param offset Offset of the line for which to set the direction.
+	 */
+	void set_direction_override(line::direction direction, line::offset offset);
+
+	/**
+	 * @brief Get the default direction setting.
+	 * @return Direction setting that would have been used for any offset
+	 * 	   not assigned its own direction value.
+	 */
+	line::direction direction_default(void) const;
+
+	/**
+	 * @brief Get the direction setting for a given offset.
+	 * @param offset Line offset for which to read the direction setting.
+	 * @return Direction setting that would have been used for given offset
+	 *         if the config object was used in a request at the time of
+	 *         the call.
+	 */
+	line::direction direction_offset(line::offset offset) const;
+
+	/**
+	 * @brief Clear the direction override at given offset.
+	 * @param offset Offset of the line for which to clear the override.
+	 * @note Does nothing if no override is set for this line.
+	 */
+	void clear_direction_override(line::offset offset) noexcept;
+
+	/**
+	 * @brief Check if the direction setting is overridden at given offset.
+	 * @param offset Offset of the line for which to check the override.
+	 * @return True if direction is overridden at this offset, false
+	 *         otherwise.
+	 */
+	bool direction_is_overridden(line::offset offset) const noexcept;
+
+	/**
+	 * @brief Set the default edge event detection.
+	 * @param edge Type of edge events to detect.
+	 */
+	void set_edge_detection_default(line::edge edge);
+
+	/**
+	 * @brief Set the edge event detection for a single line at given
+	 *        offset.
+	 * @param edge Type of edge events to detect.
+	 * @param offset Offset of the line for which to set the direction.
+	 */
+	void set_edge_detection_override(line::edge edge, line::offset offset);
+
+	/**
+	 * @brief Get the default edge detection setting.
+	 * @return Edge detection setting that would have been used for any
+	 *         offset not assigned its own direction value.
+	 */
+	line::edge edge_detection_default(void) const;
+
+	/**
+	 * @brief Get the edge event detection setting for a given offset.
+	 * @param offset Line offset for which to read the edge detection
+	 *               setting.
+	 * @return Edge event detection setting that would have been used for
+	 * 	   given offset if the config object was used in a request at
+	 * 	   the time of the call.
+	 */
+	line::edge edge_detection_offset(line::offset offset) const;
+
+	/**
+	 * @brief Clear the edge detection override at given offset.
+	 * @param offset Offset of the line for which to clear the override.
+	 * @note Does nothing if no override is set for this line.
+	 */
+	void clear_edge_detection_override(line::offset offset) noexcept;
+
+	/**
+	 * @brief Check if the edge detection setting is overridden at given
+	 *        offset.
+	 * @param offset Offset of the line for which to check the override.
+	 * @return True if edge detection is overridden at this offset, false
+	 *         otherwise.
+	 */
+	bool edge_detection_is_overridden(line::offset offset) const noexcept;
+
+	/**
+	 * @brief Set the default bias setting.
+	 * @param bias New bias.
+	 */
+	void set_bias_default(line::bias bias);
+
+	/**
+	 * @brief Set the bias for a single line at given offset.
+	 * @param bias New bias.
+	 * @param offset Offset of the line for which to set the bias.
+	 */
+	void set_bias_override(line::bias bias, line::offset offset);
+
+	/**
+	 * @brief Get the default bias setting.
+	 * @return Bias setting that would have been used for any offset not
+	 *         assigned its own direction value.
+	 */
+	line::bias bias_default(void) const;
+
+	/**
+	 * @brief Get the bias setting for a given offset.
+	 * @param offset Line offset for which to read the bias setting.
+	 * @return Bias setting that would have been used for given offset if
+	 *         the config object was used in a request at the time of the
+	 *         call.
+	 */
+	line::bias bias_offset(line::offset offset) const;
+
+	/**
+	 * @brief Clear the bias override at given offset.
+	 * @param offset Offset of the line for which to clear the override.
+	 * @note Does nothing if no override is set for this line.
+	 */
+	void clear_bias_override(line::offset offset) noexcept;
+
+	/**
+	 * @brief Check if the bias setting is overridden at given offset.
+	 * @param offset Offset of the line for which to check the override.
+	 * @return True if bias is overridden at this offset, false otherwise.
+	 */
+	bool bias_is_overridden(line::offset offset) const noexcept;
+
+	/**
+	 * @brief Set the default drive setting.
+	 * @param drive New drive.
+	 */
+	void set_drive_default(line::drive drive);
+
+	/**
+	 * @brief Set the drive for a single line at given offset.
+	 * @param drive New drive.
+	 * @param offset Offset of the line for which to set the drive.
+	 */
+	void set_drive_override(line::drive drive, line::offset offset);
+
+	/**
+	 * @brief Set the drive for a subset of offsets.
+	 * @param drive New drive.
+	 * @param offsets Vector of line offsets for which to set the drive.
+	 */
+	void set_drive(line::drive drive, const line::offsets& offsets);
+
+	/**
+	 * @brief Get the default drive setting.
+	 * @return Drive setting that would have been used for any offset not
+	 *         assigned its own direction value.
+	 */
+	line::drive drive_default(void) const;
+
+	/**
+	 * @brief Get the drive setting for a given offset.
+	 * @param offset Line offset for which to read the drive setting.
+	 * @return Drive setting that would have been used for given offset if
+	 *         the config object was used in a request at the time of the
+	 *         call.
+	 */
+	line::drive drive_offset(line::offset offset) const;
+
+	/**
+	 * @brief Clear the drive override at given offset.
+	 * @param offset Offset of the line for which to clear the override.
+	 * @note Does nothing if no override is set for this line.
+	 */
+	void clear_drive_override(line::offset offset) noexcept;
+
+	/**
+	 * @brief Check if the drive setting is overridden at given offset.
+	 * @param offset Offset of the line for which to check the override.
+	 * @return True if drive is overridden at this offset, false otherwise.
+	 */
+	bool drive_is_overridden(line::offset offset) const noexcept;
+
+	/**
+	 * @brief Set lines to active-low by default.
+	 * @param active_low New active-low setting.
+	 */
+	void set_active_low_default(bool active_low) noexcept;
+
+	/**
+	 * @brief Set a single line as active-low.
+	 * @param active_low New active-low setting.
+	 * @param offset Offset of the line for which to set the active setting.
+	 */
+	void set_active_low_override(bool active_low, line::offset offset) noexcept;
+
+	/**
+	 * @brief Check if active-low is the default setting.
+	 * @return Active-low setting that would have been used for any offset
+         *         not assigned its own value.
+	 */
+	bool active_low_default(void) const noexcept;
+
+	/**
+	 * @brief Check if the line at given offset was configured as
+	 *        active-low.
+	 * @param offset Line offset for which to read the active-low setting.
+	 * @return Active-low setting that would have been used for given
+	 *         offset if the config object was used in a request at the
+	 *         time of the call.
+	 */
+	bool active_low_offset(line::offset offset) const noexcept;
+
+	/**
+	 * @brief Clear the active-low override at given offset.
+	 * @param offset Offset of the line for which to clear the override.
+	 * @note Does nothing if no override is set for this line.
+	 */
+	void clear_active_low_override(line::offset offset) noexcept;
+
+	/**
+	 * @brief Check if the active-low setting is overridden at given offset.
+	 * @param offset Offset of the line for which to check the override.
+	 * @return True if active-low is overridden at this offset, false
+	 *         otherwise.
+	 */
+	bool active_low_is_overridden(line::offset offset) const noexcept;
+
+	/**
+	 * @brief Set the default debounce period.
+	 * @param period New debounce period. Disables debouncing if 0.
+	 */
+	void set_debounce_period_default(const ::std::chrono::microseconds& period) noexcept;
+
+	/**
+	 * @brief Set the debounce period for a single line at given offset.
+	 * @param period New debounce period. Disables debouncing if 0.
+	 * @param offset Offset of the line for which to set the debounce
+	 *               period.
+	 */
+	void set_debounce_period_override(const ::std::chrono::microseconds& period,
+					     line::offset offset) noexcept;
+
+	/**
+	 * @brief Get the default debounce period.
+	 * @return Debounce period that would have been used for any offset not
+	 *         assigned its own debounce period. 0 if not debouncing is
+	 *         disabled.
+	 */
+	::std::chrono::microseconds debounce_period_default(void) const noexcept;
+
+	/**
+	 * @brief Get the debounce period for a given offset.
+	 * @param offset Line offset for which to read the debounce period.
+	 * @return Debounce period that would have been used for given offset
+	 *         if the config object was used in a request at the time of
+	 *         the call. 0 if debouncing is disabled.
+	 */
+	::std::chrono::microseconds debounce_period_offset(line::offset offset) const noexcept;
+
+	/**
+	 * @brief Clear the debounce period override at given offset.
+	 * @param offset Offset of the line for which to clear the override.
+	 * @note Does nothing if no override is set for this line.
+	 */
+	void clear_debounce_period_override(line::offset offset) noexcept;
+
+	/**
+	 * @brief Check if the debounce period setting is overridden at given offset.
+	 * @param offset Offset of the line for which to check the override.
+	 * @return True if debounce period is overridden at this offset, false
+	 *         otherwise.
+	 */
+	bool debounce_period_is_overridden(line::offset offset) const noexcept;
+
+	/**
+	 * @brief Set the default event timestamp clock.
+	 * @param clock New clock to use.
+	 */
+	void set_event_clock_default(line::clock clock);
+
+	/**
+	 * @brief Set the event clock for a single line at given offset.
+	 * @param clock New clock to use.
+	 * @param offset Offset of the line for which to set the event clock
+	 *               type.
+	 */
+	void set_event_clock_override(line::clock clock, line::offset offset);
+
+	/**
+	 * @brief Get the default event clock setting.
+	 * @return Event clock setting that would have been used for any offset
+	 *         not assigned its own direction value.
+	 */
+	line::clock event_clock_default(void) const;
+
+	/**
+	 * @brief Get the event clock setting for a given offset.
+	 * @param offset Line offset for which to read the event clock setting.
+	 * @return Event clock setting that would have been used for given
+	 *         offset if the config object was used in a request at the
+	 *         time of the call.
+	 */
+	line::clock event_clock_offset(line::offset offset) const;
+
+	/**
+	 * @brief Clear the event clock override at given offset.
+	 * @param offset Offset of the line for which to clear the override.
+	 * @note Does nothing if no override is set for this line.
+	 */
+	void clear_event_clock_override(line::offset offset) noexcept;
+
+	/**
+	 * @brief Check if the event clock setting is overridden at given
+	 *        offset.
+	 * @param offset Offset of the line for which to check the override.
+	 * @return True if event clock is overridden at this offset, false
+	 *         otherwise.
+	 */
+	bool event_clock_is_overridden(line::offset offset) const noexcept;
+
+	/**
+	 * @brief Set the default output value.
+	 * @param value New value.
+	 */
+	void set_output_value_default(line::value value) noexcept;
+
+	/**
+	 * @brief Set the output value for a single offset.
+	 * @param offset Line offset to associate the value with.
+	 * @param value New value.
+	 */
+	void set_output_value_override(line::value value, line::offset offset) noexcept;
+
+	/**
+	 * @brief Set the output values for a set of line offsets.
+	 * @param values Vector of offset->value mappings.
+	 */
+	void set_output_values(const line::value_mappings& values);
+
+	/**
+	 * @brief Set the output values for a set of line offsets.
+	 * @param offsets Vector of line offsets for which to set output values.
+	 * @param values Vector of new line values with indexes of values
+	 *               corresponding to the indexes of offsets.
+	 */
+	void set_output_values(const line::offsets& offsets, const line::values& values);
+
+	/**
+	 * @brief Get the default output value.
+	 * @return Output value that would have been used for any offset not
+	 *         assigned its own output value.
+	 */
+	line::value output_value_default(void) const noexcept;
+
+	/**
+	 * @brief Get the output value configured for a given line.
+	 * @param offset Line offset for which to read the value.
+	 * @return Output value that would have been used for given offset if
+	 *         the config object was used in a request at the time of the
+	 *         call.
+	 */
+	line::value output_value_offset(line::offset offset) const noexcept;
+
+	/**
+	 * @brief Clear the output value override at given offset.
+	 * @param offset Offset of the line for which to clear the override.
+	 * @note Does nothing if no override is set for this line.
+	 */
+	void clear_output_value_override(line::offset offset) noexcept;
+
+	/**
+	 * @brief Check if the output value setting is overridden at given
+	 *        offset.
+	 * @param offset Offset of the line for which to check the override.
+	 * @return True if output value is overridden at this offset, false
+	 *         otherwise.
+	 */
+	bool output_value_is_overridden(line::offset offset) const noexcept;
+
+	/**
+	 * @brief Get the number of configuration overrides.
+	 * @return Number of overrides held by this object.
+	 */
+	::std::size_t num_overrides(void) const noexcept;
+
+	/**
+	 * @brief Get the list of property overrides.
+	 * @return List of configuration property overrides held by this object.
+	 */
+	override_list overrides(void) const;
+
+private:
+
+	struct impl;
+
+	::std::unique_ptr<impl> _m_priv;
+
+	friend chip;
+	friend line_request;
+};
+
+/**
+ * @brief Stream insertion operator for GPIO line config objects.
+ * @param out Output stream to write to.
+ * @param config Line config object to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const line_config& config);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_LINE_CONFIG_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/line-info.hpp b/bindings/cxx/gpiodcxx/line-info.hpp
new file mode 100644
index 0000000..e9883ab
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/line-info.hpp
@@ -0,0 +1,176 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file line-info.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_LINE_INFO_HPP__
+#define __LIBGPIOD_CXX_LINE_INFO_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <chrono>
+#include <iostream>
+#include <memory>
+#include <string>
+
+namespace gpiod {
+
+class chip;
+class info_event;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Contains an immutable snapshot of the line's state at the
+ *        time when the object of this class was instantiated.
+ */
+class line_info
+{
+public:
+
+	/**
+	 * @brief Copy constructor.
+	 * @param other Object to copy.
+	 */
+	line_info(const line_info& other) noexcept;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	line_info(line_info&& other) noexcept;
+
+	~line_info(void);
+
+	/**
+	 * @brief Copy assignment operator.
+	 * @param other Object to copy.
+	 * @return Reference to self.
+	 */
+	line_info& operator=(const line_info& other) noexcept;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 * @return Reference to self.
+	 */
+	line_info& operator=(line_info&& other) noexcept;
+
+	/**
+	 * @brief Get the hardware offset of the line.
+	 * @return Offset of the line within the parent chip.
+	 */
+	line::offset offset(void) const noexcept;
+
+	/**
+	 * @brief Get the GPIO line name.
+	 * @return Name of the GPIO line as it is represented in the kernel.
+	 *         This routine returns an empty string if the line is unnamed.
+	 */
+	::std::string name(void) const noexcept;
+
+	/**
+	 * @brief Check if the line is currently in use.
+	 * @return True if the line is in use, false otherwise.
+	 *
+	 * The user space can't know exactly why a line is busy. It may have
+	 * been requested by another process or hogged by the kernel. It only
+	 * matters that the line is used and we can't request it.
+	 */
+	bool used(void) const noexcept;
+
+	/**
+	 * @brief Read the GPIO line consumer name.
+	 * @return Name of the GPIO consumer name as it is represented in the
+	 *         kernel. This routine returns an empty string if the line is
+	 *         not used.
+	 */
+	::std::string consumer(void) const noexcept;
+
+	/**
+	 * @brief Read the GPIO line direction setting.
+	 * @return Returns DIRECTION_INPUT or DIRECTION_OUTPUT.
+	 */
+	line::direction direction(void) const;
+
+	/**
+	 * @brief Read the current edge detection setting of this line.
+	 * @return Returns EDGE_NONE, EDGE_RISING, EDGE_FALLING or EDGE_BOTH.
+	 */
+	line::edge edge_detection(void) const;
+
+	/**
+	 * @brief Read the GPIO line bias setting.
+	 * @return Returns BIAS_PULL_UP, BIAS_PULL_DOWN, BIAS_DISABLE or
+	 *         BIAS_UNKNOWN.
+	 */
+	line::bias bias(void) const;
+
+	/**
+	 * @brief Read the GPIO line drive setting.
+	 * @return Returns DRIVE_PUSH_PULL, DRIVE_OPEN_DRAIN or
+	 *         DRIVE_OPEN_SOURCE.
+	 */
+	line::drive drive(void) const;
+
+	/**
+	 * @brief Check if the signal of this line is inverted.
+	 * @return True if this line is "active-low", false otherwise.
+	 */
+	bool active_low(void) const noexcept;
+
+	/**
+	 * @brief Check if this line is debounced (either by hardware or by the
+	 *        kernel software debouncer).
+	 * @return True if the line is debounced, false otherwise.
+	 */
+	bool debounced(void) const noexcept;
+
+	/**
+	 * @brief Read the current debounce period in microseconds.
+	 * @return Current debounce period in microseconds, 0 if the line is
+	 *         not debounced.
+	 */
+	::std::chrono::microseconds debounce_period(void) const noexcept;
+
+	/**
+	 * @brief Read the current event clock setting used for edge event
+	 *        timestamps.
+	 * @return Returns MONOTONIC or REALTIME.
+	 */
+	line::clock event_clock(void) const;
+
+private:
+
+	line_info(void);
+
+	struct impl;
+
+	::std::shared_ptr<impl> _m_priv;
+
+	friend chip;
+	friend info_event;
+};
+
+/**
+ * @brief Stream insertion operator for GPIO line info objects.
+ * @param out Output stream to write to.
+ * @param info GPIO line info object to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const line_info& info);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_LINE_INFO_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/line-request.hpp b/bindings/cxx/gpiodcxx/line-request.hpp
new file mode 100644
index 0000000..28ab6e1
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/line-request.hpp
@@ -0,0 +1,221 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file line-request.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_LINE_REQUEST_HPP__
+#define __LIBGPIOD_CXX_LINE_REQUEST_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <chrono>
+#include <cstddef>
+#include <iostream>
+#include <memory>
+
+#include "misc.hpp"
+
+namespace gpiod {
+
+class chip;
+class edge_event;
+class edge_event_buffer;
+class line_config;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Stores the context of a set of requested GPIO lines.
+ */
+class line_request
+{
+public:
+
+	line_request(const line_request& other) = delete;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	line_request(line_request&& other) noexcept;
+
+	~line_request(void);
+
+	line_request& operator=(const line_request& other) = delete;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 */
+	line_request& operator=(line_request&& other) noexcept;
+
+	/**
+	 * @brief Check if this object is valid.
+	 * @return True if this object's methods can be used, false otherwise.
+	 *         False usually means the request was released. If the user
+	 *         calls any of the methods of this class on an object for
+	 *         which this operator returned false, a logic_error will be
+	 *         thrown.
+	 */
+	explicit operator bool(void) const noexcept;
+
+	/**
+	 * @brief Release the GPIO chip and free all associated resources.
+	 * @note The object can still be used after this method is called but
+	 *       using any of the mutators will result in throwing
+	 *       a logic_error exception.
+	 */
+	void release(void);
+
+	/**
+	 * @brief Get the number of requested lines.
+	 * @return Number of lines in this request.
+	 */
+	::std::size_t num_lines(void) const;
+
+	/**
+	 * @brief Get the list of offsets of requested lines.
+	 * @return List of hardware offsets of the lines in this request.
+	 */
+	line::offsets offsets(void) const;
+
+	/**
+	 * @brief Get the value of a single requested line.
+	 * @param offset Offset of the line to read within the chip.
+	 * @return Current line value.
+	 */
+	line::value get_value(line::offset offset);
+
+	/**
+	 * @brief Get the values of a subset of requested lines.
+	 * @param offsets Vector of line offsets
+	 * @return Vector of lines values with indexes of values corresponding
+	 *         to those of the offsets.
+	 */
+	line::values get_values(const line::offsets& offsets);
+
+	/**
+	 * @brief Get the values of all requested lines.
+	 * @return List of read values.
+	 */
+	line::values get_values(void);
+
+	/**
+	 * @brief Get the values of a subset of requested lines into a vector
+	 *        supplied by the caller.
+	 * @param offsets Vector of line offsets.
+	 * @param values Vector for storing the values. Its size must be at
+	 *               least that of the offsets vector. The indexes of read
+	 *               values will correspond with those in the offsets
+	 *               vector.
+	 */
+	void get_values(const line::offsets& offsets, line::values& values);
+
+	/**
+	 * @brief Get the values of all requested lines.
+	 * @param values Array in which the values will be stored. Must hold
+	 *               at least the number of elements returned by
+	 *               line_request::num_lines.
+	 */
+	void get_values(line::values& values);
+
+	/**
+	 * @brief Set the value of a single requested line.
+	 * @param offset Offset of the line to set within the chip.
+	 * @param value New line value.
+	 */
+	void set_value(line::offset offset, line::value value);
+
+	/**
+	 * @brief Set the values of a subset of requested lines.
+	 * @param values Vector containing a set of offset->value mappings.
+	 */
+	void set_values(const line::value_mappings& values);
+
+	/**
+	 * @brief Set the values of a subset of requested lines.
+	 * @param offsets Vector containing the offsets of lines to set.
+	 * @param values Vector containing new values with indexes
+	 *               corresponding with those in the offsets vector.
+	 */
+	void set_values(const line::offsets& offsets, const line::values& values);
+
+	/**
+	 * @brief Set the values of all requested lines.
+	 * @param values Array of new line values. The size must be equal to
+	 *               the value returned by line_request::num_lines.
+	 */
+	void set_values(const line::values& values);
+
+	/**
+	 * @brief Apply new config options to requested lines.
+	 * @param config New configuration.
+	 */
+	void reconfigure_lines(const line_config& config);
+
+	/**
+	 * @brief Get the file descriptor number associated with this line
+	 *        request.
+	 * @return File descriptor number.
+	 */
+	int fd(void) const;
+
+	/**
+	 * @brief Wait for edge events on any of the lines requested with edge
+	 *        detection enabled.
+	 * @param timeout Wait time limit in nanoseconds.
+	 * @return True if at least one event is ready to be read. False if the
+	 *         wait timed out.
+	 */
+	bool wait_edge_event(const ::std::chrono::nanoseconds& timeout) const;
+
+	/**
+	 * @brief Read a number of edge events from this request up to the
+	 *        maximum capacity of the buffer.
+	 * @param buffer Edge event buffer to read events into.
+	 * @return Number of events read.
+	 */
+	::std::size_t read_edge_event(edge_event_buffer& buffer);
+
+	/**
+	 * @brief Read a number of edge events from this request.
+	 * @param buffer Edge event buffer to read events into.
+	 * @param max_events Maximum number of events to read. Limited by the
+	 *                   capacity of the buffer.
+	 * @return Number of events read.
+	 */
+	::std::size_t read_edge_event(edge_event_buffer& buffer, ::std::size_t max_events);
+
+private:
+
+	line_request(void);
+
+	struct impl;
+
+	::std::unique_ptr<impl> _m_priv;
+
+	friend chip;
+};
+
+/**
+ * @brief Stream insertion operator for line requests.
+ * @param out Output stream to write to.
+ * @param request Line request object to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const line_request& request);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_LINE_REQUEST_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/line.hpp b/bindings/cxx/gpiodcxx/line.hpp
new file mode 100644
index 0000000..8e8a984
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/line.hpp
@@ -0,0 +1,274 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file line.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_LINE_HPP__
+#define __LIBGPIOD_CXX_LINE_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <ostream>
+#include <utility>
+#include <vector>
+
+namespace gpiod {
+
+/**
+ * @brief Namespace containing various type definitions for GPIO lines.
+ */
+namespace line {
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Wrapper around unsigned int for representing line offsets.
+ */
+class offset
+{
+public:
+	/**
+	 * @brief Constructor with implicit conversion from unsigned int.
+	 * @param off Line offset.
+	 */
+	offset(unsigned int off = 0) : _m_offset(off) {	}
+
+	/**
+	 * @brief Copy constructor.
+	 * @param other Object to copy.
+	 */
+	offset(const offset& other) = default;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	offset(offset&& other) = default;
+
+	~offset(void) = default;
+
+	/**
+	 * @brief Assignment operator.
+	 * @param other Object to copy.
+	 * @return Reference to self.
+	 */
+	offset& operator=(const offset& other) = default;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 * @return Reference to self.
+	 */
+	offset& operator=(offset&& other) noexcept = default;
+
+	/**
+	 * @brief Conversion operator to `unsigned int`.
+	 */
+	operator unsigned int(void) const noexcept
+	{
+		return this->_m_offset;
+	}
+
+private:
+	unsigned int _m_offset;
+};
+
+/**
+ * @brief Logical line states.
+ */
+enum class value
+{
+	INACTIVE = 0,
+	/**< Line is inactive. */
+	ACTIVE = 1,
+	/**< Line is active. */
+};
+
+/**
+ * @brief Direction settings.
+ */
+enum class direction
+{
+	AS_IS = 1,
+	/**< Request the line(s), but don't change current direction. */
+	INPUT,
+	/**< Direction is input - we're reading the state of a GPIO line. */
+	OUTPUT
+	/**< Direction is output - we're driving the GPIO line. */
+};
+
+/**
+ * @brief Edge detection settings.
+ */
+enum class edge
+{
+	NONE = 1,
+	/**< Line edge detection is disabled. */
+	RISING,
+	/**< Line detects rising edge events. */
+	FALLING,
+	/**< Line detect falling edge events. */
+	BOTH
+	/**< Line detects both rising and falling edge events. */
+};
+
+/**
+ * @brief Internal bias settings.
+ */
+enum class bias
+{
+	AS_IS = 1,
+	/**< Don't change the bias setting when applying line config. */
+	UNKNOWN,
+	/**< The internal bias state is unknown. */
+	DISABLED,
+	/**< The internal bias is disabled. */
+	PULL_UP,
+	/**< The internal pull-up bias is enabled. */
+	PULL_DOWN
+	/**< The internal pull-down bias is enabled. */
+};
+
+/**
+ * @brief Drive settings.
+ */
+enum class drive
+{
+	PUSH_PULL = 1,
+	/**< Drive setting is push-pull. */
+	OPEN_DRAIN,
+	/**< Line output is open-drain. */
+	OPEN_SOURCE
+	/**< Line output is open-source. */
+};
+
+/**
+ * @brief Event clock settings.
+ */
+enum class clock
+{
+	MONOTONIC = 1,
+	/**< Line uses the monotonic clock for edge event timestamps. */
+	REALTIME
+	/**< Line uses the realtime clock for edge event timestamps. */
+};
+
+/**
+ * @brief Vector of line offsets.
+ */
+using offsets = ::std::vector<offset>;
+
+/**
+ * @brief Vector of line values.
+ */
+using values = ::std::vector<value>;
+
+/**
+ * @brief Represents a mapping of a line offset to line logical state.
+ */
+using value_mapping = ::std::pair<offset, value>;
+
+/**
+ * @brief Vector of offset->value mappings. Each mapping is defined as a pair
+ *        of an unsigned and signed integers.
+ */
+using value_mappings = ::std::vector<value_mapping>;
+
+/**
+ * @brief Stream insertion operator for logical line values.
+ * @param out Output stream.
+ * @param val Value to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, value val);
+
+/**
+ * @brief Stream insertion operator for direction values.
+ * @param out Output stream.
+ * @param dir Value to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, direction dir);
+
+/**
+ * @brief Stream insertion operator for edge detection values.
+ * @param out Output stream.
+ * @param edge Value to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, edge edge);
+
+/**
+ * @brief Stream insertion operator for bias values.
+ * @param out Output stream.
+ * @param bias Value to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, bias bias);
+
+/**
+ * @brief Stream insertion operator for drive values.
+ * @param out Output stream.
+ * @param drive Value to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, drive drive);
+
+/**
+ * @brief Stream insertion operator for event clock values.
+ * @param out Output stream.
+ * @param clock Value to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, clock clock);
+
+/**
+ * @brief Stream insertion operator for the list of output values.
+ * @param out Output stream.
+ * @param vals Object to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const values& vals);
+
+/**
+ * @brief Stream insertion operator for the list of line offsets.
+ * @param out Output stream.
+ * @param offs Object to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const offsets& offs);
+
+/**
+ * @brief Stream insertion operator for the offset-to-value mapping.
+ * @param out Output stream.
+ * @param mapping Value to insert into the output stream in a human-readable
+ *        form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const value_mapping& mapping);
+
+/**
+ * @brief Stream insertion operator for the list of offset-to-value mappings.
+ * @param out Output stream.
+ * @param mappings Object to insert into the output stream in a human-readable
+ *        form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const value_mappings& mappings);
+
+/**
+ * @}
+ */
+
+} /* namespace line */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_LINE_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/misc.hpp b/bindings/cxx/gpiodcxx/misc.hpp
new file mode 100644
index 0000000..6e0084b
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/misc.hpp
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file misc.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_MISC_HPP__
+#define __LIBGPIOD_CXX_MISC_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <string>
+
+namespace gpiod {
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Check if the file pointed to by path is a GPIO chip character device.
+ * @param path Path to check.
+ * @return True if the file exists and is a GPIO chip character device or a
+ *         symbolic link to it.
+ */
+bool is_gpiochip_device(const ::std::filesystem::path& path);
+
+/**
+ * @brief Get the human readable version string for libgpiod API
+ * @return String containing the library version.
+ */
+const ::std::string& version_string(void);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_MISC_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/request-config.hpp b/bindings/cxx/gpiodcxx/request-config.hpp
new file mode 100644
index 0000000..52444c9
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/request-config.hpp
@@ -0,0 +1,163 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file request-config.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_REQUEST_CONFIG_HPP__
+#define __LIBGPIOD_CXX_REQUEST_CONFIG_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <any>
+#include <cstddef>
+#include <iostream>
+#include <map>
+#include <memory>
+#include <string>
+
+#include "line.hpp"
+
+namespace gpiod {
+
+class chip;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Stores a set of options passed to the kernel when making a line
+ *        request.
+ */
+class request_config
+{
+public:
+
+	/**
+	 * @brief List of available configuration settings. Used in the
+	 *        constructor and :request_config::set_property.
+	 */
+	enum class property {
+		OFFSETS = 1,
+		/**< List of line offsets to request. */
+		CONSUMER,
+		/**< Consumer string. */
+		EVENT_BUFFER_SIZE,
+		/**< Suggested size of the edge event buffer. */
+	};
+
+	/**
+	 * @brief Map of mappings between property types and property values.
+	 */
+	using properties = ::std::map<property, ::std::any>;
+
+	/**
+	 * @brief Constructor.
+	 * @param props List of config properties. See
+	 *              :request_config::set_property.
+	 */
+	explicit request_config(const properties& props = properties());
+
+	request_config(const request_config& other) = delete;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	request_config(request_config&& other) noexcept;
+
+	~request_config(void);
+
+	request_config& operator=(const request_config& other) = delete;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 * @return Reference to self.
+	 */
+	request_config& operator=(request_config&& other) noexcept;
+
+	/**
+	 * @brief Set the value of a single config property.
+	 * @param prop Property to set.
+	 * @param val Property value. The type must correspond to the property
+	 *            being set: `std::string` or `const char*` for
+	 *            :property::CONSUMER, `:line::offsets` for
+	 *            :property::OFFSETS and `unsigned long` for
+	 *            :property::EVENT_BUFFER_SIZE.
+	 */
+	void set_property(property prop, const ::std::any& val);
+
+	/**
+	 * @brief Set line offsets for this request.
+	 * @param offsets Vector of line offsets to request.
+	 */
+	void set_offsets(const line::offsets& offsets) noexcept;
+
+	/**
+	 * @brief Get the number of offsets configured in this request config.
+	 * @return Number of line offsets in this request config.
+	 */
+	::std::size_t num_offsets(void) const noexcept;
+
+	/**
+	 * @brief Set the consumer name.
+	 * @param consumer New consumer name.
+	 */
+	void set_consumer(const ::std::string& consumer) noexcept;
+
+	/**
+	 * @brief Get the consumer name.
+	 * @return Currently configured consumer name. May be an empty string.
+	 */
+	::std::string consumer(void) const noexcept;
+
+	/**
+	 * @brief Get the hardware offsets of lines in this request config.
+	 * @return List of line offsets.
+	 */
+	line::offsets offsets(void) const;
+
+	/**
+	 * @brief Set the size of the kernel event buffer.
+	 * @param event_buffer_size New event buffer size.
+	 * @note The kernel may adjust the value if it's too high. If set to 0,
+	 *       the default value will be used.
+	 */
+	void set_event_buffer_size(::std::size_t event_buffer_size) noexcept;
+
+	/**
+	 * @brief Get the edge event buffer size from this request config.
+	 * @return Current edge event buffer size setting.
+	 */
+	::std::size_t event_buffer_size(void) const noexcept;
+
+private:
+
+	struct impl;
+
+	::std::unique_ptr<impl> _m_priv;
+
+	friend chip;
+};
+
+/**
+ * @brief Stream insertion operator for request_config objects.
+ * @param out Output stream to write to.
+ * @param config request_config to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const request_config& config);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_REQUEST_CONFIG_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/timestamp.hpp b/bindings/cxx/gpiodcxx/timestamp.hpp
new file mode 100644
index 0000000..d707fee
--- /dev/null
+++ b/bindings/cxx/gpiodcxx/timestamp.hpp
@@ -0,0 +1,122 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file timestamp.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_TIMESTAMP_HPP__
+#define __LIBGPIOD_CXX_TIMESTAMP_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <chrono>
+#include <cstdint>
+
+namespace gpiod {
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Stores the edge and info event timestamps as returned by the kernel
+ *        and allows to convert them to std::chrono::time_point.
+ */
+class timestamp
+{
+public:
+
+	/**
+	 * @brief Monotonic time_point.
+	 */
+	using time_point_monotonic = ::std::chrono::time_point<::std::chrono::steady_clock>;
+
+	/**
+	 * @brief Real-time time_point.
+	 */
+	using time_point_realtime = ::std::chrono::time_point<::std::chrono::system_clock>;
+
+	/**
+	 * @brief Constructor with implicit  conversion from `uint64_t`.
+	 * @param ns Timestamp in nanoseconds.
+	 */
+	timestamp(::std::uint64_t ns) : _m_ns(ns) { }
+
+	/**
+	 * @brief Copy constructor.
+	 * @param other Object to copy.
+	 */
+	timestamp(const timestamp& other) noexcept = default;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Object to move.
+	 */
+	timestamp(timestamp&& other) noexcept = default;
+
+	/**
+	 * @brief Assignment operator.
+	 * @param other Object to copy.
+	 * @return Reference to self.
+	 */
+	timestamp& operator=(const timestamp& other) noexcept = default;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Object to move.
+	 * @return Reference to self.
+	 */
+	timestamp& operator=(timestamp&& other) noexcept = default;
+
+	~timestamp(void) = default;
+
+	/**
+	 * @brief Conversion operator to `std::uint64_t`.
+	 */
+	operator ::std::uint64_t(void) noexcept
+	{
+		return this->ns();
+	}
+
+	/**
+	 * @brief Get the timestamp in nanoseconds.
+	 * @return Timestamp in nanoseconds.
+	 */
+	::std::uint64_t ns(void) const noexcept
+	{
+		return this->_m_ns;
+	}
+
+	/**
+	 * @brief Convert the timestamp to a monotonic time_point.
+	 * @return time_point associated with the steady clock.
+	 */
+	time_point_monotonic to_time_point_monotonic(void) const
+	{
+		return time_point_monotonic(::std::chrono::nanoseconds(this->ns()));
+	}
+
+	/**
+	 * @brief Convert the timestamp to a real-time time_point.
+	 * @return time_point associated with the system clock.
+	 */
+	time_point_realtime to_time_point_realtime(void) const
+	{
+		return time_point_realtime(::std::chrono::nanoseconds(this->ns()));
+	}
+
+private:
+	::std::uint64_t _m_ns;
+};
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_TIMESTAMP_HPP__ */
diff --git a/bindings/cxx/info-event.cpp b/bindings/cxx/info-event.cpp
new file mode 100644
index 0000000..6492e42
--- /dev/null
+++ b/bindings/cxx/info-event.cpp
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <map>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+const ::std::map<int, info_event::event_type> event_type_mapping = {
+	{ GPIOD_INFO_EVENT_LINE_REQUESTED,	info_event::event_type::LINE_REQUESTED },
+	{ GPIOD_INFO_EVENT_LINE_RELEASED,	info_event::event_type::LINE_RELEASED },
+	{ GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED,	info_event::event_type::LINE_CONFIG_CHANGED }
+};
+
+const ::std::map<info_event::event_type, ::std::string> event_type_names = {
+	{ info_event::event_type::LINE_REQUESTED,	"LINE_REQUESTED" },
+	{ info_event::event_type::LINE_RELEASED,	"LINE_RELEASED" },
+	{ info_event::event_type::LINE_CONFIG_CHANGED,	"LINE_CONFIG_CHANGED" }
+};
+
+} /* namespace */
+
+void info_event::impl::set_info_event_ptr(info_event_ptr& new_event)
+{
+	::gpiod_line_info* info = ::gpiod_info_event_get_line_info(new_event.get());
+
+	line_info_ptr copy(::gpiod_line_info_copy(info));
+	if (!copy)
+		throw_from_errno("unable to copy the line info object");
+
+	this->event = ::std::move(new_event);
+	this->info._m_priv->set_info_ptr(copy);
+}
+
+info_event::info_event(void)
+	: _m_priv(new impl)
+{
+
+}
+
+GPIOD_CXX_API info_event::info_event(const info_event& other)
+	: _m_priv(other._m_priv)
+{
+
+}
+
+GPIOD_CXX_API info_event::info_event(info_event&& other) noexcept
+	: _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API info_event::~info_event(void)
+{
+
+}
+
+GPIOD_CXX_API info_event& info_event::operator=(const info_event& other)
+{
+	this->_m_priv = other._m_priv;
+
+	return *this;
+}
+
+GPIOD_CXX_API info_event& info_event::operator=(info_event&& other) noexcept
+{
+	this->_m_priv = ::std::move(other._m_priv);
+
+	return *this;
+}
+
+GPIOD_CXX_API info_event::event_type info_event::type(void) const
+{
+	int type = ::gpiod_info_event_get_event_type(this->_m_priv->event.get());
+
+	return map_int_to_enum(type, event_type_mapping);
+}
+
+GPIOD_CXX_API ::std::uint64_t info_event::timestamp_ns(void) const noexcept
+{
+	return ::gpiod_info_event_get_timestamp_ns(this->_m_priv->event.get());
+}
+
+GPIOD_CXX_API const line_info& info_event::get_line_info(void) const noexcept
+{
+	return this->_m_priv->info;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const info_event& event)
+{
+	out << "info_event(event_type='" << event_type_names.at(event.type()) <<
+	       "', timestamp=" << event.timestamp_ns() <<
+	       ", line_info=" << event.get_line_info() <<
+	       ")";
+
+	return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/internal.cpp b/bindings/cxx/internal.cpp
new file mode 100644
index 0000000..9f1f38f
--- /dev/null
+++ b/bindings/cxx/internal.cpp
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <map>
+#include <stdexcept>
+#include <system_error>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+void throw_from_errno(const ::std::string& what)
+{
+	switch (errno) {
+	case EINVAL:
+		throw ::std::invalid_argument(what);
+	case E2BIG:
+		throw ::std::length_error(what);
+	case ENOMEM:
+		throw ::std::bad_alloc();
+	case EDOM:
+		throw ::std::domain_error(what);
+	default:
+		throw ::std::system_error(errno, ::std::system_category(), what);
+	}
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/internal.hpp b/bindings/cxx/internal.hpp
index 9406d30..c4b5d34 100644
--- a/bindings/cxx/internal.hpp
+++ b/bindings/cxx/internal.hpp
@@ -1,9 +1,207 @@
 /* SPDX-License-Identifier: LGPL-3.0-or-later */
-/* SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <bgolaszewski@baylibre.com> */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
 
-#ifndef __LIBGPIOD_GPIOD_CXX_INTERNAL_HPP__
-#define __LIBGPIOD_GPIOD_CXX_INTERNAL_HPP__
+#ifndef __LIBGPIOD_CXX_INTERNAL_HPP__
+#define __LIBGPIOD_CXX_INTERNAL_HPP__
 
-#define GPIOD_CXX_API __attribute__((visibility("default")))
+#include <gpiod.h>
+#include <iostream>
+#include <iterator>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
 
-#endif /* __LIBGPIOD_GPIOD_CXX_INTERNAL_HPP__ */
+#include "gpiod.hpp"
+
+#define GPIOD_CXX_UNUSED	__attribute__((unused))
+#define GPIOD_CXX_NORETURN	__attribute__((noreturn))
+
+namespace gpiod {
+
+template<class enum_type> enum_type
+map_int_to_enum(int value, const ::std::map<int, enum_type>& mapping)
+{
+	try {
+		return mapping.at(value);
+	} catch (const ::std::out_of_range& err) {
+		throw bad_mapping(::std::string("invalid value for ") +
+				  typeid(enum_type).name());
+	}
+}
+
+void throw_from_errno(const ::std::string& what);
+
+template<class T, void F(T*)> struct deleter
+{
+	void operator()(T* ptr)
+	{
+		F(ptr);
+	}
+};
+
+using chip_info_deleter = deleter<::gpiod_chip_info, ::gpiod_chip_info_free>;
+using line_info_deleter = deleter<::gpiod_line_info, ::gpiod_line_info_free>;
+using info_event_deleter = deleter<::gpiod_info_event, ::gpiod_info_event_free>;
+using line_config_deleter = deleter<::gpiod_line_config, ::gpiod_line_config_free>;
+using request_config_deleter = deleter<::gpiod_request_config, ::gpiod_request_config_free>;
+using line_request_deleter = deleter<::gpiod_line_request, ::gpiod_line_request_release>;
+using edge_event_deleter = deleter<::gpiod_edge_event, ::gpiod_edge_event_free>;
+using edge_event_buffer_deleter = deleter<::gpiod_edge_event_buffer,
+					  ::gpiod_edge_event_buffer_free>;
+
+using chip_info_ptr = ::std::unique_ptr<::gpiod_chip_info, chip_info_deleter>;
+using line_info_ptr = ::std::unique_ptr<::gpiod_line_info, line_info_deleter>;
+using info_event_ptr = ::std::unique_ptr<::gpiod_info_event, info_event_deleter>;
+using line_config_ptr = ::std::unique_ptr<::gpiod_line_config, line_config_deleter>;
+using request_config_ptr = ::std::unique_ptr<::gpiod_request_config, request_config_deleter>;
+using line_request_ptr = ::std::unique_ptr<::gpiod_line_request, line_request_deleter>;
+using edge_event_ptr = ::std::unique_ptr<::gpiod_edge_event, edge_event_deleter>;
+using edge_event_buffer_ptr = ::std::unique_ptr<::gpiod_edge_event_buffer,
+						edge_event_buffer_deleter>;
+
+struct chip_info::impl
+{
+	impl(void) = default;
+	impl(const impl& other) = delete;
+	impl(impl&& other) = delete;
+	impl& operator=(const impl& other) = delete;
+	impl& operator=(impl&& other) = delete;
+
+	void set_info_ptr(chip_info_ptr& new_info);
+
+	chip_info_ptr info;
+};
+
+struct line_info::impl
+{
+	impl(void) = default;
+	impl(const impl& other) = delete;
+	impl(impl&& other) = delete;
+	impl& operator=(const impl& other) = delete;
+	impl& operator=(impl&& other) = delete;
+
+	void set_info_ptr(line_info_ptr& new_info);
+
+	line_info_ptr info;
+};
+
+struct info_event::impl
+{
+	impl(void) = default;
+	impl(const impl& other) = delete;
+	impl(impl&& other) = delete;
+	impl& operator=(const impl& other) = delete;
+	impl& operator=(impl&& other) = delete;
+
+	void set_info_event_ptr(info_event_ptr& new_event);
+
+	info_event_ptr event;
+	line_info info;
+};
+
+struct line_config::impl
+{
+	impl(void);
+	impl(const impl& other) = delete;
+	impl(impl&& other) = delete;
+	impl& operator=(const impl& other) = delete;
+	impl& operator=(impl&& other) = delete;
+
+	line_config_ptr config;
+};
+
+struct request_config::impl
+{
+	impl(void);
+	impl(const impl& other) = delete;
+	impl(impl&& other) = delete;
+	impl& operator=(const impl& other) = delete;
+	impl& operator=(impl&& other) = delete;
+
+	request_config_ptr config;
+};
+
+struct line_request::impl
+{
+	impl(void) = default;
+	impl(const impl& other) = delete;
+	impl(impl&& other) = delete;
+	impl& operator=(const impl& other) = delete;
+	impl& operator=(impl&& other) = delete;
+
+	void throw_if_released(void) const;
+	void set_request_ptr(line_request_ptr& ptr);
+	void fill_offset_buf(const line::offsets& offsets);
+
+	line_request_ptr request;
+
+	/*
+	 * Used when reading/setting the line values in order to avoid
+	 * allocating a new buffer on every call. We're not doing it for
+	 * offsets in the line & request config structures because they don't
+	 * require high performance unlike the set/get value calls.
+	 */
+	::std::vector<unsigned int> offset_buf;
+};
+
+struct edge_event::impl
+{
+	impl(void) = default;
+	impl(const impl& other) = delete;
+	impl(impl&& other) = delete;
+	virtual ~impl(void) = default;
+	impl& operator=(const impl& other) = delete;
+	impl& operator=(impl&& other) = delete;
+
+	virtual ::gpiod_edge_event* get_event_ptr(void) const noexcept = 0;
+	virtual ::std::shared_ptr<impl> copy(const ::std::shared_ptr<impl>& self) const = 0;
+};
+
+struct edge_event::impl_managed : public edge_event::impl
+{
+	impl_managed(void) = default;
+	impl_managed(const impl_managed& other) = delete;
+	impl_managed(impl_managed&& other) = delete;
+	virtual ~impl_managed(void) = default;
+	impl_managed& operator=(const impl_managed& other) = delete;
+	impl_managed& operator=(impl_managed&& other) = delete;
+
+	::gpiod_edge_event* get_event_ptr(void) const noexcept override;
+	::std::shared_ptr<impl> copy(const ::std::shared_ptr<impl>& self) const override;
+
+	edge_event_ptr event;
+};
+
+struct edge_event::impl_external : public edge_event::impl
+{
+	impl_external(void);
+	impl_external(const impl_external& other) = delete;
+	impl_external(impl_external&& other) = delete;
+	virtual ~impl_external(void) = default;
+	impl_external& operator=(const impl_external& other) = delete;
+	impl_external& operator=(impl_external&& other) = delete;
+
+	::gpiod_edge_event* get_event_ptr(void) const noexcept override;
+	::std::shared_ptr<impl> copy(const ::std::shared_ptr<impl>& self) const override;
+
+	::gpiod_edge_event *event;
+};
+
+struct edge_event_buffer::impl
+{
+	impl(unsigned int capacity);
+	impl(const impl& other) = delete;
+	impl(impl&& other) = delete;
+	impl& operator=(const impl& other) = delete;
+	impl& operator=(impl&& other) = delete;
+
+	int read_events(const line_request_ptr& request, unsigned int max_events);
+
+	edge_event_buffer_ptr buffer;
+	::std::vector<edge_event> events;
+};
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_INTERNAL_HPP__ */
diff --git a/bindings/cxx/iter.cpp b/bindings/cxx/iter.cpp
deleted file mode 100644
index 09d46f3..0000000
--- a/bindings/cxx/iter.cpp
+++ /dev/null
@@ -1,60 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <gpiod.hpp>
-#include <system_error>
-
-#include "internal.hpp"
-
-namespace gpiod {
-
-GPIOD_CXX_API line_iter begin(line_iter iter) noexcept
-{
-	return iter;
-}
-
-GPIOD_CXX_API line_iter end(const line_iter&) noexcept
-{
-	return line_iter();
-}
-
-GPIOD_CXX_API line_iter::line_iter(const chip& owner)
-	: _m_current(owner.get_line(0))
-{
-
-}
-
-GPIOD_CXX_API line_iter& line_iter::operator++(void)
-{
-	unsigned int offset = this->_m_current.offset() + 1;
-	chip owner = this->_m_current.get_chip();
-
-	if (offset == owner.num_lines())
-		this->_m_current = line(); /* Last element */
-	else
-		this->_m_current = owner.get_line(offset);
-
-	return *this;
-}
-
-GPIOD_CXX_API const line& line_iter::operator*(void) const
-{
-	return this->_m_current;
-}
-
-GPIOD_CXX_API const line* line_iter::operator->(void) const
-{
-	return ::std::addressof(this->_m_current);
-}
-
-GPIOD_CXX_API bool line_iter::operator==(const line_iter& rhs) const noexcept
-{
-	return this->_m_current._m_line == rhs._m_current._m_line;
-}
-
-GPIOD_CXX_API bool line_iter::operator!=(const line_iter& rhs) const noexcept
-{
-	return this->_m_current._m_line != rhs._m_current._m_line;
-}
-
-} /* namespace gpiod */
diff --git a/bindings/cxx/line-config.cpp b/bindings/cxx/line-config.cpp
new file mode 100644
index 0000000..7765328
--- /dev/null
+++ b/bindings/cxx/line-config.cpp
@@ -0,0 +1,685 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <iterator>
+#include <map>
+#include <sstream>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+template<class enum_type>
+::std::map<int, enum_type> make_reverse_maping(const ::std::map<enum_type, int>& mapping)
+{
+	::std::map<int, enum_type> ret;
+
+	for (const auto &item: mapping)
+		ret[item.second] = item.first;
+
+	return ret;
+}
+
+const ::std::map<line::direction, int> direction_mapping = {
+	{ line::direction::AS_IS,	GPIOD_LINE_DIRECTION_AS_IS },
+	{ line::direction::INPUT,	GPIOD_LINE_DIRECTION_INPUT },
+	{ line::direction::OUTPUT,	GPIOD_LINE_DIRECTION_OUTPUT }
+};
+
+const ::std::map<int, line::direction> reverse_direction_mapping = make_reverse_maping(direction_mapping);
+
+const ::std::map<line::edge, int> edge_mapping = {
+	{ line::edge::NONE,		GPIOD_LINE_EDGE_NONE },
+	{ line::edge::FALLING,		GPIOD_LINE_EDGE_FALLING },
+	{ line::edge::RISING,		GPIOD_LINE_EDGE_RISING },
+	{ line::edge::BOTH,		GPIOD_LINE_EDGE_BOTH }
+};
+
+const ::std::map<int, line::edge> reverse_edge_mapping = make_reverse_maping(edge_mapping);
+
+const ::std::map<line::bias, int> bias_mapping = {
+	{ line::bias::AS_IS,		GPIOD_LINE_BIAS_AS_IS },
+	{ line::bias::DISABLED,		GPIOD_LINE_BIAS_DISABLED },
+	{ line::bias::PULL_UP,		GPIOD_LINE_BIAS_PULL_UP },
+	{ line::bias::PULL_DOWN,	GPIOD_LINE_BIAS_PULL_DOWN }
+};
+
+const ::std::map<int, line::bias> reverse_bias_mapping = make_reverse_maping(bias_mapping);
+
+const ::std::map<line::drive, int> drive_mapping = {
+	{ line::drive::PUSH_PULL,	GPIOD_LINE_DRIVE_PUSH_PULL },
+	{ line::drive::OPEN_DRAIN,	GPIOD_LINE_DRIVE_OPEN_DRAIN },
+	{ line::drive::OPEN_SOURCE,	GPIOD_LINE_DRIVE_OPEN_SOURCE }
+};
+
+const ::std::map<int, line::drive> reverse_drive_mapping = make_reverse_maping(drive_mapping);
+
+const ::std::map<line::clock, int> clock_mapping = {
+	{ line::clock::MONOTONIC,	GPIOD_LINE_EVENT_CLOCK_MONOTONIC },
+	{ line::clock::REALTIME,	GPIOD_LINE_EVENT_CLOCK_REALTIME },
+};
+
+const ::std::map<int, line::clock> reverse_clock_mapping = make_reverse_maping(clock_mapping);
+
+template<class key_type, class value_type, class exception_type>
+value_type map_setting(const key_type& key, const ::std::map<key_type, value_type>& mapping)
+{
+	value_type ret;
+
+	try {
+		ret = mapping.at(key);
+	} catch (const ::std::out_of_range& err) {
+		throw exception_type(::std::string("invalid value for ") +
+				     typeid(key_type).name());
+	}
+
+	return ret;
+}
+
+::gpiod_line_config* make_line_config(void)
+{
+	::gpiod_line_config *config = ::gpiod_line_config_new();
+	if (!config)
+		throw_from_errno("Unable to allocate the line config object");
+
+	return config;
+}
+
+template<class enum_type>
+int do_map_value(enum_type value, const ::std::map<enum_type, int>& mapping)
+{
+	return map_setting<enum_type, int, ::std::invalid_argument>(value, mapping);
+}
+
+template<class enum_type, void set_func(::gpiod_line_config*, int)>
+void set_mapped_value_default(::gpiod_line_config* config, enum_type value,
+			      const ::std::map<enum_type, int>& mapping)
+{
+	int mapped_val = do_map_value(value, mapping);
+
+	set_func(config, mapped_val);
+}
+
+template<class enum_type, void set_func(::gpiod_line_config*, int, unsigned int)>
+void set_mapped_value_override(::gpiod_line_config* config, enum_type value, line::offset offset,
+			       const ::std::map<enum_type, int>& mapping)
+{
+	int mapped_val = do_map_value(value, mapping);
+
+	set_func(config, mapped_val, offset);
+}
+
+template<class ret_type, int get_func(::gpiod_line_config*)>
+ret_type get_mapped_value_default(::gpiod_line_config* config,
+				  const ::std::map<int, ret_type>& mapping)
+{
+	int mapped_val = get_func(config);
+
+	return map_int_to_enum(mapped_val, mapping);
+}
+
+template<class ret_type, int get_func(::gpiod_line_config*, unsigned int)>
+ret_type get_mapped_value_offset(::gpiod_line_config* config, line::offset offset,
+				 const ::std::map<int, ret_type>& mapping)
+{
+	int mapped_val = get_func(config, offset);
+
+	return map_int_to_enum(mapped_val, mapping);
+}
+
+const ::std::map<int, line_config::property> property_mapping = {
+	{ GPIOD_LINE_CONFIG_PROP_DIRECTION,		line_config::property::DIRECTION },
+	{ GPIOD_LINE_CONFIG_PROP_EDGE,			line_config::property::EDGE },
+	{ GPIOD_LINE_CONFIG_PROP_BIAS,			line_config::property::BIAS },
+	{ GPIOD_LINE_CONFIG_PROP_DRIVE,			line_config::property::DRIVE },
+	{ GPIOD_LINE_CONFIG_PROP_ACTIVE_LOW,		line_config::property::ACTIVE_LOW },
+	{ GPIOD_LINE_CONFIG_PROP_DEBOUNCE_PERIOD_US,	line_config::property::DEBOUNCE_PERIOD },
+	{ GPIOD_LINE_CONFIG_PROP_EVENT_CLOCK,		line_config::property::EVENT_CLOCK },
+	{ GPIOD_LINE_CONFIG_PROP_OUTPUT_VALUE,		line_config::property::OUTPUT_VALUE }
+};
+
+} /* namespace */
+
+line_config::impl::impl(void)
+	: config(make_line_config())
+{
+
+}
+
+GPIOD_CXX_API line_config::line_config(const properties& props)
+	: _m_priv(new impl)
+{
+	for (const auto& prop: props) {
+		if (prop.first == property::OUTPUT_VALUES)
+			this->set_output_values(::std::any_cast<line::value_mappings>(prop.second));
+		else
+			this->set_property_default(prop.first, prop.second);
+	}
+}
+
+GPIOD_CXX_API line_config::line_config(line_config&& other) noexcept
+	: _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API line_config::~line_config(void)
+{
+
+}
+
+GPIOD_CXX_API void line_config::reset(void) noexcept
+{
+	::gpiod_line_config_reset(this->_m_priv->config.get());
+}
+
+GPIOD_CXX_API line_config& line_config::operator=(line_config&& other) noexcept
+{
+	this->_m_priv = ::std::move(other._m_priv);
+
+	return *this;
+}
+
+GPIOD_CXX_API void line_config::set_property_default(property prop, const ::std::any& val)
+{
+	switch(prop) {
+	case property::DIRECTION:
+		this->set_direction_default(::std::any_cast<line::direction>(val));
+		break;
+	case property::EDGE:
+		this->set_edge_detection_default(::std::any_cast<line::edge>(val));
+		break;
+	case property::BIAS:
+		this->set_bias_default(::std::any_cast<line::bias>(val));
+		break;
+	case property::DRIVE:
+		this->set_drive_default(::std::any_cast<line::drive>(val));
+		break;
+	case property::ACTIVE_LOW:
+		this->set_active_low_default(::std::any_cast<bool>(val));
+		break;
+	case property::DEBOUNCE_PERIOD:
+		this->set_debounce_period_default(::std::any_cast<::std::chrono::microseconds>(val));
+		break;
+	case property::EVENT_CLOCK:
+		this->set_event_clock_default(::std::any_cast<line::clock>(val));
+		break;
+	case property::OUTPUT_VALUE:
+		this->set_output_value_default(::std::any_cast<line::value>(val));
+		break;
+	default:
+		throw ::std::invalid_argument("invalid property type");
+	}
+}
+
+GPIOD_CXX_API void line_config::set_property_offset(property prop, line::offset offset,
+						    const ::std::any& val)
+{
+	switch(prop) {
+	case property::DIRECTION:
+		this->set_direction_override(::std::any_cast<line::direction>(val), offset);
+		break;
+	case property::EDGE:
+		this->set_edge_detection_override(::std::any_cast<line::edge>(val), offset);
+		break;
+	case property::BIAS:
+		this->set_bias_override(::std::any_cast<line::bias>(val), offset);
+		break;
+	case property::DRIVE:
+		this->set_drive_override(::std::any_cast<line::drive>(val), offset);
+		break;
+	case property::ACTIVE_LOW:
+		this->set_active_low_override(::std::any_cast<bool>(val), offset);
+		break;
+	case property::DEBOUNCE_PERIOD:
+		this->set_debounce_period_override(::std::any_cast<::std::chrono::microseconds>(val),
+						      offset);
+		break;
+	case property::EVENT_CLOCK:
+		this->set_event_clock_override(::std::any_cast<line::clock>(val), offset);
+		break;
+	case property::OUTPUT_VALUE:
+		this->set_output_value_override(::std::any_cast<line::value>(val), offset);
+		break;
+	default:
+		throw ::std::invalid_argument("invalid property type");
+	}
+}
+
+GPIOD_CXX_API void line_config::set_direction_default(line::direction direction)
+{
+	set_mapped_value_default<line::direction,
+				 ::gpiod_line_config_set_direction_default>(this->_m_priv->config.get(),
+									    direction, direction_mapping);
+}
+
+GPIOD_CXX_API void line_config::set_direction_override(line::direction direction, line::offset offset)
+{
+	set_mapped_value_override<line::direction,
+				  ::gpiod_line_config_set_direction_override>(this->_m_priv->config.get(),
+									      direction, offset,
+									      direction_mapping);
+}
+
+GPIOD_CXX_API line::direction line_config::direction_default(void) const
+{
+	return get_mapped_value_default<line::direction,
+					::gpiod_line_config_get_direction_default>(
+							this->_m_priv->config.get(),
+							reverse_direction_mapping);
+}
+
+GPIOD_CXX_API line::direction line_config::direction_offset(line::offset offset) const
+{
+	return get_mapped_value_offset<line::direction,
+				       ::gpiod_line_config_get_direction_offset>(
+						       this->_m_priv->config.get(),
+						       offset, reverse_direction_mapping);
+}
+
+GPIOD_CXX_API void line_config::clear_direction_override(line::offset offset) noexcept
+{
+	::gpiod_line_config_clear_direction_override(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API bool line_config::direction_is_overridden(line::offset offset) const noexcept
+{
+	return ::gpiod_line_config_direction_is_overridden(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API void line_config::set_edge_detection_default(line::edge edge)
+{
+	set_mapped_value_default<line::edge,
+				 ::gpiod_line_config_set_edge_detection_default>(
+						 this->_m_priv->config.get(),
+						 edge, edge_mapping);
+}
+
+GPIOD_CXX_API void line_config::set_edge_detection_override(line::edge edge, line::offset offset)
+{
+	set_mapped_value_override<line::edge,
+				  ::gpiod_line_config_set_edge_detection_override>(
+						this->_m_priv->config.get(),
+						edge, offset, edge_mapping);
+}
+
+GPIOD_CXX_API line::edge line_config::edge_detection_default(void) const
+{
+	return get_mapped_value_default<line::edge,
+					::gpiod_line_config_get_edge_detection_default>(
+							this->_m_priv->config.get(),
+							reverse_edge_mapping);
+}
+
+GPIOD_CXX_API line::edge line_config::edge_detection_offset(line::offset offset) const
+{
+	return get_mapped_value_offset<line::edge,
+				       ::gpiod_line_config_get_edge_detection_offset>(
+						       this->_m_priv->config.get(),
+						       offset, reverse_edge_mapping);
+}
+
+GPIOD_CXX_API void line_config::clear_edge_detection_override(line::offset offset) noexcept
+{
+	::gpiod_line_config_clear_edge_detection_override(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API bool line_config::edge_detection_is_overridden(line::offset offset) const noexcept
+{
+	return ::gpiod_line_config_edge_detection_is_overridden(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API void line_config::set_bias_default(line::bias bias)
+{
+	set_mapped_value_default<line::bias,
+				 ::gpiod_line_config_set_bias_default>(this->_m_priv->config.get(),
+								       bias, bias_mapping);
+}
+
+GPIOD_CXX_API void line_config::set_bias_override(line::bias bias, line::offset offset)
+{
+	set_mapped_value_override<line::bias,
+				 ::gpiod_line_config_set_bias_override>(this->_m_priv->config.get(),
+									bias, offset, bias_mapping);
+}
+
+GPIOD_CXX_API line::bias line_config::bias_default(void) const
+{
+	return get_mapped_value_default<line::bias,
+					::gpiod_line_config_get_bias_default>(this->_m_priv->config.get(),
+									      reverse_bias_mapping);
+}
+
+GPIOD_CXX_API line::bias line_config::bias_offset(line::offset offset) const
+{
+	return get_mapped_value_offset<line::bias,
+				       ::gpiod_line_config_get_bias_offset>(this->_m_priv->config.get(),
+									    offset, reverse_bias_mapping);
+}
+
+GPIOD_CXX_API void line_config::clear_bias_override(line::offset offset) noexcept
+{
+	::gpiod_line_config_clear_bias_override(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API bool line_config::bias_is_overridden(line::offset offset) const noexcept
+{
+	return ::gpiod_line_config_bias_is_overridden(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API void line_config::set_drive_default(line::drive drive)
+{
+	set_mapped_value_default<line::drive,
+				 ::gpiod_line_config_set_drive_default>(this->_m_priv->config.get(),
+									drive, drive_mapping);
+}
+
+GPIOD_CXX_API void line_config::set_drive_override(line::drive drive, line::offset offset)
+{
+	set_mapped_value_override<line::drive,
+				  ::gpiod_line_config_set_drive_override>(this->_m_priv->config.get(),
+									  drive, offset, drive_mapping);
+}
+
+GPIOD_CXX_API line::drive line_config::drive_default(void) const
+{
+	return get_mapped_value_default<line::drive,
+					::gpiod_line_config_get_drive_default>(this->_m_priv->config.get(),
+									       reverse_drive_mapping);
+}
+
+GPIOD_CXX_API line::drive line_config::drive_offset(line::offset offset) const
+{
+	return get_mapped_value_offset<line::drive,
+				       ::gpiod_line_config_get_drive_offset>(this->_m_priv->config.get(),
+									     offset, reverse_drive_mapping);
+}
+
+GPIOD_CXX_API void line_config::clear_drive_override(line::offset offset) noexcept
+{
+	::gpiod_line_config_clear_drive_override(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API bool line_config::drive_is_overridden(line::offset offset) const noexcept
+{
+	return ::gpiod_line_config_drive_is_overridden(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API void line_config::set_active_low_default(bool active_low) noexcept
+{
+	::gpiod_line_config_set_active_low_default(this->_m_priv->config.get(), active_low);
+}
+
+GPIOD_CXX_API void line_config::set_active_low_override(bool active_low, line::offset offset) noexcept
+{
+	::gpiod_line_config_set_active_low_override(this->_m_priv->config.get(), active_low, offset);
+}
+
+GPIOD_CXX_API bool line_config::active_low_default(void) const noexcept
+{
+	return ::gpiod_line_config_get_active_low_default(this->_m_priv->config.get());
+}
+
+GPIOD_CXX_API bool line_config::active_low_offset(line::offset offset) const noexcept
+{
+	return ::gpiod_line_config_get_active_low_offset(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API void line_config::clear_active_low_override(line::offset offset) noexcept
+{
+	::gpiod_line_config_clear_active_low_override(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API bool line_config::active_low_is_overridden(line::offset offset) const noexcept
+{
+	return ::gpiod_line_config_active_low_is_overridden(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API void
+line_config::set_debounce_period_default(const ::std::chrono::microseconds& period) noexcept
+{
+	::gpiod_line_config_set_debounce_period_us_default(this->_m_priv->config.get(), period.count());
+}
+
+GPIOD_CXX_API void
+line_config::set_debounce_period_override(const ::std::chrono::microseconds& period,
+					     line::offset offset) noexcept
+{
+	::gpiod_line_config_set_debounce_period_us_override(this->_m_priv->config.get(),
+							    period.count(), offset);
+}
+
+GPIOD_CXX_API ::std::chrono::microseconds line_config::debounce_period_default(void) const noexcept
+{
+	return ::std::chrono::microseconds(
+			::gpiod_line_config_get_debounce_period_us_default(this->_m_priv->config.get()));
+}
+
+GPIOD_CXX_API ::std::chrono::microseconds
+line_config::debounce_period_offset(line::offset offset) const noexcept
+{
+	return ::std::chrono::microseconds(
+			::gpiod_line_config_get_debounce_period_us_offset(this->_m_priv->config.get(),
+									  offset));
+}
+
+GPIOD_CXX_API void line_config::clear_debounce_period_override(line::offset offset) noexcept
+{
+	::gpiod_line_config_clear_debounce_period_us_override(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API bool line_config::debounce_period_is_overridden(line::offset offset) const noexcept
+{
+	return ::gpiod_line_config_debounce_period_us_is_overridden(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API void line_config::set_event_clock_default(line::clock clock)
+{
+	set_mapped_value_default<line::clock,
+				 ::gpiod_line_config_set_event_clock_default>(this->_m_priv->config.get(),
+									      clock, clock_mapping);
+}
+
+GPIOD_CXX_API void line_config::set_event_clock_override(line::clock clock, line::offset offset)
+{
+	set_mapped_value_override<line::clock,
+				  ::gpiod_line_config_set_event_clock_override>(this->_m_priv->config.get(),
+										clock, offset,
+										clock_mapping);
+}
+
+GPIOD_CXX_API line::clock line_config::event_clock_default(void) const
+{
+	return get_mapped_value_default<line::clock,
+					::gpiod_line_config_get_event_clock_default>(
+							this->_m_priv->config.get(),
+							reverse_clock_mapping);
+}
+
+GPIOD_CXX_API line::clock line_config::event_clock_offset(line::offset offset) const
+{
+	return get_mapped_value_offset<line::clock,
+					::gpiod_line_config_get_event_clock_offset>(
+							this->_m_priv->config.get(),
+							offset, reverse_clock_mapping);
+}
+
+GPIOD_CXX_API void line_config::clear_event_clock_override(line::offset offset) noexcept
+{
+	::gpiod_line_config_clear_event_clock_override(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API bool line_config::event_clock_is_overridden(line::offset offset) const noexcept
+{
+	return ::gpiod_line_config_event_clock_is_overridden(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API void line_config::set_output_value_default(line::value value) noexcept
+{
+	::gpiod_line_config_set_output_value_default(this->_m_priv->config.get(), static_cast<int>(value));
+}
+
+GPIOD_CXX_API void line_config::set_output_value_override(line::value value, line::offset offset) noexcept
+{
+	::gpiod_line_config_set_output_value_override(this->_m_priv->config.get(),
+						      offset, static_cast<int>(value));
+}
+
+GPIOD_CXX_API void line_config::set_output_values(const line::value_mappings& values)
+{
+	line::offsets offsets;
+	line::values vals;
+
+	if (values.empty())
+		return;
+
+	offsets.reserve(values.size());
+	vals.reserve(values.size());
+
+	for (auto& val: values) {
+		offsets.push_back(val.first);
+		vals.push_back(val.second);
+	}
+
+	this->set_output_values(offsets, vals);
+}
+
+GPIOD_CXX_API void line_config::set_output_values(const line::offsets& offsets,
+						  const line::values& values)
+{
+	if (offsets.size() != values.size())
+		throw ::std::invalid_argument("values must have the same size as the offsets");
+
+	if (offsets.empty())
+		return;
+
+	::std::vector<unsigned int> buf(offsets.size());
+
+	for (unsigned int i = 0; i < offsets.size(); i++)
+		buf[i] = offsets[i];
+
+	::gpiod_line_config_set_output_values(this->_m_priv->config.get(),
+					      offsets.size(), buf.data(),
+					      reinterpret_cast<const int*>(values.data()));
+}
+
+GPIOD_CXX_API line::value line_config::output_value_default(void) const noexcept
+{
+	return static_cast<line::value>(::gpiod_line_config_get_output_value_default(
+								this->_m_priv->config.get()));
+}
+
+GPIOD_CXX_API line::value line_config::output_value_offset(line::offset offset) const noexcept
+{
+	return static_cast<line::value>(
+			::gpiod_line_config_get_output_value_offset(this->_m_priv->config.get(),
+								    offset));
+}
+
+GPIOD_CXX_API void line_config::clear_output_value_override(line::offset offset) noexcept
+{
+	::gpiod_line_config_clear_output_value_override(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API bool line_config::output_value_is_overridden(line::offset offset) const noexcept
+{
+	return ::gpiod_line_config_output_value_is_overridden(this->_m_priv->config.get(), offset);
+}
+
+GPIOD_CXX_API ::std::size_t line_config::num_overrides(void) const noexcept
+{
+	return ::gpiod_line_config_get_num_overrides(this->_m_priv->config.get());
+}
+
+GPIOD_CXX_API line_config::override_list line_config::overrides(void) const
+{
+	unsigned int num_overrides = this->num_overrides();
+	override_list ret(num_overrides);
+	::std::vector<unsigned int> offsets(num_overrides);
+	::std::vector<int> props(num_overrides);
+
+	::gpiod_line_config_get_overrides(this->_m_priv->config.get(), offsets.data(), props.data());
+
+	for (unsigned int i = 0; i < num_overrides; i++)
+		ret[i] = { offsets[i], property_mapping.at(props[i]) };
+
+	return ret;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line_config& config)
+{
+	out << "line_config(defaults=(direction=" << config.direction_default() <<
+	       ", edge_detection=" << config.edge_detection_default() <<
+	       ", bias=" << config.bias_default() <<
+	       ", drive=" << config.drive_default() << ", " <<
+	       (config.active_low_default() ? "active-low" : "active-high") <<
+	       ", debounce_period=" << config.debounce_period_default().count() << "us" <<
+	       ", event_clock=" << config.event_clock_default() <<
+	       ", default_output_value=" << config.output_value_default() <<
+	       "), ";
+
+	if (config.num_overrides()) {
+		::std::vector<::std::string> overrides(config.num_overrides());
+		::std::vector<::std::string>::iterator it = overrides.begin();
+
+		out << "overrides=[";
+
+		for (const auto& override: config.overrides()) {
+			line::offset offset = override.first;
+			line_config::property prop = override.second;
+			::std::stringstream out;
+
+			out << "(offset=" << offset << " -> ";
+
+			switch (prop) {
+			case line_config::property::DIRECTION:
+				out << "direction=" << config.direction_offset(offset);
+				break;
+			case line_config::property::EDGE:
+				out << "edge_detection=" << config.edge_detection_offset(offset);
+				break;
+			case line_config::property::BIAS:
+				out << "bias=" << config.bias_offset(offset);
+				break;
+			case line_config::property::DRIVE:
+				out << "drive=" << config.drive_offset(offset);
+				break;
+			case line_config::property::ACTIVE_LOW:
+				out << (config.active_low_offset(offset) ? "active-low" : "active-high");
+				break;
+			case line_config::property::DEBOUNCE_PERIOD:
+				out << "debounce_period=" <<
+				       config.debounce_period_offset(offset).count() << "us";
+				break;
+			case line_config::property::EVENT_CLOCK:
+				out << "event_clock=" << config.event_clock_offset(offset);
+				break;
+			case line_config::property::OUTPUT_VALUE:
+				out << "output_value=" << config.output_value_offset(offset);
+				break;
+			default:
+				/* OUTPUT_VALUES is ignored. */
+				break;
+			}
+
+			out << ")";
+
+			*it = out.str();
+			it++;
+		}
+
+		::std::copy(overrides.begin(), ::std::prev(overrides.end()),
+			    ::std::ostream_iterator<::std::string>(out, ", "));
+		out << overrides.back();
+
+		out << "]";
+	}
+
+	out << ")";
+
+	return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/line-info.cpp b/bindings/cxx/line-info.cpp
new file mode 100644
index 0000000..bc7673d
--- /dev/null
+++ b/bindings/cxx/line-info.cpp
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <map>
+#include <iostream>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+const ::std::map<int, line::direction> direction_mapping = {
+	{ GPIOD_LINE_DIRECTION_INPUT,		line::direction::INPUT },
+	{ GPIOD_LINE_DIRECTION_OUTPUT,		line::direction::OUTPUT }
+};
+
+const ::std::map<int, line::bias> bias_mapping = {
+	{ GPIOD_LINE_BIAS_UNKNOWN,		line::bias::UNKNOWN },
+	{ GPIOD_LINE_BIAS_DISABLED,		line::bias::DISABLED },
+	{ GPIOD_LINE_BIAS_PULL_UP,		line::bias::PULL_UP },
+	{ GPIOD_LINE_BIAS_PULL_DOWN,		line::bias::PULL_DOWN }
+};
+
+const ::std::map<int, line::drive> drive_mapping = {
+	{ GPIOD_LINE_DRIVE_PUSH_PULL,		line::drive::PUSH_PULL },
+	{ GPIOD_LINE_DRIVE_OPEN_DRAIN,		line::drive::OPEN_DRAIN },
+	{ GPIOD_LINE_DRIVE_OPEN_SOURCE,		line::drive::OPEN_SOURCE }
+};
+
+const ::std::map<int, line::edge> edge_mapping = {
+	{ GPIOD_LINE_EDGE_NONE,			line::edge::NONE },
+	{ GPIOD_LINE_EDGE_RISING,		line::edge::RISING },
+	{ GPIOD_LINE_EDGE_FALLING,		line::edge::FALLING },
+	{ GPIOD_LINE_EDGE_BOTH,			line::edge::BOTH }
+};
+
+const ::std::map<int, line::clock> clock_mapping = {
+	{ GPIOD_LINE_EVENT_CLOCK_MONOTONIC,	line::clock::MONOTONIC },
+	{ GPIOD_LINE_EVENT_CLOCK_REALTIME,	line::clock::REALTIME }
+};
+
+} /* namespace */
+
+void line_info::impl::set_info_ptr(line_info_ptr& new_info)
+{
+	this->info = ::std::move(new_info);
+}
+
+line_info::line_info(void)
+	: _m_priv(new impl)
+{
+
+}
+
+GPIOD_CXX_API line_info::line_info(const line_info& other) noexcept
+	: _m_priv(other._m_priv)
+{
+
+}
+
+GPIOD_CXX_API line_info::line_info(line_info&& other) noexcept
+	: _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API line_info::~line_info(void)
+{
+
+}
+
+GPIOD_CXX_API line_info& line_info::operator=(const line_info& other) noexcept
+{
+	this->_m_priv = other._m_priv;
+
+	return *this;
+}
+
+GPIOD_CXX_API line_info& line_info::operator=(line_info&& other) noexcept
+{
+	this->_m_priv = ::std::move(other._m_priv);
+
+	return *this;
+}
+
+GPIOD_CXX_API line::offset line_info::offset(void) const noexcept
+{
+	return ::gpiod_line_info_get_offset(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API ::std::string line_info::name(void) const noexcept
+{
+	const char* name = ::gpiod_line_info_get_name(this->_m_priv->info.get());
+
+	return name ?: "";
+}
+
+GPIOD_CXX_API bool line_info::used(void) const noexcept
+{
+	return ::gpiod_line_info_is_used(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API ::std::string line_info::consumer(void) const noexcept
+{
+	const char* consumer = ::gpiod_line_info_get_consumer(this->_m_priv->info.get());
+
+	return consumer ?: "";
+}
+
+GPIOD_CXX_API line::direction line_info::direction(void) const
+{
+	int direction = ::gpiod_line_info_get_direction(this->_m_priv->info.get());
+
+	return map_int_to_enum(direction, direction_mapping);
+}
+
+GPIOD_CXX_API bool line_info::active_low(void) const noexcept
+{
+	return ::gpiod_line_info_is_active_low(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API line::bias line_info::bias(void) const
+{
+	int bias = ::gpiod_line_info_get_bias(this->_m_priv->info.get());
+
+	return bias_mapping.at(bias);
+}
+
+GPIOD_CXX_API line::drive line_info::drive(void) const
+{
+	int drive = ::gpiod_line_info_get_drive(this->_m_priv->info.get());
+
+	return drive_mapping.at(drive);
+}
+
+GPIOD_CXX_API line::edge line_info::edge_detection(void) const
+{
+	int edge = ::gpiod_line_info_get_edge_detection(this->_m_priv->info.get());
+
+	return edge_mapping.at(edge);
+}
+
+GPIOD_CXX_API line::clock line_info::event_clock(void) const
+{
+	int clock = ::gpiod_line_info_get_event_clock(this->_m_priv->info.get());
+
+	return clock_mapping.at(clock);
+}
+
+GPIOD_CXX_API bool line_info::debounced(void) const  noexcept
+{
+	return ::gpiod_line_info_is_debounced(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API ::std::chrono::microseconds line_info::debounce_period(void) const  noexcept
+{
+	return ::std::chrono::microseconds(
+			::gpiod_line_info_get_debounce_period_us(this->_m_priv->info.get()));
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line_info& info)
+{
+	::std::string name, consumer;
+
+	name = info.name().empty() ? "unnamed" : ::std::string("'") + info.name() + "'";
+	consumer = info.consumer().empty() ? "unused" : ::std::string("'") + info.name() + "'";
+
+	out << "line_info(offset=" << info.offset() <<
+	       ", name=" << name <<
+	       ", used=" << ::std::boolalpha << info.used() <<
+	       ", consumer=" << consumer <<
+	       ", direction=" << info.direction() <<
+	       ", active_low=" << ::std::boolalpha << info.active_low() <<
+	       ", bias=" << info.bias() <<
+	       ", drive=" << info.drive() <<
+	       ", edge_detection=" << info.edge_detection() <<
+	       ", event_clock=" << info.event_clock() <<
+	       ", debounced=" << ::std::boolalpha << info.debounced();
+
+	if (info.debounced())
+		out << ", debounce_period=" << info.debounce_period().count() << "us";
+
+	out << ")";
+
+	return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/line-request.cpp b/bindings/cxx/line-request.cpp
new file mode 100644
index 0000000..8574e68
--- /dev/null
+++ b/bindings/cxx/line-request.cpp
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <iterator>
+#include <utility>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+void line_request::impl::throw_if_released(void) const
+{
+	if (!this->request)
+		throw request_released("GPIO lines have been released");
+}
+
+void line_request::impl::set_request_ptr(line_request_ptr& ptr)
+{
+	this->request = ::std::move(ptr);
+	this->offset_buf.resize(::gpiod_line_request_get_num_lines(this->request.get()));
+}
+
+void line_request::impl::fill_offset_buf(const line::offsets& offsets)
+{
+	for (unsigned int i = 0; i < offsets.size(); i++)
+		this->offset_buf[i] = offsets[i];
+}
+
+line_request::line_request(void)
+	: _m_priv(new impl)
+{
+
+}
+
+GPIOD_CXX_API line_request::line_request(line_request&& other) noexcept
+	: _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API line_request::~line_request(void)
+{
+
+}
+
+GPIOD_CXX_API line_request& line_request::operator=(line_request&& other) noexcept
+{
+	this->_m_priv = ::std::move(other._m_priv);
+
+	return *this;
+}
+
+GPIOD_CXX_API line_request::operator bool(void) const noexcept
+{
+	return this->_m_priv->request.get() != nullptr;
+}
+
+GPIOD_CXX_API void line_request::release(void)
+{
+	this->_m_priv->throw_if_released();
+
+	this->_m_priv->request.reset();
+}
+
+GPIOD_CXX_API ::std::size_t line_request::num_lines(void) const
+{
+	this->_m_priv->throw_if_released();
+
+	return ::gpiod_line_request_get_num_lines(this->_m_priv->request.get());
+}
+
+GPIOD_CXX_API line::offsets line_request::offsets(void) const
+{
+	this->_m_priv->throw_if_released();
+
+	::std::vector<unsigned int> buf(this->num_lines());
+	line::offsets offsets(this->num_lines());
+
+	::gpiod_line_request_get_offsets(this->_m_priv->request.get(), buf.data());
+
+	for (unsigned int i = 0; i < this->num_lines(); i++)
+		offsets[i] = buf[i];
+
+	return offsets;
+}
+
+GPIOD_CXX_API line::value line_request::get_value(line::offset offset)
+{
+	return this->get_values({ offset }).front();
+}
+
+GPIOD_CXX_API line::values
+line_request::get_values(const line::offsets& offsets)
+{
+	line::values vals(offsets.size());
+
+	this->get_values(offsets, vals);
+
+	return vals;
+}
+
+GPIOD_CXX_API line::values line_request::get_values(void)
+{
+	return this->get_values(this->offsets());
+}
+
+GPIOD_CXX_API void line_request::get_values(const line::offsets& offsets, line::values& values)
+{
+	this->_m_priv->throw_if_released();
+
+	if (offsets.size() != values.size())
+		throw ::std::invalid_argument("values must have the same size as the offsets");
+
+	this->_m_priv->fill_offset_buf(offsets);
+
+	int ret = ::gpiod_line_request_get_values_subset(this->_m_priv->request.get(),
+							 offsets.size(), this->_m_priv->offset_buf.data(),
+							 reinterpret_cast<int*>(values.data()));
+	if (ret)
+		throw_from_errno("unable to retrieve line values");
+}
+
+GPIOD_CXX_API void line_request::get_values(line::values& values)
+{
+	this->get_values(this->offsets(), values);
+}
+
+GPIOD_CXX_API void line_request::line_request::set_value(line::offset offset, line::value value)
+{
+	this->set_values({ offset }, { value });
+}
+
+GPIOD_CXX_API void
+line_request::set_values(const line::value_mappings& values)
+{
+	line::offsets offsets(values.size());
+	line::values vals(values.size());
+
+	for (unsigned int i = 0; i < values.size(); i++) {
+		offsets[i] = values[i].first;
+		vals[i] = values[i].second;
+	}
+
+	this->set_values(offsets, vals);
+}
+
+GPIOD_CXX_API void line_request::set_values(const line::offsets& offsets,
+					    const line::values& values)
+{
+	this->_m_priv->throw_if_released();
+
+	if (offsets.size() != values.size())
+		throw ::std::invalid_argument("values must have the same size as the offsets");
+
+	this->_m_priv->fill_offset_buf(offsets);
+
+	int ret = ::gpiod_line_request_set_values_subset(this->_m_priv->request.get(),
+							 offsets.size(), this->_m_priv->offset_buf.data(),
+							 reinterpret_cast<const int*>(values.data()));
+	if (ret)
+		throw_from_errno("unable to set line values");
+}
+
+GPIOD_CXX_API void line_request::set_values(const line::values& values)
+{
+	this->set_values(this->offsets(), values);
+}
+
+GPIOD_CXX_API void line_request::reconfigure_lines(const line_config& config)
+{
+	this->_m_priv->throw_if_released();
+
+	int ret = ::gpiod_line_request_reconfigure_lines(this->_m_priv->request.get(),
+							 config._m_priv->config.get());
+	if (ret)
+		throw_from_errno("unable to reconfigure GPIO lines");
+}
+
+GPIOD_CXX_API int line_request::fd(void) const
+{
+	this->_m_priv->throw_if_released();
+
+	return ::gpiod_line_request_get_fd(this->_m_priv->request.get());
+}
+
+GPIOD_CXX_API bool line_request::wait_edge_event(const ::std::chrono::nanoseconds& timeout) const
+{
+	this->_m_priv->throw_if_released();
+
+	int ret = ::gpiod_line_request_wait_edge_event(this->_m_priv->request.get(),
+						       timeout.count());
+	if (ret < 0)
+		throw_from_errno("error waiting for edge events");
+
+	return ret;
+}
+
+GPIOD_CXX_API ::std::size_t line_request::read_edge_event(edge_event_buffer& buffer)
+{
+	return this->read_edge_event(buffer, buffer.capacity());
+}
+
+GPIOD_CXX_API ::std::size_t
+line_request::read_edge_event(edge_event_buffer& buffer, ::std::size_t max_events)
+{
+	this->_m_priv->throw_if_released();
+
+	return buffer._m_priv->read_events(this->_m_priv->request, max_events);
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line_request& request)
+{
+	if (!request)
+		out << "line_request(released)";
+	else
+		out << "line_request(num_lines=" << request.num_lines() <<
+		       ", line_offsets=[" << request.offsets() <<
+		       "], fd=" << request.fd() <<
+		       ")";
+
+	return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/line.cpp b/bindings/cxx/line.cpp
index cfcf2fb..bbeb5aa 100644
--- a/bindings/cxx/line.cpp
+++ b/bindings/cxx/line.cpp
@@ -1,321 +1,128 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <gpiod.hpp>
-#include <array>
-#include <map>
-#include <system_error>
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
 
 #include "internal.hpp"
 
 namespace gpiod {
+namespace line {
 
 namespace {
 
-const ::std::map<int, int> drive_mapping = {
-	{ GPIOD_LINE_DRIVE_PUSH_PULL,	line::DRIVE_PUSH_PULL, },
-	{ GPIOD_LINE_DRIVE_OPEN_DRAIN,	line::DRIVE_OPEN_DRAIN, },
-	{ GPIOD_LINE_DRIVE_OPEN_SOURCE,	line::DRIVE_OPEN_SOURCE, },
+const ::std::map<line::value, ::std::string> value_names = {
+	{ line::value::INACTIVE,	"INACTIVE" },
+	{ line::value::ACTIVE,		"ACTIVE" }
 };
 
-const ::std::map<int, int> bias_mapping = {
-	{ GPIOD_LINE_BIAS_UNKNOWN,	line::BIAS_UNKNOWN, },
-	{ GPIOD_LINE_BIAS_DISABLED,	line::BIAS_DISABLED, },
-	{ GPIOD_LINE_BIAS_PULL_UP,	line::BIAS_PULL_UP, },
-	{ GPIOD_LINE_BIAS_PULL_DOWN,	line::BIAS_PULL_DOWN, },
+const ::std::map<line::direction, ::std::string> direction_names = {
+	{ line::direction::AS_IS,	"AS_IS" },
+	{ line::direction::INPUT,	"INPUT" },
+	{ line::direction::OUTPUT,	"OUTPUT" }
 };
 
-} /* namespace */
-
-GPIOD_CXX_API line::line(void)
-	: _m_line(nullptr),
-	  _m_owner()
-{
-
-}
-
-GPIOD_CXX_API line::line(::gpiod_line* line, const chip& owner)
-	: _m_line(line),
-	  _m_owner(owner._m_chip)
-{
-
-}
-
-GPIOD_CXX_API unsigned int line::offset(void) const
-{
-	this->throw_if_null();
-	line::chip_guard lock_chip(*this);
-
-	return ::gpiod_line_offset(this->_m_line);
-}
-
-GPIOD_CXX_API ::std::string line::name(void) const
-{
-	this->throw_if_null();
-	line::chip_guard lock_chip(*this);
-
-	const char* name = ::gpiod_line_name(this->_m_line);
-
-	return name ? ::std::string(name) : ::std::string();
-}
-
-GPIOD_CXX_API ::std::string line::consumer(void) const
-{
-	this->throw_if_null();
-	line::chip_guard lock_chip(*this);
-
-	const char* consumer = ::gpiod_line_consumer(this->_m_line);
-
-	return consumer ? ::std::string(consumer) : ::std::string();
-}
-
-GPIOD_CXX_API int line::direction(void) const
-{
-	this->throw_if_null();
-	line::chip_guard lock_chip(*this);
-
-	int dir = ::gpiod_line_direction(this->_m_line);
-
-	return dir == GPIOD_LINE_DIRECTION_INPUT ? DIRECTION_INPUT : DIRECTION_OUTPUT;
-}
-
-GPIOD_CXX_API bool line::is_active_low(void) const
-{
-	this->throw_if_null();
-	line::chip_guard lock_chip(*this);
-
-	return ::gpiod_line_is_active_low(this->_m_line);
-}
-
-GPIOD_CXX_API int line::bias(void) const
-{
-	this->throw_if_null();
-	line::chip_guard lock_chip(*this);
-
-	return bias_mapping.at(::gpiod_line_bias(this->_m_line));
-}
-
-GPIOD_CXX_API bool line::is_used(void) const
-{
-	this->throw_if_null();
-	line::chip_guard lock_chip(*this);
-
-	return ::gpiod_line_is_used(this->_m_line);
-}
-
-GPIOD_CXX_API int line::drive(void) const
-{
-	this->throw_if_null();
-	line::chip_guard lock_chip(*this);
-
-	return drive_mapping.at(::gpiod_line_drive(this->_m_line));
-}
-
-GPIOD_CXX_API void line::request(const line_request& config, int default_val) const
-{
-	this->throw_if_null();
-
-	line_bulk bulk({ *this });
-
-	bulk.request(config, { default_val });
-}
-
-GPIOD_CXX_API void line::release(void) const
-{
-	this->throw_if_null();
-
-	line_bulk bulk({ *this });
-
-	bulk.release();
-}
-
-/*
- * REVISIT: Check the performance of get/set_value & event_wait compared to
- * the C API. Creating a line_bulk object involves a memory allocation every
- * time this method if called. If the performance is significantly lower,
- * switch to calling the C functions for setting/getting line values and
- * polling for events on single lines directly.
- */
-
-GPIOD_CXX_API int line::get_value(void) const
-{
-	this->throw_if_null();
-
-	line_bulk bulk({ *this });
-
-	return bulk.get_values()[0];
-}
-
-GPIOD_CXX_API void line::set_value(int val) const
-{
-	this->throw_if_null();
-
-	line_bulk bulk({ *this });
-
-	bulk.set_values({ val });
-}
-
-GPIOD_CXX_API void line::set_config(int direction, ::std::bitset<32> flags,
-				    int value) const
-{
-	this->throw_if_null();
-
-	line_bulk bulk({ *this });
-
-	bulk.set_config(direction, flags, { value });
-}
-
-GPIOD_CXX_API void line::set_flags(::std::bitset<32> flags) const
-{
-	this->throw_if_null();
-
-	line_bulk bulk({ *this });
+const ::std::map<line::bias, ::std::string> bias_names = {
+	{ line::bias::AS_IS,		"AS_IS" },
+	{ line::bias::UNKNOWN,		"UNKNOWN" },
+	{ line::bias::DISABLED,		"DISABLED" },
+	{ line::bias::PULL_UP,		"PULL_UP" },
+	{ line::bias::PULL_DOWN,	"PULL_DOWN" }
+};
 
-	bulk.set_flags(flags);
-}
+const ::std::map<line::drive, ::std::string> drive_names = {
+	{ line::drive::PUSH_PULL,	"PUSH/PULL" },
+	{ line::drive::OPEN_DRAIN,	"OPEN_DRAIN" },
+	{ line::drive::OPEN_SOURCE,	"OPEN_SOURCE" }
+};
 
-GPIOD_CXX_API void line::set_direction_input() const
-{
-	this->throw_if_null();
+const ::std::map<line::edge, ::std::string> edge_names = {
+	{ line::edge::NONE,		"NONE" },
+	{ line::edge::RISING,		"RISING_EDGE" },
+	{ line::edge::FALLING,		"FALLING_EDGE" },
+	{ line::edge::BOTH,		"BOTH_EDGES" }
+};
 
-	line_bulk bulk({ *this });
+const ::std::map<line::clock, ::std::string> clock_names = {
+	{ line::clock::MONOTONIC,	"MONOTONIC" },
+	{ line::clock::REALTIME,	"REALTIME" }
+};
 
-	bulk.set_direction_input();
-}
+} /* namespace */
 
-GPIOD_CXX_API void line::set_direction_output(int value) const
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::value val)
 {
-	this->throw_if_null();
+	out << value_names.at(val);
 
-	line_bulk bulk({ *this });
-
-	bulk.set_direction_output({ value });
+	return out;
 }
 
-GPIOD_CXX_API bool line::event_wait(const ::std::chrono::nanoseconds& timeout) const
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::direction dir)
 {
-	this->throw_if_null();
-
-	line_bulk bulk({ *this });
-
-	line_bulk event_bulk = bulk.event_wait(timeout);
+	out << direction_names.at(dir);
 
-	return !!event_bulk;
+	return out;
 }
 
-GPIOD_CXX_API line_event line::make_line_event(const ::gpiod_line_event& event) const noexcept
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::edge edge)
 {
-	line_event ret;
+	out << edge_names.at(edge);
 
-	if (event.event_type == GPIOD_LINE_EVENT_RISING_EDGE)
-		ret.event_type = line_event::RISING_EDGE;
-	else if (event.event_type == GPIOD_LINE_EVENT_FALLING_EDGE)
-		ret.event_type = line_event::FALLING_EDGE;
-
-	ret.timestamp = ::std::chrono::duration_cast<::std::chrono::nanoseconds>(
-				::std::chrono::seconds(event.ts.tv_sec)) +
-				::std::chrono::nanoseconds(event.ts.tv_nsec);
-
-	ret.source = *this;
-
-	return ret;
+	return out;
 }
 
-GPIOD_CXX_API line_event line::event_read(void) const
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::bias bias)
 {
-	this->throw_if_null();
-	line::chip_guard lock_chip(*this);
+	out << bias_names.at(bias);
 
-	::gpiod_line_event event_buf;
-	line_event event;
-	int rv;
-
-	rv = ::gpiod_line_event_read(this->_m_line, ::std::addressof(event_buf));
-	if (rv < 0)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "error reading line event");
-
-	return this->make_line_event(event_buf);
+	return out;
 }
 
-GPIOD_CXX_API ::std::vector<line_event> line::event_read_multiple(void) const
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::drive drive)
 {
-	this->throw_if_null();
-	line::chip_guard lock_chip(*this);
-
-	/* 16 is the maximum number of events stored in the kernel FIFO. */
-	::std::array<::gpiod_line_event, 16> event_buf;
-	::std::vector<line_event> events;
-	int rv;
+	out << drive_names.at(drive);
 
-	rv = ::gpiod_line_event_read_multiple(this->_m_line,
-					      event_buf.data(), event_buf.size());
-	if (rv < 0)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "error reading multiple line events");
-
-	events.reserve(rv);
-	for (int i = 0; i < rv; i++)
-		events.push_back(this->make_line_event(event_buf[i]));
-
-	return events;
+	return out;
 }
 
-GPIOD_CXX_API int line::event_get_fd(void) const
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::clock clock)
 {
-	this->throw_if_null();
-	line::chip_guard lock_chip(*this);
-
-	int ret = ::gpiod_line_event_get_fd(this->_m_line);
-
-	if (ret < 0)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "unable to get the line event file descriptor");
-
-	return ret;
-}
+	out << clock_names.at(clock);
 
-GPIOD_CXX_API const chip line::get_chip(void) const
-{
-	return chip(this->_m_owner);
+	return out;
 }
 
-GPIOD_CXX_API void line::reset(void)
+template<typename T>
+::std::ostream& insert_vector(::std::ostream& out,
+			      const ::std::string& name, const ::std::vector<T>& vec)
 {
-	this->_m_line = nullptr;
-	this->_m_owner.reset();
-}
+	out << name << "([";
+	::std::copy(vec.begin(), ::std::prev(vec.end()),
+		    ::std::ostream_iterator<T>(out, ", "));
+	out << vec.back();
+	out << "])";
 
-GPIOD_CXX_API bool line::operator==(const line& rhs) const noexcept
-{
-	return this->_m_line == rhs._m_line;
+	return out;
 }
 
-GPIOD_CXX_API bool line::operator!=(const line& rhs) const noexcept
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const offsets& offs)
 {
-	return this->_m_line != rhs._m_line;
+	return insert_vector(out, "offsets", offs);
 }
 
-GPIOD_CXX_API line::operator bool(void) const noexcept
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line::values& vals)
 {
-	return this->_m_line != nullptr;
+	return insert_vector(out, "values", vals);
 }
 
-GPIOD_CXX_API bool line::operator!(void) const noexcept
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line::value_mapping& mapping)
 {
-	return this->_m_line == nullptr;
-}
+	out << "(" << mapping.first << ", " << mapping.second << ")";
 
-GPIOD_CXX_API void line::throw_if_null(void) const
-{
-	if (!this->_m_line)
-		throw ::std::logic_error("object not holding a GPIO line handle");
+	return out;
 }
 
-GPIOD_CXX_API line::chip_guard::chip_guard(const line& line)
-	: _m_chip(line._m_owner)
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line::value_mappings& mappings)
 {
-
+	return insert_vector(out, "value_mappings", mappings);
 }
 
+} /* namespace line */
 } /* namespace gpiod */
diff --git a/bindings/cxx/line_bulk.cpp b/bindings/cxx/line_bulk.cpp
deleted file mode 100644
index a9261c0..0000000
--- a/bindings/cxx/line_bulk.cpp
+++ /dev/null
@@ -1,366 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <gpiod.hpp>
-#include <map>
-#include <system_error>
-
-#include "internal.hpp"
-
-namespace gpiod {
-
-GPIOD_CXX_API const ::std::bitset<32> line_request::FLAG_ACTIVE_LOW(GPIOD_BIT(0));
-GPIOD_CXX_API const ::std::bitset<32> line_request::FLAG_OPEN_SOURCE(GPIOD_BIT(1));
-GPIOD_CXX_API const ::std::bitset<32> line_request::FLAG_OPEN_DRAIN(GPIOD_BIT(2));
-GPIOD_CXX_API const ::std::bitset<32> line_request::FLAG_BIAS_DISABLED(GPIOD_BIT(3));
-GPIOD_CXX_API const ::std::bitset<32> line_request::FLAG_BIAS_PULL_DOWN(GPIOD_BIT(4));
-GPIOD_CXX_API const ::std::bitset<32> line_request::FLAG_BIAS_PULL_UP(GPIOD_BIT(5));
-
-namespace {
-
-const ::std::map<int, int> reqtype_mapping = {
-	{ line_request::DIRECTION_AS_IS,	GPIOD_LINE_REQUEST_DIRECTION_AS_IS, },
-	{ line_request::DIRECTION_INPUT,	GPIOD_LINE_REQUEST_DIRECTION_INPUT, },
-	{ line_request::DIRECTION_OUTPUT,	GPIOD_LINE_REQUEST_DIRECTION_OUTPUT, },
-	{ line_request::EVENT_FALLING_EDGE,	GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE, },
-	{ line_request::EVENT_RISING_EDGE,	GPIOD_LINE_REQUEST_EVENT_RISING_EDGE, },
-	{ line_request::EVENT_BOTH_EDGES,	GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES, },
-};
-
-struct bitset_cmp
-{
-	bool operator()(const ::std::bitset<32>& lhs, const ::std::bitset<32>& rhs) const
-	{
-		return lhs.to_ulong() < rhs.to_ulong();
-	}
-};
-
-const ::std::map<::std::bitset<32>, int, bitset_cmp> reqflag_mapping = {
-	{ line_request::FLAG_ACTIVE_LOW,	GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW, },
-	{ line_request::FLAG_OPEN_DRAIN,	GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN, },
-	{ line_request::FLAG_OPEN_SOURCE,	GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE, },
-	{ line_request::FLAG_BIAS_DISABLED,	GPIOD_LINE_REQUEST_FLAG_BIAS_DISABLED, },
-	{ line_request::FLAG_BIAS_PULL_DOWN,	GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_DOWN, },
-	{ line_request::FLAG_BIAS_PULL_UP,	GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP, },
-};
-
-} /* namespace */
-
-GPIOD_CXX_API const unsigned int line_bulk::MAX_LINES = 64;
-
-GPIOD_CXX_API line_bulk::line_bulk(const ::std::vector<line>& lines)
-	: _m_bulk()
-{
-	this->_m_bulk.reserve(lines.size());
-
-	for (auto& it: lines)
-		this->append(it);
-}
-
-GPIOD_CXX_API void line_bulk::append(const line& new_line)
-{
-	if (!new_line)
-		throw ::std::logic_error("line_bulk cannot hold empty line objects");
-
-	if (this->_m_bulk.size() >= MAX_LINES)
-		throw ::std::logic_error("maximum number of lines reached");
-
-	if (this->_m_bulk.size() >= 1 && this->_m_bulk.begin()->get_chip() != new_line.get_chip())
-		throw ::std::logic_error("line_bulk cannot hold GPIO lines from different chips");
-
-	this->_m_bulk.push_back(new_line);
-}
-
-GPIOD_CXX_API line& line_bulk::get(unsigned int index)
-{
-	return this->_m_bulk.at(index);
-}
-
-GPIOD_CXX_API line& line_bulk::operator[](unsigned int index)
-{
-	return this->_m_bulk[index];
-}
-
-GPIOD_CXX_API unsigned int line_bulk::size(void) const noexcept
-{
-	return this->_m_bulk.size();
-}
-
-GPIOD_CXX_API bool line_bulk::empty(void) const noexcept
-{
-	return this->_m_bulk.empty();
-}
-
-GPIOD_CXX_API void line_bulk::clear(void)
-{
-	this->_m_bulk.clear();
-}
-
-GPIOD_CXX_API void line_bulk::request(const line_request& config, const ::std::vector<int> default_vals) const
-{
-	this->throw_if_empty();
-	line::chip_guard lock_chip(this->_m_bulk.front());
-
-	if (!default_vals.empty() && this->size() != default_vals.size())
-		throw ::std::invalid_argument("the number of default values must correspond with the number of lines");
-
-	::gpiod_line_request_config conf;
-	auto bulk = this->to_line_bulk();
-	int rv;
-
-	conf.consumer = config.consumer.c_str();
-	conf.request_type = reqtype_mapping.at(config.request_type);
-	conf.flags = 0;
-
-	for (auto& it: reqflag_mapping) {
-		if ((it.first & config.flags).to_ulong())
-			conf.flags |= it.second;
-	}
-
-	rv = ::gpiod_line_request_bulk(bulk.get(),
-				       ::std::addressof(conf),
-				       default_vals.empty() ? NULL : default_vals.data());
-	if (rv)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "error requesting GPIO lines");
-}
-
-GPIOD_CXX_API void line_bulk::release(void) const
-{
-	this->throw_if_empty();
-	line::chip_guard lock_chip(this->_m_bulk.front());
-
-	auto bulk = this->to_line_bulk();
-
-	::gpiod_line_release_bulk(bulk.get());
-}
-
-GPIOD_CXX_API ::std::vector<int> line_bulk::get_values(void) const
-{
-	this->throw_if_empty();
-	line::chip_guard lock_chip(this->_m_bulk.front());
-
-	auto bulk = this->to_line_bulk();
-	::std::vector<int> values;
-	int rv;
-
-	values.resize(this->_m_bulk.size());
-
-	rv = ::gpiod_line_get_value_bulk(bulk.get(), values.data());
-	if (rv)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "error reading GPIO line values");
-
-	return values;
-}
-
-GPIOD_CXX_API void line_bulk::set_values(const ::std::vector<int>& values) const
-{
-	this->throw_if_empty();
-	line::chip_guard lock_chip(this->_m_bulk.front());
-
-	if (values.size() != this->_m_bulk.size())
-		throw ::std::invalid_argument("the size of values array must correspond with the number of lines");
-
-	auto bulk = this->to_line_bulk();
-	int rv;
-
-	rv = ::gpiod_line_set_value_bulk(bulk.get(), values.data());
-	if (rv)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "error setting GPIO line values");
-}
-
-GPIOD_CXX_API void line_bulk::set_config(int direction, ::std::bitset<32> flags,
-					 const ::std::vector<int> values) const
-{
-	this->throw_if_empty();
-	line::chip_guard lock_chip(this->_m_bulk.front());
-
-	if (!values.empty() && this->_m_bulk.size() != values.size())
-		throw ::std::invalid_argument("the number of default values must correspond with the number of lines");
-
-	auto bulk = this->to_line_bulk();
-	int rv, gflags;
-
-	gflags = 0;
-
-	for (auto& it: reqflag_mapping) {
-		if ((it.first & flags).to_ulong())
-			gflags |= it.second;
-	}
-
-	rv = ::gpiod_line_set_config_bulk(bulk.get(), direction,
-					  gflags, values.data());
-	if (rv)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "error setting GPIO line config");
-}
-
-GPIOD_CXX_API void line_bulk::set_flags(::std::bitset<32> flags) const
-{
-	this->throw_if_empty();
-	line::chip_guard lock_chip(this->_m_bulk.front());
-
-	auto bulk = this->to_line_bulk();
-	int rv, gflags;
-
-	gflags = 0;
-
-	for (auto& it: reqflag_mapping) {
-		if ((it.first & flags).to_ulong())
-			gflags |= it.second;
-	}
-
-	rv = ::gpiod_line_set_flags_bulk(bulk.get(), gflags);
-	if (rv)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "error setting GPIO line flags");
-}
-
-GPIOD_CXX_API void line_bulk::set_direction_input() const
-{
-	this->throw_if_empty();
-	line::chip_guard lock_chip(this->_m_bulk.front());
-
-	auto bulk = this->to_line_bulk();
-	int rv;
-
-	rv = ::gpiod_line_set_direction_input_bulk(bulk.get());
-	if (rv)
-		throw ::std::system_error(errno, ::std::system_category(),
-			"error setting GPIO line direction to input");
-}
-
-GPIOD_CXX_API void line_bulk::set_direction_output(const ::std::vector<int>& values) const
-{
-	this->throw_if_empty();
-	line::chip_guard lock_chip(this->_m_bulk.front());
-
-	if (values.size() != this->_m_bulk.size())
-		throw ::std::invalid_argument("the size of values array must correspond with the number of lines");
-
-	auto bulk = this->to_line_bulk();
-	int rv;
-
-	rv = ::gpiod_line_set_direction_output_bulk(bulk.get(), values.data());
-	if (rv)
-		throw ::std::system_error(errno, ::std::system_category(),
-			"error setting GPIO line direction to output");
-}
-
-GPIOD_CXX_API line_bulk line_bulk::event_wait(const ::std::chrono::nanoseconds& timeout) const
-{
-	this->throw_if_empty();
-	line::chip_guard lock_chip(this->_m_bulk.front());
-
-	auto ev_bulk = this->make_line_bulk_ptr();
-	auto bulk = this->to_line_bulk();
-	::timespec ts;
-	line_bulk ret;
-	int rv;
-
-	ts.tv_sec = timeout.count() / 1000000000ULL;
-	ts.tv_nsec = timeout.count() % 1000000000ULL;
-
-	rv = ::gpiod_line_event_wait_bulk(bulk.get(), ::std::addressof(ts), ev_bulk.get());
-	if (rv < 0) {
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "error polling for events");
-	} else if (rv > 0) {
-		auto chip = this->_m_bulk[0].get_chip();
-		auto num_lines = ::gpiod_line_bulk_num_lines(ev_bulk.get());
-
-		for (unsigned int i = 0; i < num_lines; i++)
-			ret.append(line(::gpiod_line_bulk_get_line(ev_bulk.get(), i), chip));
-	}
-
-	return ret;
-}
-
-GPIOD_CXX_API line_bulk::operator bool(void) const noexcept
-{
-	return !this->_m_bulk.empty();
-}
-
-GPIOD_CXX_API bool line_bulk::operator!(void) const noexcept
-{
-	return this->_m_bulk.empty();
-}
-
-GPIOD_CXX_API line_bulk::iterator::iterator(const ::std::vector<line>::iterator& it)
-	: _m_iter(it)
-{
-
-}
-
-GPIOD_CXX_API line_bulk::iterator& line_bulk::iterator::operator++(void)
-{
-	this->_m_iter++;
-
-	return *this;
-}
-
-GPIOD_CXX_API const line& line_bulk::iterator::operator*(void) const
-{
-	return *this->_m_iter;
-}
-
-GPIOD_CXX_API const line* line_bulk::iterator::operator->(void) const
-{
-	return this->_m_iter.operator->();
-}
-
-GPIOD_CXX_API bool line_bulk::iterator::operator==(const iterator& rhs) const noexcept
-{
-	return this->_m_iter == rhs._m_iter;
-}
-
-GPIOD_CXX_API bool line_bulk::iterator::operator!=(const iterator& rhs) const noexcept
-{
-	return this->_m_iter != rhs._m_iter;
-}
-
-GPIOD_CXX_API line_bulk::iterator line_bulk::begin(void) noexcept
-{
-	return line_bulk::iterator(this->_m_bulk.begin());
-}
-
-GPIOD_CXX_API line_bulk::iterator line_bulk::end(void) noexcept
-{
-	return line_bulk::iterator(this->_m_bulk.end());
-}
-
-GPIOD_CXX_API void line_bulk::throw_if_empty(void) const
-{
-	if (this->_m_bulk.empty())
-		throw ::std::logic_error("line_bulk not holding any GPIO lines");
-}
-
-GPIOD_CXX_API line_bulk::line_bulk_ptr line_bulk::make_line_bulk_ptr(void) const
-{
-	line_bulk_ptr bulk(::gpiod_line_bulk_new(this->size()));
-
-	if (!bulk)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "unable to allocate new bulk object");
-
-	return bulk;
-}
-
-GPIOD_CXX_API line_bulk::line_bulk_ptr line_bulk::to_line_bulk(void) const
-{
-	line_bulk_ptr bulk = this->make_line_bulk_ptr();
-
-	for (auto& it: this->_m_bulk)
-		::gpiod_line_bulk_add_line(bulk.get(), it._m_line);
-
-	return bulk;
-}
-
-GPIOD_CXX_API void line_bulk::line_bulk_deleter::operator()(::gpiod_line_bulk *bulk)
-{
-	::gpiod_line_bulk_free(bulk);
-}
-
-} /* namespace gpiod */
diff --git a/bindings/cxx/misc.cpp b/bindings/cxx/misc.cpp
new file mode 100644
index 0000000..237cd14
--- /dev/null
+++ b/bindings/cxx/misc.cpp
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+GPIOD_CXX_API bool is_gpiochip_device(const ::std::filesystem::path& path)
+{
+	return ::gpiod_is_gpiochip_device(path.c_str());
+}
+
+GPIOD_CXX_API const ::std::string& version_string(void)
+{
+	static const ::std::string version(::gpiod_version_string());
+
+	return version;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/request-config.cpp b/bindings/cxx/request-config.cpp
new file mode 100644
index 0000000..2491cd9
--- /dev/null
+++ b/bindings/cxx/request-config.cpp
@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <utility>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+GPIOD_CXX_NORETURN void throw_bad_value_type(void)
+{
+	throw ::std::invalid_argument("bad value type for property");
+}
+
+request_config_ptr make_request_config(void)
+{
+	request_config_ptr config(::gpiod_request_config_new());
+	if (!config)
+		throw_from_errno("Unable to allocate the request config object");
+
+	return config;
+}
+
+::std::string get_string_from_value(const ::std::any& val)
+{
+	if (val.type() == typeid(::std::string))
+		return ::std::any_cast<::std::string>(val);
+	else if (val.type() == typeid(const char*))
+		return ::std::any_cast<const char*>(val);
+
+	throw_bad_value_type();
+}
+
+unsigned int get_unsigned_int_from_value(const ::std::any& val)
+{
+	if (val.type() == typeid(unsigned int)) {
+		return ::std::any_cast<unsigned int>(val);
+	} else if (val.type() == typeid(int)) {
+		int bufsize = ::std::any_cast<int>(val);
+		if (bufsize < 0)
+			bufsize = 0;
+
+		return static_cast<unsigned int>(bufsize);
+	}
+
+	throw_bad_value_type();
+}
+
+} /* namespace */
+
+request_config::impl::impl(void)
+	: config(make_request_config())
+{
+
+}
+
+GPIOD_CXX_API request_config::request_config(const properties& props)
+	: _m_priv(new impl)
+{
+	for (const auto& prop: props)
+		this->set_property(prop.first, prop.second);
+}
+
+GPIOD_CXX_API request_config::request_config(request_config&& other) noexcept
+	: _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API request_config::~request_config(void)
+{
+
+}
+
+GPIOD_CXX_API request_config& request_config::operator=(request_config&& other) noexcept
+{
+	this->_m_priv = ::std::move(other._m_priv);
+
+	return *this;
+}
+
+GPIOD_CXX_API void request_config::set_property(property prop, const ::std::any& val)
+{
+	switch (prop) {
+	case property::OFFSETS:
+		try {
+			this->set_offsets(::std::any_cast<line::offsets>(val));
+		} catch (const ::std::bad_any_cast& ex) {
+			throw_bad_value_type();
+		}
+		break;
+	case property::CONSUMER:
+		this->set_consumer(get_string_from_value(val));
+		break;
+	case property::EVENT_BUFFER_SIZE:
+		this->set_event_buffer_size(get_unsigned_int_from_value(val));
+		break;
+	default:
+		throw ::std::invalid_argument("unknown property");
+	}
+}
+
+GPIOD_CXX_API void request_config::set_offsets(const line::offsets& offsets) noexcept
+{
+	::std::vector<unsigned int> buf(offsets.size());
+
+	for (unsigned int i = 0; i < offsets.size(); i++)
+		buf[i] = offsets[i];
+
+	::gpiod_request_config_set_offsets(this->_m_priv->config.get(),
+					   buf.size(), buf.data());
+}
+
+GPIOD_CXX_API ::std::size_t request_config::num_offsets(void) const noexcept
+{
+	return ::gpiod_request_config_get_num_offsets(this->_m_priv->config.get());
+}
+
+GPIOD_CXX_API void
+request_config::set_consumer(const ::std::string& consumer) noexcept
+{
+	::gpiod_request_config_set_consumer(this->_m_priv->config.get(), consumer.c_str());
+}
+
+GPIOD_CXX_API ::std::string request_config::consumer(void) const noexcept
+{
+	const char* consumer = ::gpiod_request_config_get_consumer(this->_m_priv->config.get());
+
+	return consumer ?: "";
+}
+
+GPIOD_CXX_API line::offsets request_config::offsets(void) const
+{
+	line::offsets ret(this->num_offsets());
+	::std::vector<unsigned int> buf(this->num_offsets());
+
+	::gpiod_request_config_get_offsets(this->_m_priv->config.get(), buf.data());
+
+	for (unsigned int i = 0; i < this->num_offsets(); i++)
+		ret[i] = buf[i];
+
+	return ret;
+}
+
+GPIOD_CXX_API void
+request_config::set_event_buffer_size(::std::size_t event_buffer_size) noexcept
+{
+	::gpiod_request_config_set_event_buffer_size(this->_m_priv->config.get(),
+						     event_buffer_size);
+}
+
+GPIOD_CXX_API ::std::size_t request_config::event_buffer_size(void) const noexcept
+{
+	return ::gpiod_request_config_get_event_buffer_size(this->_m_priv->config.get());
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const request_config& config)
+{
+	::std::string consumer;
+
+	consumer = config.consumer().empty() ? "N/A" : ::std::string("'") + config.consumer() + "'";
+
+	out << "request_config(consumer=" << consumer <<
+	       ", num_offsets=" << config.num_offsets() <<
+	       ", offsets=(" << config.offsets() << ")" <<
+	       ", event_buffer_size=" << config.event_buffer_size() <<
+	       ")";
+
+	return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/tests/Makefile.am b/bindings/cxx/tests/Makefile.am
index cbdecdc..5ca5f6f 100644
--- a/bindings/cxx/tests/Makefile.am
+++ b/bindings/cxx/tests/Makefile.am
@@ -2,20 +2,29 @@
 # SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
 
 AM_CPPFLAGS = -I$(top_srcdir)/bindings/cxx/ -I$(top_srcdir)/include
-AM_CPPFLAGS += -I$(top_srcdir)/tests/mockup/
-AM_CPPFLAGS += -Wall -Wextra -g -std=gnu++11 $(CATCH2_CFLAGS)
+AM_CPPFLAGS += -I$(top_srcdir)/tests/gpiosim/
+AM_CPPFLAGS += -Wall -Wextra -g -std=gnu++17 $(CATCH2_CFLAGS)
+AM_CPPFLAGS += $(PROFILING_CFLAGS)
 AM_LDFLAGS = -lgpiodcxx -L$(top_builddir)/bindings/cxx/
-AM_LDFLAGS += -lgpiomockup -L$(top_builddir)/tests/mockup/
+AM_LDFLAGS += -lgpiosim -L$(top_builddir)/tests/gpiosim/
+AM_LDFLAGS += $(PROFILING_LDFLAGS)
 AM_LDFLAGS += -pthread
 
 bin_PROGRAMS = gpiod-cxx-test
 
 gpiod_cxx_test_SOURCES =			\
+		check-kernel.cpp		\
 		gpiod-cxx-test-main.cpp		\
-		gpiod-cxx-test.cpp		\
-		gpio-mockup.cpp			\
-		gpio-mockup.hpp			\
+		gpiosim.cpp			\
+		gpiosim.hpp			\
+		helpers.cpp			\
+		helpers.hpp			\
 		tests-chip.cpp			\
-		tests-event.cpp			\
-		tests-iter.cpp			\
-		tests-line.cpp
+		tests-chip-info.cpp		\
+		tests-edge-event.cpp		\
+		tests-line-config.cpp		\
+		tests-line-info.cpp		\
+		tests-line-request.cpp		\
+		tests-info-event.cpp		\
+		tests-misc.cpp			\
+		tests-request-config.cpp
diff --git a/bindings/cxx/tests/check-kernel.cpp b/bindings/cxx/tests/check-kernel.cpp
new file mode 100644
index 0000000..7ba00cc
--- /dev/null
+++ b/bindings/cxx/tests/check-kernel.cpp
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <linux/version.h>
+#include <sys/utsname.h>
+#include <system_error>
+#include <sstream>
+
+namespace {
+
+class kernel_checker
+{
+public:
+	kernel_checker(int major, int minor, int release)
+	{
+		int curr_major, curr_minor, curr_release, curr_ver, req_ver;
+		::std::string major_str, minor_str, release_str;
+		::utsname un;
+		int ret;
+
+		ret = ::uname(::std::addressof(un));
+		if (ret)
+			throw ::std::system_error(errno, ::std::system_category(),
+						  "unable to read the kernel version");
+
+		::std::stringstream ver_stream(::std::string(un.release));
+		::std::getline(ver_stream, major_str, '.');
+		::std::getline(ver_stream, minor_str, '.');
+		::std::getline(ver_stream, release_str, '-');
+
+		curr_major = ::std::stoi(major_str, nullptr, 0);
+		curr_minor = ::std::stoi(minor_str, nullptr, 0);
+		curr_release = ::std::stoi(release_str, nullptr, 0);
+
+		curr_ver = KERNEL_VERSION(curr_major, curr_minor, curr_release);
+		req_ver = KERNEL_VERSION(major, minor, release);
+
+		if (curr_ver < req_ver)
+			throw ::std::runtime_error("kernel release must be at least: " +
+						   ::std::to_string(major) + "." +
+						   ::std::to_string(minor) + "." +
+						   ::std::to_string(release));
+	}
+};
+
+kernel_checker require_kernel(5, 16, 0);
+
+} /* namespace */
diff --git a/bindings/cxx/tests/gpio-mockup.cpp b/bindings/cxx/tests/gpio-mockup.cpp
deleted file mode 100644
index 2e99dd4..0000000
--- a/bindings/cxx/tests/gpio-mockup.cpp
+++ /dev/null
@@ -1,153 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <system_error>
-
-#include "gpio-mockup.hpp"
-
-namespace gpiod {
-namespace test {
-
-const ::std::bitset<32> mockup::FLAG_NAMED_LINES("1");
-
-mockup& mockup::instance(void)
-{
-	static mockup mockup;
-
-	return mockup;
-}
-
-mockup::mockup(void) : _m_mockup(::gpio_mockup_new())
-{
-	if (!this->_m_mockup)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "unable to create the gpio-mockup context");
-}
-
-mockup::~mockup(void)
-{
-	::gpio_mockup_unref(this->_m_mockup);
-}
-
-void mockup::probe(const ::std::vector<unsigned int>& chip_sizes,
-		   const ::std::bitset<32>& flags)
-{
-	int ret, probe_flags = 0;
-
-	if (flags.to_ulong() & FLAG_NAMED_LINES.to_ulong())
-		probe_flags |= GPIO_MOCKUP_FLAG_NAMED_LINES;
-
-	ret = ::gpio_mockup_probe(this->_m_mockup, chip_sizes.size(),
-				  chip_sizes.data(), probe_flags);
-	if (ret)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "unable to probe gpio-mockup module");
-}
-
-void mockup::remove(void)
-{
-	int ret = ::gpio_mockup_remove(this->_m_mockup);
-	if (ret)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "unable to remove gpio-mockup module");
-}
-
-::std::string mockup::chip_name(unsigned int idx) const
-{
-	const char *name = ::gpio_mockup_chip_name(this->_m_mockup, idx);
-	if (!name)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "unable to retrieve the chip name");
-
-	return ::std::string(name);
-}
-
-::std::string mockup::chip_path(unsigned int idx) const
-{
-	const char *path = ::gpio_mockup_chip_path(this->_m_mockup, idx);
-	if (!path)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "unable to retrieve the chip path");
-
-	return ::std::string(path);
-}
-
-unsigned int mockup::chip_num(unsigned int idx) const
-{
-	int num = ::gpio_mockup_chip_num(this->_m_mockup, idx);
-	if (num < 0)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "unable to retrieve the chip number");
-
-	return num;
-}
-
-int mockup::chip_get_value(unsigned int chip_idx, unsigned int line_offset)
-{
-	int val = ::gpio_mockup_get_value(this->_m_mockup, chip_idx, line_offset);
-	if (val < 0)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "error reading the line value");
-
-	return val;
-}
-
-void mockup::chip_set_pull(unsigned int chip_idx, unsigned int line_offset, int pull)
-{
-	int ret = ::gpio_mockup_set_pull(this->_m_mockup, chip_idx, line_offset, pull);
-	if (ret)
-		throw ::std::system_error(errno, ::std::system_category(),
-					  "error setting line pull");
-}
-
-mockup::probe_guard::probe_guard(const ::std::vector<unsigned int>& chip_sizes,
-				 const ::std::bitset<32>& flags)
-{
-	mockup::instance().probe(chip_sizes, flags);
-}
-
-mockup::probe_guard::~probe_guard(void)
-{
-	mockup::instance().remove();
-}
-
-mockup::event_thread::event_thread(unsigned int chip_index,
-				   unsigned int line_offset, unsigned int period_ms)
-	: _m_chip_index(chip_index),
-	  _m_line_offset(line_offset),
-	  _m_period_ms(period_ms),
-	  _m_stop(false),
-	  _m_mutex(),
-	  _m_cond(),
-	  _m_thread(&event_thread::event_worker, this)
-{
-
-}
-
-mockup::event_thread::~event_thread(void)
-{
-	::std::unique_lock<::std::mutex> lock(this->_m_mutex);
-	this->_m_stop = true;
-	this->_m_cond.notify_all();
-	lock.unlock();
-	this->_m_thread.join();
-}
-
-void mockup::event_thread::event_worker(void)
-{
-	for (unsigned int i = 0;; i++) {
-		::std::unique_lock<::std::mutex> lock(this->_m_mutex);
-
-		if (this->_m_stop)
-			break;
-
-		::std::cv_status status = this->_m_cond.wait_for(lock,
-						std::chrono::milliseconds(this->_m_period_ms));
-		if (status == ::std::cv_status::timeout)
-			mockup::instance().chip_set_pull(this->_m_chip_index,
-							 this->_m_line_offset, i % 2);
-	}
-}
-
-} /* namespace test */
-} /* namespace gpiod */
diff --git a/bindings/cxx/tests/gpio-mockup.hpp b/bindings/cxx/tests/gpio-mockup.hpp
deleted file mode 100644
index 9ca27bd..0000000
--- a/bindings/cxx/tests/gpio-mockup.hpp
+++ /dev/null
@@ -1,94 +0,0 @@
-/* SPDX-License-Identifier: LGPL-3.0-or-later */
-/* SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com> */
-
-#ifndef __GPIOD_CXX_TEST_HPP__
-#define __GPIOD_CXX_TEST_HPP__
-
-#include <bitset>
-#include <condition_variable>
-#include <gpio-mockup.h>
-#include <mutex>
-#include <string>
-#include <vector>
-#include <thread>
-
-namespace gpiod {
-namespace test {
-
-class mockup
-{
-public:
-
-	static mockup& instance(void);
-
-	mockup(const mockup& other) = delete;
-	mockup(mockup&& other) = delete;
-	mockup& operator=(const mockup& other) = delete;
-	mockup& operator=(mockup&& other) = delete;
-
-	void probe(const ::std::vector<unsigned int>& chip_sizes,
-		   const ::std::bitset<32>& flags = 0);
-	void remove(void);
-
-	std::string chip_name(unsigned int idx) const;
-	std::string chip_path(unsigned int idx) const;
-	unsigned int chip_num(unsigned int idx) const;
-
-	int chip_get_value(unsigned int chip_idx, unsigned int line_offset);
-	void chip_set_pull(unsigned int chip_idx, unsigned int line_offset, int pull);
-
-	static const ::std::bitset<32> FLAG_NAMED_LINES;
-
-	class probe_guard
-	{
-	public:
-
-		probe_guard(const ::std::vector<unsigned int>& chip_sizes,
-			    const ::std::bitset<32>& flags = 0);
-		~probe_guard(void);
-
-		probe_guard(const probe_guard& other) = delete;
-		probe_guard(probe_guard&& other) = delete;
-		probe_guard& operator=(const probe_guard& other) = delete;
-		probe_guard& operator=(probe_guard&& other) = delete;
-	};
-
-	class event_thread
-	{
-	public:
-
-		event_thread(unsigned int chip_index, unsigned int line_offset, unsigned int period_ms);
-		~event_thread(void);
-
-		event_thread(const event_thread& other) = delete;
-		event_thread(event_thread&& other) = delete;
-		event_thread& operator=(const event_thread& other) = delete;
-		event_thread& operator=(event_thread&& other) = delete;
-
-	private:
-
-		void event_worker(void);
-
-		unsigned int _m_chip_index;
-		unsigned int _m_line_offset;
-		unsigned int _m_period_ms;
-
-		bool _m_stop;
-
-		::std::mutex _m_mutex;
-		::std::condition_variable _m_cond;
-		::std::thread _m_thread;
-	};
-
-private:
-
-	mockup(void);
-	~mockup(void);
-
-	::gpio_mockup *_m_mockup;
-};
-
-} /* namespace test */
-} /* namespace gpiod */
-
-#endif /* __GPIOD_CXX_TEST_HPP__ */
diff --git a/bindings/cxx/tests/gpiod-cxx-test.cpp b/bindings/cxx/tests/gpiod-cxx-test.cpp
deleted file mode 100644
index 834f372..0000000
--- a/bindings/cxx/tests/gpiod-cxx-test.cpp
+++ /dev/null
@@ -1,55 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <linux/version.h>
-#include <sys/utsname.h>
-#include <system_error>
-#include <sstream>
-
-namespace {
-
-class kernel_checker
-{
-public:
-
-	kernel_checker(unsigned int major, unsigned int minor, unsigned int release)
-	{
-		unsigned long curr_major, curr_minor, curr_release, curr_ver, req_ver;
-		::std::string major_str, minor_str, release_str;
-		::utsname un;
-		int ret;
-
-		ret = ::uname(::std::addressof(un));
-		if (ret)
-			throw ::std::system_error(errno, ::std::system_category(),
-						  "unable to read the kernel version");
-
-		::std::stringstream ver_stream(::std::string(un.release));
-		::std::getline(ver_stream, major_str, '.');
-		::std::getline(ver_stream, minor_str, '.');
-		::std::getline(ver_stream, release_str, '.');
-
-		curr_major = ::std::stoul(major_str, nullptr, 0);
-		curr_minor = ::std::stoul(minor_str, nullptr, 0);
-		curr_release = ::std::stoul(release_str, nullptr, 0);
-
-		curr_ver = KERNEL_VERSION(curr_major, curr_minor, curr_release);
-		req_ver = KERNEL_VERSION(major, minor, release);
-
-		if (curr_ver < req_ver)
-			throw ::std::system_error(EOPNOTSUPP, ::std::system_category(),
-						  "kernel release must be at least: " +
-						  ::std::to_string(major) + "." +
-						  ::std::to_string(minor) + "." +
-						  ::std::to_string(release));
-	}
-
-	kernel_checker(const kernel_checker& other) = delete;
-	kernel_checker(kernel_checker&& other) = delete;
-	kernel_checker& operator=(const kernel_checker& other) = delete;
-	kernel_checker& operator=(kernel_checker&& other) = delete;
-};
-
-kernel_checker require_kernel(5, 10, 0);
-
-} /* namespace */
diff --git a/bindings/cxx/tests/gpiosim.cpp b/bindings/cxx/tests/gpiosim.cpp
new file mode 100644
index 0000000..739e206
--- /dev/null
+++ b/bindings/cxx/tests/gpiosim.cpp
@@ -0,0 +1,264 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#include <atomic>
+#include <functional>
+#include <map>
+#include <mutex>
+#include <system_error>
+#include <unistd.h>
+
+#include "gpiosim.h"
+#include "gpiosim.hpp"
+
+#define NORETURN __attribute__((noreturn))
+
+namespace gpiosim {
+
+namespace {
+
+const ::std::map<chip::pull, int> pull_mapping = {
+	{ chip::pull::PULL_UP,		GPIOSIM_PULL_UP },
+	{ chip::pull::PULL_DOWN,	GPIOSIM_PULL_DOWN }
+};
+
+const ::std::map<chip::hog_direction, int> hog_dir_mapping = {
+	{ chip::hog_direction::INPUT,		GPIOSIM_HOG_DIR_INPUT },
+	{ chip::hog_direction::OUTPUT_HIGH,	GPIOSIM_HOG_DIR_OUTPUT_HIGH },
+	{ chip::hog_direction::OUTPUT_LOW,	GPIOSIM_HOG_DIR_OUTPUT_LOW }
+};
+
+const ::std::map<int, chip::value> value_mapping = {
+	{ GPIOSIM_VALUE_INACTIVE,	chip::value::INACTIVE },
+	{ GPIOSIM_VALUE_ACTIVE,		chip::value::ACTIVE }
+};
+
+template<class gpiosim_type, void free_func(gpiosim_type*)> struct deleter
+{
+	void operator()(gpiosim_type* ptr)
+	{
+		free_func(ptr);
+	}
+};
+
+using ctx_deleter = deleter<::gpiosim_ctx, ::gpiosim_ctx_unref>;
+using dev_deleter = deleter<::gpiosim_dev, ::gpiosim_dev_unref>;
+using bank_deleter = deleter<::gpiosim_bank, ::gpiosim_bank_unref>;
+
+using ctx_ptr = ::std::unique_ptr<::gpiosim_ctx, ctx_deleter>;
+using dev_ptr = ::std::unique_ptr<::gpiosim_dev, dev_deleter>;
+using bank_ptr = ::std::unique_ptr<::gpiosim_bank, bank_deleter>;
+
+ctx_ptr sim_ctx;
+::std::once_flag ctx_once;
+const ::pid_t pid = ::getpid();
+::std::atomic_uint chip_count;
+
+void init_ctx(void)
+{
+	sim_ctx.reset(gpiosim_ctx_new());
+	if (!sim_ctx)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "unable to create the GPIO simulator context");
+}
+
+dev_ptr make_sim_dev(void)
+{
+	::std::call_once(ctx_once, init_ctx);
+
+	::std::string name = "gpiod_cxx_test_dev." + ::std::to_string(pid) +
+			     "." + ::std::to_string(chip_count++);
+
+	dev_ptr dev(::gpiosim_dev_new(sim_ctx.get(), name.c_str()));
+	if (!dev)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "failed to create a new GPIO simulator device");
+
+	return dev;
+}
+
+bank_ptr make_sim_bank(const dev_ptr& dev)
+{
+	bank_ptr bank(::gpiosim_bank_new(dev.get(), "bank"));
+	if (!bank)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "failed to create a new GPIO simulator bank");
+
+	return bank;
+}
+
+NORETURN void throw_invalid_type(void)
+{
+	throw ::std::logic_error("invalid type for property");
+}
+
+unsigned any_to_unsigned_int(const ::std::any& val)
+{
+	if (val.type() == typeid(int)) {
+		auto num_lines = ::std::any_cast<int>(val);
+		if (num_lines < 0)
+			throw ::std::invalid_argument("negative value not accepted");
+
+		   return static_cast<unsigned int>(num_lines);
+	} else if (val.type() == typeid(unsigned int)) {
+		return ::std::any_cast<unsigned int>(val);
+	}
+
+	throw_invalid_type();
+}
+
+::std::string any_to_string(const ::std::any& val)
+{
+	if (val.type() == typeid(::std::string))
+		return ::std::any_cast<::std::string>(val);
+	else if (val.type() == typeid(const char*))
+		return ::std::any_cast<const char*>(val);
+
+	throw_invalid_type();
+}
+
+} /* namespace */
+
+struct chip::impl
+{
+	impl(void)
+		: dev(make_sim_dev()),
+		  bank(make_sim_bank(this->dev)),
+		  has_num_lines(false),
+		  has_label(false)
+	{
+
+	}
+
+	impl(const impl& other) = delete;
+	impl(impl&& other) = delete;
+	~impl(void) = default;
+	impl& operator=(const impl& other) = delete;
+	impl& operator=(impl&& other) = delete;
+
+	static const ::std::map<chip::property,
+				::std::function<void (impl&,
+						      const ::std::any&)>> setter_mapping;
+
+	void set_property(chip::property prop, const ::std::any& val)
+	{
+		setter_mapping.at(prop)(*this, val);
+	}
+
+	void set_num_lines(const ::std::any& val)
+	{
+		if (this->has_num_lines)
+			throw ::std::logic_error("number of lines can be set at most once");
+
+		int ret = ::gpiosim_bank_set_num_lines(this->bank.get(), any_to_unsigned_int(val));
+		if (ret)
+			throw ::std::system_error(errno, ::std::system_category(),
+						  "failed to set the number of lines");
+
+		this->has_num_lines = true;
+	}
+
+	void set_label(const ::std::any& val)
+	{
+		if (this->has_label)
+			throw ::std::logic_error("label can be set at most once");
+
+		int ret = ::gpiosim_bank_set_label(this->bank.get(),
+						   any_to_string(val).c_str());
+		if (ret)
+			throw ::std::system_error(errno, ::std::system_category(),
+						  "failed to set the chip label");
+
+		this->has_label = true;
+	}
+
+	void set_line_name(const ::std::any& val)
+	{
+		auto name = ::std::any_cast<line_name>(val);
+
+		int ret = ::gpiosim_bank_set_line_name(this->bank.get(),
+						       ::std::get<0>(name),
+						       ::std::get<1>(name).c_str());
+		if (ret)
+			throw ::std::system_error(errno, ::std::system_category(),
+						  "failed to set simulated line name");
+	}
+
+	void set_line_hog(const ::std::any& val)
+	{
+		auto hog = ::std::any_cast<line_hog>(val);
+
+		int ret = ::gpiosim_bank_hog_line(this->bank.get(),
+						  ::std::get<0>(hog),
+						  ::std::get<1>(hog).c_str(),
+						  hog_dir_mapping.at(::std::get<2>(hog)));
+		if (ret)
+			throw ::std::system_error(errno, ::std::system_category(),
+						  "failed to hog a simulated line");
+	}
+
+	dev_ptr dev;
+	bank_ptr bank;
+	bool has_num_lines;
+	bool has_label;
+};
+
+const ::std::map<chip::property,
+		 ::std::function<void (chip::impl&,
+				       const ::std::any&)>> chip::impl::setter_mapping = {
+	{ chip::property::NUM_LINES,	&chip::impl::set_num_lines },
+	{ chip::property::LABEL,	&chip::impl::set_label },
+	{ chip::property::LINE_NAME,	&chip::impl::set_line_name },
+	{ chip::property::HOG,		&chip::impl::set_line_hog }
+};
+
+chip::chip(const properties& args)
+	: _m_priv(new impl)
+{
+	int ret;
+
+	for (const auto& arg: args)
+		this->_m_priv.get()->set_property(arg.first, arg.second);
+
+	ret = ::gpiosim_dev_enable(this->_m_priv->dev.get());
+	if (ret)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "failed to enable the simulated GPIO chip");
+}
+
+chip::~chip(void)
+{
+	this->_m_priv.reset(nullptr);
+	chip_count--;
+}
+
+::std::filesystem::path chip::dev_path(void) const
+{
+	return ::gpiosim_bank_get_dev_path(this->_m_priv->bank.get());
+}
+
+::std::string chip::name(void) const
+{
+	return ::gpiosim_bank_get_chip_name(this->_m_priv->bank.get());
+}
+
+chip::value chip::get_value(unsigned int offset)
+{
+	int val = ::gpiosim_bank_get_value(this->_m_priv->bank.get(), offset);
+	if (val < 0)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "failed to read the simulated GPIO line value");
+
+	return value_mapping.at(val);
+}
+
+void chip::set_pull(unsigned int offset, pull pull)
+{
+	int ret = ::gpiosim_bank_set_pull(this->_m_priv->bank.get(),
+					  offset, pull_mapping.at(pull));
+	if (ret)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "failed to set the pull of simulated GPIO line");
+}
+
+} /* namespace gpiosim */
diff --git a/bindings/cxx/tests/gpiosim.hpp b/bindings/cxx/tests/gpiosim.hpp
new file mode 100644
index 0000000..53870c1
--- /dev/null
+++ b/bindings/cxx/tests/gpiosim.hpp
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __GPIOD_CXX_GPIOSIM_HPP__
+#define __GPIOD_CXX_GPIOSIM_HPP__
+
+#include <any>
+#include <filesystem>
+#include <memory>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+namespace gpiosim {
+
+class chip
+{
+public:
+	enum class property {
+		NUM_LINES = 1,
+		LABEL,
+		LINE_NAME,
+		HOG
+	};
+
+	enum class pull {
+		PULL_UP = 1,
+		PULL_DOWN
+	};
+
+	enum class hog_direction {
+		INPUT = 1,
+		OUTPUT_HIGH,
+		OUTPUT_LOW
+	};
+
+	enum class value {
+		INACTIVE = 0,
+		ACTIVE = 1
+	};
+
+	using line_name = ::std::tuple<unsigned int, ::std::string>;
+	using line_hog = ::std::tuple<unsigned int, ::std::string, hog_direction>;
+	using properties = ::std::vector<::std::pair<property, ::std::any>>;
+
+	explicit chip(const properties& args = properties());
+	chip(const chip& other) = delete;
+	chip(chip&& other) = delete;
+	~chip(void);
+
+	chip& operator=(const chip& other) = delete;
+	chip& operator=(chip&& other) = delete;
+
+	::std::filesystem::path dev_path(void) const;
+	::std::string name(void) const;
+
+	value get_value(unsigned int offset);
+	void set_pull(unsigned int offset, pull pull);
+
+private:
+
+	struct impl;
+
+	::std::unique_ptr<impl> _m_priv;
+};
+
+} /* namespace gpiosim */
+
+#endif /* __GPIOD_CXX_GPIOSIM_HPP__ */
diff --git a/bindings/cxx/tests/helpers.cpp b/bindings/cxx/tests/helpers.cpp
new file mode 100644
index 0000000..b82d03b
--- /dev/null
+++ b/bindings/cxx/tests/helpers.cpp
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "helpers.hpp"
+
+system_error_matcher::system_error_matcher(int expected_errno)
+	: _m_cond(::std::system_category().default_error_condition(expected_errno))
+{
+
+}
+
+::std::string system_error_matcher::describe(void) const
+{
+	return "matches: errno " + ::std::to_string(this->_m_cond.value());
+}
+
+bool system_error_matcher::match(const ::std::system_error& error) const
+{
+	return error.code().value() == this->_m_cond.value();
+}
+
+regex_matcher::regex_matcher(const ::std::string& pattern)
+	: _m_pattern(pattern),
+	  _m_repr("matches: regex \"" + pattern + "\"")
+{
+
+}
+
+::std::string regex_matcher::describe(void) const
+{
+	return this->_m_repr;
+}
+
+bool regex_matcher::match(const ::std::string& str) const
+{
+	return ::std::regex_match(str, this->_m_pattern);
+}
diff --git a/bindings/cxx/tests/helpers.hpp b/bindings/cxx/tests/helpers.hpp
new file mode 100644
index 0000000..8a421ad
--- /dev/null
+++ b/bindings/cxx/tests/helpers.hpp
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __GPIOD_CXX_TEST_HELPERS_HPP__
+#define __GPIOD_CXX_TEST_HELPERS_HPP__
+
+#include <catch2/catch.hpp>
+#include <regex>
+#include <string>
+#include <sstream>
+#include <system_error>
+
+class system_error_matcher : public Catch::MatcherBase<::std::system_error>
+{
+public:
+	explicit system_error_matcher(int expected_errno);
+	::std::string describe(void) const override;
+	bool match(const ::std::system_error& error) const override;
+
+private:
+	::std::error_condition _m_cond;
+};
+
+class regex_matcher : public Catch::MatcherBase<::std::string>
+{
+public:
+	explicit regex_matcher(const ::std::string& pattern);
+	::std::string describe(void) const override;
+	bool match(const ::std::string& str) const override;
+
+private:
+	::std::regex _m_pattern;
+	::std::string _m_repr;
+};
+
+#endif /* __GPIOD_CXX_TEST_HELPERS_HPP__ */
diff --git a/bindings/cxx/tests/tests-chip-info.cpp b/bindings/cxx/tests/tests-chip-info.cpp
new file mode 100644
index 0000000..051a6dd
--- /dev/null
+++ b/bindings/cxx/tests/tests-chip-info.cpp
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+#include <sstream>
+
+#include "gpiosim.hpp"
+
+using property = ::gpiosim::chip::property;
+
+namespace {
+
+TEST_CASE("chip_info properties can be read", "[chip-info][chip]")
+{
+	::gpiosim::chip sim({{ property::NUM_LINES, 8 }, { property::LABEL, "foobar" }});
+	::gpiod::chip chip(sim.dev_path());
+	auto info = chip.get_info();
+
+	SECTION("get chip name")
+	{
+		REQUIRE_THAT(info.name(), Catch::Equals(sim.name()));
+	}
+
+	SECTION("get chip label")
+	{
+		REQUIRE_THAT(info.label(), Catch::Equals("foobar"));
+	}
+
+	SECTION("get num_lines")
+	{
+		REQUIRE(info.num_lines() == 8);
+	}
+}
+
+TEST_CASE("chip_info can be copied and moved", "[chip-info]")
+{
+	::gpiosim::chip sim({{ property::NUM_LINES, 4 }, { property::LABEL, "foobar" }});
+	::gpiod::chip chip(sim.dev_path());
+	auto info = chip.get_info();
+
+	SECTION("copy constructor works")
+	{
+		auto copy(info);
+
+		REQUIRE_THAT(copy.name(), Catch::Equals(sim.name()));
+		REQUIRE_THAT(copy.label(), Catch::Equals("foobar"));
+		REQUIRE(copy.num_lines() == 4);
+
+		REQUIRE_THAT(info.name(), Catch::Equals(sim.name()));
+		REQUIRE_THAT(info.label(), Catch::Equals("foobar"));
+		REQUIRE(info.num_lines() == 4);
+	}
+
+	SECTION("assignment operator works")
+	{
+		auto copy = chip.get_info();
+
+		copy = info;
+
+		REQUIRE_THAT(copy.name(), Catch::Equals(sim.name()));
+		REQUIRE_THAT(copy.label(), Catch::Equals("foobar"));
+		REQUIRE(copy.num_lines() == 4);
+
+		REQUIRE_THAT(info.name(), Catch::Equals(sim.name()));
+		REQUIRE_THAT(info.label(), Catch::Equals("foobar"));
+		REQUIRE(info.num_lines() == 4);
+	}
+
+	SECTION("move constructor works")
+	{
+		auto moved(std::move(info));
+
+		REQUIRE_THAT(moved.name(), Catch::Equals(sim.name()));
+		REQUIRE_THAT(moved.label(), Catch::Equals("foobar"));
+		REQUIRE(moved.num_lines() == 4);
+	}
+
+	SECTION("move assignment operator works")
+	{
+		auto moved = chip.get_info();
+
+		moved = ::std::move(info);
+
+		REQUIRE_THAT(moved.name(), Catch::Equals(sim.name()));
+		REQUIRE_THAT(moved.label(), Catch::Equals("foobar"));
+		REQUIRE(moved.num_lines() == 4);
+	}
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-chip.cpp b/bindings/cxx/tests/tests-chip.cpp
index aea00fa..3e63ec2 100644
--- a/bindings/cxx/tests/tests-chip.cpp
+++ b/bindings/cxx/tests/tests-chip.cpp
@@ -1,173 +1,172 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
 
 #include <catch2/catch.hpp>
 #include <gpiod.hpp>
+#include <sstream>
+#include <system_error>
+#include <utility>
 
-#include "gpio-mockup.hpp"
+#include "gpiosim.hpp"
+#include "helpers.hpp"
 
-using ::gpiod::test::mockup;
+using property = ::gpiosim::chip::property;
+using line_name = ::gpiosim::chip::line_name;
 
-TEST_CASE("GPIO chip device can be verified with is_gpiochip_device()", "[chip]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
+namespace {
 
-	SECTION("good chip")
+TEST_CASE("chip constructor works", "[chip]")
+{
+	SECTION("open an existing GPIO chip")
 	{
-		REQUIRE(::gpiod::is_gpiochip_device(mockup::instance().chip_path(0)));
+		::gpiosim::chip sim;
+
+		REQUIRE_NOTHROW(::gpiod::chip(sim.dev_path()));
 	}
 
-	SECTION("not a chip")
+	SECTION("opening a nonexistent file fails with ENOENT")
 	{
-		REQUIRE_FALSE(::gpiod::is_gpiochip_device("/dev/null"));
+		REQUIRE_THROWS_MATCHES(::gpiod::chip("/dev/nonexistent"),
+				       ::std::system_error, system_error_matcher(ENOENT));
 	}
 
-	SECTION("nonexistent file")
+	SECTION("opening a file that is not a device fails with ENOTTY")
 	{
-		REQUIRE_FALSE(::gpiod::is_gpiochip_device("/dev/nonexistent_device"));
+		REQUIRE_THROWS_MATCHES(::gpiod::chip("/tmp"),
+				       ::std::system_error, system_error_matcher(ENOTTY));
 	}
-}
 
-TEST_CASE("GPIO chip device can be opened", "[chip]")
-{
-	mockup::probe_guard mockup_chips({ 8, 8, 8 });
-
-	SECTION("open from constructor")
+	SECTION("opening a non-GPIO character device fails with ENODEV")
 	{
-		REQUIRE_NOTHROW(::gpiod::chip(mockup::instance().chip_path(1)));
+		REQUIRE_THROWS_MATCHES(::gpiod::chip("/dev/null"),
+				       ::std::system_error, system_error_matcher(ENODEV));
 	}
 
-	SECTION("open from open() method")
+	SECTION("move constructor")
 	{
-		::gpiod::chip chip;
-
-		REQUIRE_FALSE(!!chip);
+		::gpiosim::chip sim({{ property::LABEL, "foobar" }});
 
-		REQUIRE_NOTHROW(chip.open(mockup::instance().chip_path(1)));
+		::gpiod::chip first(sim.dev_path());
+		REQUIRE_THAT(first.get_info().label(), Catch::Equals("foobar"));
+		::gpiod::chip second(::std::move(first));
+		REQUIRE_THAT(second.get_info().label(), Catch::Equals("foobar"));
 	}
 }
 
-TEST_CASE("Uninitialized GPIO chip behaves correctly", "[chip]")
+TEST_CASE("chip operators work", "[chip]")
 {
-	::gpiod::chip chip;
+	::gpiosim::chip sim({{ property::LABEL, "foobar" }});
+	::gpiod::chip chip(sim.dev_path());
 
-	SECTION("uninitialized chip is 'false'")
+	SECTION("assignment operator")
 	{
-		REQUIRE_FALSE(!!chip);
+		::gpiosim::chip moved_sim({{ property::LABEL, "moved" }});
+		::gpiod::chip moved_chip(moved_sim.dev_path());
+
+		REQUIRE_THAT(chip.get_info().label(), Catch::Equals("foobar"));
+		chip = ::std::move(moved_chip);
+		REQUIRE_THAT(chip.get_info().label(), Catch::Equals("moved"));
 	}
 
-	SECTION("using uninitialized chip throws logic_error")
+	SECTION("boolean operator")
 	{
-		REQUIRE_THROWS_AS(chip.name(), ::std::logic_error);
+		REQUIRE(chip);
+		chip.close();
+		REQUIRE_FALSE(chip);
 	}
 }
 
-TEST_CASE("Trying to open a nonexistent chip throws system_error", "[chip]")
+TEST_CASE("chip properties can be read", "[chip]")
 {
-	REQUIRE_THROWS_AS(::gpiod::chip("nonexistent-chip"), ::std::system_error);
-}
+	::gpiosim::chip sim({{ property::NUM_LINES, 8 }, { property::LABEL, "foobar" }});
+	::gpiod::chip chip(sim.dev_path());
 
-TEST_CASE("Chip object can be reset", "[chip]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	REQUIRE(chip);
-	chip.reset();
-	REQUIRE_FALSE(!!chip);
-}
-
-TEST_CASE("Chip info can be correctly retrieved", "[chip]")
-{
-	mockup::probe_guard mockup_chips({ 8, 16, 8 });
+	SECTION("get device path")
+	{
+		REQUIRE_THAT(chip.path(), Catch::Equals(sim.dev_path()));
+	}
 
-	::gpiod::chip chip(mockup::instance().chip_path(1));
-	REQUIRE(chip.name() == mockup::instance().chip_name(1));
-	REQUIRE(chip.label() == "gpio-mockup-B");
-	REQUIRE(chip.num_lines() == 16);
+	SECTION("get file descriptor")
+	{
+		REQUIRE(chip.fd() >= 0);
+	}
 }
 
-TEST_CASE("Chip object can be copied and compared", "[chip]")
+TEST_CASE("line lookup by name works", "[chip]")
 {
-	mockup::probe_guard mockup_chips({ 8, 8 });
-
-	::gpiod::chip chip1(mockup::instance().chip_path(0));
-	auto chip2 = chip1;
-	REQUIRE(chip1 == chip2);
-	REQUIRE_FALSE(chip1 != chip2);
-	::gpiod::chip chip3(mockup::instance().chip_path(1));
-	REQUIRE(chip1 != chip3);
-	REQUIRE_FALSE(chip2 == chip3);
-}
+	::gpiosim::chip sim({
+		{ property::NUM_LINES, 8 },
+		{ property::LINE_NAME, line_name(0, "foo") },
+		{ property::LINE_NAME, line_name(2, "bar") },
+		{ property::LINE_NAME, line_name(3, "baz") },
+		{ property::LINE_NAME, line_name(5, "xyz") }
+	});
 
-TEST_CASE("Lines can be retrieved from chip objects", "[chip]")
-{
-	mockup::probe_guard mockup_chips({ 8, 8, 8 }, mockup::FLAG_NAMED_LINES);
-	::gpiod::chip chip(mockup::instance().chip_path(1));
+	::gpiod::chip chip(sim.dev_path());
 
-	SECTION("get single line by offset")
+	SECTION("lookup successful")
 	{
-		auto line = chip.get_line(3);
-		REQUIRE(line.name() == "gpio-mockup-B-3");
+		REQUIRE(chip.find_line("baz") == 3);
 	}
 
-	SECTION("find single line by name")
+	SECTION("lookup failed")
 	{
-		auto offset = chip.find_line("gpio-mockup-B-3");
-		REQUIRE(offset == 3);
+		REQUIRE(chip.find_line("nonexistent") < 0);
 	}
+}
 
-	SECTION("get multiple lines by offsets")
-	{
-		auto lines = chip.get_lines({ 1, 3, 4, 7});
-		REQUIRE(lines.size() == 4);
-		REQUIRE(lines.get(0).name() == "gpio-mockup-B-1");
-		REQUIRE(lines.get(1).name() == "gpio-mockup-B-3");
-		REQUIRE(lines.get(2).name() == "gpio-mockup-B-4");
-		REQUIRE(lines.get(3).name() == "gpio-mockup-B-7");
-	}
+TEST_CASE("line lookup: behavior for duplicate names", "[chip]")
+{
+	::gpiosim::chip sim({
+		{ property::NUM_LINES, 8 },
+		{ property::LINE_NAME, line_name(0, "foo") },
+		{ property::LINE_NAME, line_name(2, "bar") },
+		{ property::LINE_NAME, line_name(3, "baz") },
+		{ property::LINE_NAME, line_name(5, "bar") }
+	});
 
-	SECTION("get multiple lines by offsets in mixed order")
-	{
-		auto lines = chip.get_lines({ 5, 1, 3, 2});
-		REQUIRE(lines.size() == 4);
-		REQUIRE(lines.get(0).name() == "gpio-mockup-B-5");
-		REQUIRE(lines.get(1).name() == "gpio-mockup-B-1");
-		REQUIRE(lines.get(2).name() == "gpio-mockup-B-3");
-		REQUIRE(lines.get(3).name() == "gpio-mockup-B-2");
-	}
+	::gpiod::chip chip(sim.dev_path());
+
+	REQUIRE(chip.find_line("bar") == 2);
 }
 
-TEST_CASE("All lines can be retrieved from a chip at once", "[chip]")
+TEST_CASE("closed chip can no longer be used", "[chip]")
 {
-	mockup::probe_guard mockup_chips({ 4 });
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-
-	auto lines = chip.get_all_lines();
-	REQUIRE(lines.size() == 4);
-	REQUIRE(lines.get(0).offset() == 0);
-	REQUIRE(lines.get(1).offset() == 1);
-	REQUIRE(lines.get(2).offset() == 2);
-	REQUIRE(lines.get(3).offset() == 3);
+	::gpiosim::chip sim;
+
+	::gpiod::chip chip(sim.dev_path());
+	chip.close();
+	REQUIRE_THROWS_AS(chip.path(), ::gpiod::chip_closed);
 }
 
-TEST_CASE("Errors occurring when retrieving lines are correctly reported", "[chip]")
+TEST_CASE("stream insertion operator works for chip and chip_info", "[chip][chip-info]")
 {
-	mockup::probe_guard mockup_chips({ 8 }, mockup::FLAG_NAMED_LINES);
-	::gpiod::chip chip(mockup::instance().chip_path(0));
+	::gpiosim::chip sim({
+		{ property::NUM_LINES, 4 },
+		{ property::LABEL, "foobar" }
+	});
 
-	SECTION("invalid offset (single line)")
-	{
-		REQUIRE_THROWS_AS(chip.get_line(9), ::std::out_of_range);
-	}
+	::gpiod::chip chip(sim.dev_path());
+	::std::stringstream buf;
 
-	SECTION("invalid offset (multiple lines)")
+	SECTION("open chip")
 	{
-		REQUIRE_THROWS_AS(chip.get_lines({ 1, 19, 4, 7 }), ::std::out_of_range);
+		::std::stringstream expected;
+
+		expected << "chip(path=" << sim.dev_path() <<
+			    ", info=chip_info(name=\"" << sim.name() <<
+			    "\", label=\"foobar\", num_lines=4))";
+
+		buf << chip;
+		REQUIRE_THAT(buf.str(), Catch::Equals(expected.str()));
 	}
 
-	SECTION("line not found by name")
+	SECTION("closed chip")
 	{
-		REQUIRE(chip.find_line("nonexistent-line") == -1);
+		chip.close();
+		buf << chip;
+		REQUIRE_THAT(buf.str(), Catch::Equals("chip(closed)"));
 	}
 }
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-edge-event.cpp b/bindings/cxx/tests/tests-edge-event.cpp
new file mode 100644
index 0000000..88783c6
--- /dev/null
+++ b/bindings/cxx/tests/tests-edge-event.cpp
@@ -0,0 +1,417 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <chrono>
+#include <gpiod.hpp>
+#include <sstream>
+#include <thread>
+#include <utility>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using simprop = ::gpiosim::chip::property;
+using reqprop = ::gpiod::request_config::property;
+using lineprop = ::gpiod::line_config::property;
+using direction = ::gpiod::line::direction;
+using edge = ::gpiod::line::edge;
+using offsets = ::gpiod::line::offsets;
+using pull = ::gpiosim::chip::pull;
+using event_type = ::gpiod::edge_event::event_type;
+
+namespace {
+
+TEST_CASE("edge_event_buffer capacity settings work", "[edge-event]")
+{
+	SECTION("default capacity")
+	{
+		REQUIRE(::gpiod::edge_event_buffer().capacity() == 64);
+	}
+
+	SECTION("user-defined capacity")
+	{
+		REQUIRE(::gpiod::edge_event_buffer(123).capacity() == 123);
+	}
+
+	SECTION("max capacity")
+	{
+		REQUIRE(::gpiod::edge_event_buffer(16 * 64 * 2).capacity() == 1024);
+	}
+}
+
+TEST_CASE("edge_event wait timeout", "[edge-event]")
+{
+	::gpiosim::chip sim;
+	::gpiod::chip chip(sim.dev_path());
+
+	auto request = chip.request_lines(
+		::gpiod::request_config({
+			{ reqprop::OFFSETS, offsets({ 0 })}
+		}),
+		::gpiod::line_config({
+			{ lineprop::EDGE, edge::BOTH }
+		})
+	);
+
+	REQUIRE_FALSE(request.wait_edge_event(::std::chrono::milliseconds(100)));
+}
+
+TEST_CASE("output mode and edge detection don't work together", "[edge-event]")
+{
+	::gpiosim::chip sim;
+	::gpiod::chip chip(sim.dev_path());
+
+	REQUIRE_THROWS_AS(
+		chip.request_lines(
+			::gpiod::request_config({
+				{ reqprop::OFFSETS, offsets({ 0 })}
+			}),
+			::gpiod::line_config({
+				{ lineprop::DIRECTION, direction::OUTPUT },
+				{ lineprop::EDGE, edge::BOTH }
+			})
+		),
+		::std::invalid_argument
+	);
+}
+
+void trigger_falling_and_rising_edge(::gpiosim::chip& sim, unsigned int offset)
+{
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+	sim.set_pull(offset, pull::PULL_UP);
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+	sim.set_pull(offset, pull::PULL_DOWN);
+}
+
+void trigger_rising_edge_events_on_two_offsets(::gpiosim::chip& sim,
+					       unsigned int off0, unsigned int off1)
+{
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+	sim.set_pull(off0, pull::PULL_UP);
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+	sim.set_pull(off1, pull::PULL_UP);
+}
+
+TEST_CASE("waiting for and reading edge events works", "[edge-event]")
+{
+	::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+	::gpiod::chip chip(sim.dev_path());
+	::gpiod::edge_event_buffer buffer;
+
+	SECTION("both edge events")
+	{
+		auto request = chip.request_lines(
+			::gpiod::request_config({
+				{ reqprop::OFFSETS, offsets({ 2 })}
+			}),
+			::gpiod::line_config({
+				{ lineprop::EDGE, edge::BOTH }
+			})
+		);
+
+		::std::uint64_t ts_rising, ts_falling;
+
+		::std::thread thread(trigger_falling_and_rising_edge, ::std::ref(sim), 2);
+
+		REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+		REQUIRE(request.read_edge_event(buffer, 1) == 1);
+		REQUIRE(buffer.num_events() == 1);
+		auto event = buffer.get_event(0);
+		REQUIRE(event.type() == event_type::RISING_EDGE);
+		REQUIRE(event.line_offset() == 2);
+		ts_rising = event.timestamp_ns();
+
+		REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+		REQUIRE(request.read_edge_event(buffer, 1) == 1);
+		REQUIRE(buffer.num_events() == 1);
+		event = buffer.get_event(0);
+		REQUIRE(event.type() == event_type::FALLING_EDGE);
+		REQUIRE(event.line_offset() == 2);
+		ts_falling = event.timestamp_ns();
+
+		REQUIRE_FALSE(request.wait_edge_event(::std::chrono::milliseconds(100)));
+
+		thread.join();
+
+		REQUIRE(ts_falling > ts_rising);
+	}
+
+	SECTION("rising edge event")
+	{
+		auto request = chip.request_lines(
+			::gpiod::request_config({
+				{ reqprop::OFFSETS, offsets({ 6 })}
+			}),
+			::gpiod::line_config({
+				{ lineprop::EDGE, edge::RISING }
+			})
+		);
+
+		::std::thread thread(trigger_falling_and_rising_edge, ::std::ref(sim), 6);
+
+		REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+		REQUIRE(request.read_edge_event(buffer, 1) == 1);
+		REQUIRE(buffer.num_events() == 1);
+		auto event = buffer.get_event(0);
+		REQUIRE(event.type() == event_type::RISING_EDGE);
+		REQUIRE(event.line_offset() == 6);
+
+		REQUIRE_FALSE(request.wait_edge_event(::std::chrono::milliseconds(100)));
+
+		thread.join();
+	}
+
+	SECTION("falling edge event")
+	{
+		auto request = chip.request_lines(
+			::gpiod::request_config({
+				{ reqprop::OFFSETS, offsets({ 7 })}
+			}),
+			::gpiod::line_config({
+				{ lineprop::EDGE, edge::FALLING }
+			})
+		);
+
+		::std::thread thread(trigger_falling_and_rising_edge, ::std::ref(sim), 7);
+
+		REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+		REQUIRE(request.read_edge_event(buffer, 1) == 1);
+		REQUIRE(buffer.num_events() == 1);
+		auto event = buffer.get_event(0);
+		REQUIRE(event.type() == event_type::FALLING_EDGE);
+		REQUIRE(event.line_offset() == 7);
+
+		REQUIRE_FALSE(request.wait_edge_event(::std::chrono::milliseconds(100)));
+
+		thread.join();
+	}
+
+	SECTION("sequence numbers")
+	{
+		auto request = chip.request_lines(
+			::gpiod::request_config({
+				{ reqprop::OFFSETS, offsets({ 0, 1 })}
+			}),
+			::gpiod::line_config({
+				{ lineprop::EDGE, edge::BOTH }
+			})
+		);
+
+		::std::thread thread(trigger_rising_edge_events_on_two_offsets, ::std::ref(sim), 0, 1);
+
+		REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+		REQUIRE(request.read_edge_event(buffer, 1) == 1);
+		REQUIRE(buffer.num_events() == 1);
+		auto event = buffer.get_event(0);
+		REQUIRE(event.type() == event_type::RISING_EDGE);
+		REQUIRE(event.line_offset() == 0);
+		REQUIRE(event.global_seqno() == 1);
+		REQUIRE(event.line_seqno() == 1);
+
+		REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+		REQUIRE(request.read_edge_event(buffer, 1) == 1);
+		REQUIRE(buffer.num_events() == 1);
+		event = buffer.get_event(0);
+		REQUIRE(event.type() == event_type::RISING_EDGE);
+		REQUIRE(event.line_offset() == 1);
+		REQUIRE(event.global_seqno() == 2);
+		REQUIRE(event.line_seqno() == 1);
+
+		thread.join();
+	}
+}
+
+TEST_CASE("reading multiple events", "[edge-event]")
+{
+	::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+	::gpiod::chip chip(sim.dev_path());
+
+	auto request = chip.request_lines(
+		::gpiod::request_config({
+			{ reqprop::OFFSETS, offsets({ 1 })}
+		}),
+		::gpiod::line_config({
+			{ lineprop::EDGE, edge::BOTH }
+		})
+	);
+
+	unsigned long line_seqno = 1, global_seqno = 1;
+
+	sim.set_pull(1, pull::PULL_UP);
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+	sim.set_pull(1, pull::PULL_DOWN);
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+	sim.set_pull(1, pull::PULL_UP);
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+	SECTION("read multiple events")
+	{
+		::gpiod::edge_event_buffer buffer;
+
+		REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+		REQUIRE(request.read_edge_event(buffer) == 3);
+		REQUIRE(buffer.num_events() == 3);
+
+		for (const auto& event: buffer) {
+			REQUIRE(event.line_offset() == 1);
+			REQUIRE(event.line_seqno() == line_seqno++);
+			REQUIRE(event.global_seqno() == global_seqno++);
+		}
+	}
+
+	SECTION("read over capacity")
+	{
+		::gpiod::edge_event_buffer buffer(2);
+
+		REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+		REQUIRE(request.read_edge_event(buffer) == 2);
+		REQUIRE(buffer.num_events() == 2);
+	}
+}
+
+TEST_CASE("edge_event_buffer can be moved", "[edge-event]")
+{
+	::gpiosim::chip sim({{ simprop::NUM_LINES, 2 }});
+	::gpiod::chip chip(sim.dev_path());
+	::gpiod::edge_event_buffer buffer(13);
+
+	/* Get some events into the buffer. */
+	auto request = chip.request_lines(
+		::gpiod::request_config({
+			{ reqprop::OFFSETS, offsets({ 1 })}
+		}),
+		::gpiod::line_config({
+			{ lineprop::EDGE, edge::BOTH }
+		})
+	);
+
+	sim.set_pull(1, pull::PULL_UP);
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+	sim.set_pull(1, pull::PULL_DOWN);
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+	sim.set_pull(1, pull::PULL_UP);
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(500));
+
+	REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+	REQUIRE(request.read_edge_event(buffer) == 3);
+
+	SECTION("move constructor works")
+	{
+		auto moved(::std::move(buffer));
+		REQUIRE(moved.capacity() == 13);
+		REQUIRE(moved.num_events() == 3);
+	}
+
+	SECTION("move assignment operator works")
+	{
+		::gpiod::edge_event_buffer moved;
+
+		moved = ::std::move(buffer);
+		REQUIRE(moved.capacity() == 13);
+		REQUIRE(moved.num_events() == 3);
+	}
+}
+
+TEST_CASE("edge_event can be copied and moved", "[edge-event]")
+{
+	::gpiosim::chip sim;
+	::gpiod::chip chip(sim.dev_path());
+	::gpiod::edge_event_buffer buffer;
+
+	auto request = chip.request_lines(
+		::gpiod::request_config({
+			{ reqprop::OFFSETS, offsets({ 0 })}
+		}),
+		::gpiod::line_config({
+			{ lineprop::EDGE, edge::BOTH }
+		})
+	);
+
+	sim.set_pull(0, pull::PULL_UP);
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+	REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+	REQUIRE(request.read_edge_event(buffer) == 1);
+	auto event = buffer.get_event(0);
+
+	sim.set_pull(0, pull::PULL_DOWN);
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+	REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+	REQUIRE(request.read_edge_event(buffer) == 1);
+	auto copy = buffer.get_event(0);
+
+	SECTION("copy constructor works")
+	{
+		auto copy(event);
+		REQUIRE(copy.line_offset() == 0);
+		REQUIRE(copy.type() == event_type::RISING_EDGE);
+		REQUIRE(event.line_offset() == 0);
+		REQUIRE(event.type() == event_type::RISING_EDGE);
+	}
+
+	SECTION("move constructor works")
+	{
+		auto copy(::std::move(event));
+		REQUIRE(copy.line_offset() == 0);
+		REQUIRE(copy.type() == event_type::RISING_EDGE);
+	}
+
+	SECTION("assignment operator works")
+	{
+		copy = event;
+		REQUIRE(copy.line_offset() == 0);
+		REQUIRE(copy.type() == event_type::RISING_EDGE);
+		REQUIRE(event.line_offset() == 0);
+		REQUIRE(event.type() == event_type::RISING_EDGE);
+	}
+
+	SECTION("move assignment operator works")
+	{
+		copy = ::std::move(event);
+		REQUIRE(copy.line_offset() == 0);
+		REQUIRE(copy.type() == event_type::RISING_EDGE);
+	}
+}
+
+TEST_CASE("stream insertion operators work for edge_event and edge_event_buffer", "[edge-event]")
+{
+	/*
+	 * This tests the stream insertion operators for both edge_event and
+	 * edge_event_buffer classes.
+	 */
+
+	::gpiosim::chip sim;
+	::gpiod::chip chip(sim.dev_path());
+	::gpiod::edge_event_buffer buffer;
+	::std::stringstream sbuf, expected;
+
+	auto request = chip.request_lines(
+		::gpiod::request_config({
+			{ reqprop::OFFSETS, offsets({ 0 })}
+		}),
+		::gpiod::line_config({
+			{ lineprop::EDGE, edge::BOTH }
+		})
+	);
+
+	sim.set_pull(0, pull::PULL_UP);
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+	sim.set_pull(0, pull::PULL_DOWN);
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+
+	REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+	REQUIRE(request.read_edge_event(buffer) == 2);
+
+	sbuf << buffer;
+
+	expected << "edge_event_buffer\\(num_events=2, capacity=64, events=\\[edge_event\\" <<
+		    "(type='RISING_EDGE', timestamp=[1-9][0-9]+, line_offset=0, global_seqno=1, " <<
+		    "line_seqno=1\\), edge_event\\(type='FALLING_EDGE', timestamp=[1-9][0-9]+, " <<
+		    "line_offset=0, global_seqno=2, line_seqno=2\\)\\]\\)";
+
+	REQUIRE_THAT(sbuf.str(), regex_matcher(expected.str()));
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-event.cpp b/bindings/cxx/tests/tests-event.cpp
deleted file mode 100644
index aeb50dd..0000000
--- a/bindings/cxx/tests/tests-event.cpp
+++ /dev/null
@@ -1,280 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <catch2/catch.hpp>
-#include <gpiod.hpp>
-#include <poll.h>
-#include <thread>
-
-#include "gpio-mockup.hpp"
-
-using ::gpiod::test::mockup;
-
-namespace {
-
-const ::std::string consumer = "event-test";
-
-} /* namespace */
-
-TEST_CASE("Line events can be detected", "[event][line]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-	mockup::event_thread events(0, 4, 200);
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	auto line = chip.get_line(4);
-	::gpiod::line_request config;
-
-	config.consumer = consumer.c_str();
-
-	SECTION("rising edge")
-	{
-		config.request_type = ::gpiod::line_request::EVENT_RISING_EDGE;
-		line.request(config);
-
-		auto got_event = line.event_wait(::std::chrono::seconds(1));
-		REQUIRE(got_event);
-
-		auto event = line.event_read();
-		REQUIRE(event.source == line);
-		REQUIRE(event.event_type == ::gpiod::line_event::RISING_EDGE);
-	}
-
-	SECTION("falling edge")
-	{
-		config.request_type = ::gpiod::line_request::EVENT_FALLING_EDGE;
-
-		line.request(config);
-
-		auto got_event = line.event_wait(::std::chrono::seconds(1));
-		REQUIRE(got_event);
-
-		auto event = line.event_read();
-		REQUIRE(event.source == line);
-		REQUIRE(event.event_type == ::gpiod::line_event::FALLING_EDGE);
-	}
-
-	SECTION("both edges")
-	{
-		config.request_type = ::gpiod::line_request::EVENT_BOTH_EDGES;
-
-		line.request(config);
-
-		auto got_event = line.event_wait(::std::chrono::seconds(1));
-		REQUIRE(got_event);
-
-		auto event = line.event_read();
-		REQUIRE(event.source == line);
-		REQUIRE(event.event_type == ::gpiod::line_event::RISING_EDGE);
-
-		got_event = line.event_wait(::std::chrono::seconds(1));
-		REQUIRE(got_event);
-
-		event = line.event_read();
-		REQUIRE(event.source == line);
-		REQUIRE(event.event_type == ::gpiod::line_event::FALLING_EDGE);
-	}
-
-	SECTION("active-low")
-	{
-		config.request_type = ::gpiod::line_request::EVENT_BOTH_EDGES;
-		config.flags = ::gpiod::line_request::FLAG_ACTIVE_LOW;
-
-		line.request(config);
-
-		auto got_event = line.event_wait(::std::chrono::seconds(1));
-		REQUIRE(got_event);
-
-		auto event = line.event_read();
-		REQUIRE(event.source == line);
-		REQUIRE(event.event_type == ::gpiod::line_event::FALLING_EDGE);
-
-		got_event = line.event_wait(::std::chrono::seconds(1));
-		REQUIRE(got_event);
-
-		event = line.event_read();
-		REQUIRE(event.source == line);
-		REQUIRE(event.event_type == ::gpiod::line_event::RISING_EDGE);
-	}
-}
-
-TEST_CASE("Watching line_bulk objects for events works", "[event][bulk]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-	mockup::event_thread events(0, 2, 200);
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	auto lines = chip.get_lines({ 0, 1, 2, 3 });
-	::gpiod::line_request config;
-
-	config.consumer = consumer.c_str();
-	config.request_type = ::gpiod::line_request::EVENT_BOTH_EDGES;
-	lines.request(config);
-
-	auto event_lines = lines.event_wait(::std::chrono::seconds(1));
-	REQUIRE(event_lines);
-	REQUIRE(event_lines.size() == 1);
-
-	auto event = event_lines.get(0).event_read();
-	REQUIRE(event.source == event_lines.get(0));
-	REQUIRE(event.event_type == ::gpiod::line_event::RISING_EDGE);
-
-	event_lines = lines.event_wait(::std::chrono::seconds(1));
-	REQUIRE(event_lines);
-	REQUIRE(event_lines.size() == 1);
-
-	event = event_lines.get(0).event_read();
-	REQUIRE(event.source == event_lines.get(0));
-	REQUIRE(event.event_type == ::gpiod::line_event::FALLING_EDGE);
-}
-
-TEST_CASE("It's possible to retrieve the event file descriptor", "[event][line]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	auto line = chip.get_line(4);
-	::gpiod::line_request config;
-
-	config.consumer = consumer.c_str();
-
-	SECTION("get the fd")
-	{
-		config.request_type = ::gpiod::line_request::EVENT_BOTH_EDGES;
-
-		line.request(config);
-		REQUIRE(line.event_get_fd() >= 0);
-	}
-
-	SECTION("error if not requested")
-	{
-		REQUIRE_THROWS_AS(line.event_get_fd(), ::std::system_error);
-	}
-
-	SECTION("error if requested for values")
-	{
-		config.request_type = ::gpiod::line_request::DIRECTION_INPUT;
-
-		line.request(config);
-		REQUIRE_THROWS_AS(line.event_get_fd(), ::std::system_error);
-	}
-}
-
-TEST_CASE("Event file descriptors can be used for polling", "[event]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-	mockup::event_thread events(0, 3, 200);
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	auto lines = chip.get_lines({ 0, 1, 2, 3, 4, 5 });
-
-	::gpiod::line_request config;
-	config.consumer = consumer.c_str();
-	config.request_type = ::gpiod::line_request::EVENT_BOTH_EDGES;
-
-	lines.request(config);
-
-	::std::vector<::pollfd> fds(3);
-	fds[0].fd = lines[1].event_get_fd();
-	fds[1].fd = lines[3].event_get_fd();
-	fds[2].fd = lines[5].event_get_fd();
-
-	fds[0].events = fds[1].events = fds[2].events = POLLIN | POLLPRI;
-
-	int ret = ::poll(fds.data(), 3, 1000);
-	REQUIRE(ret == 1);
-
-	for (int i = 0; i < 3; i++) {
-		if (fds[i].revents) {
-			auto event = lines[3].event_read();
-			REQUIRE(event.source == lines[3]);
-			REQUIRE(event.event_type == ::gpiod::line_event::RISING_EDGE);
-		}
-	}
-}
-
-TEST_CASE("It's possible to read a value from a line requested for events", "[event][line]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	auto line = chip.get_line(4);
-	::gpiod::line_request config;
-
-	config.consumer = consumer.c_str();
-	config.request_type = ::gpiod::line_request::EVENT_BOTH_EDGES;
-
-	mockup::instance().chip_set_pull(0, 4, 1);
-
-	SECTION("active-high (default)")
-	{
-		line.request(config);
-		REQUIRE(line.get_value() == 1);
-	}
-
-	SECTION("active-low")
-	{
-		config.flags = ::gpiod::line_request::FLAG_ACTIVE_LOW;
-		line.request(config);
-		REQUIRE(line.get_value() == 0);
-	}
-}
-
-TEST_CASE("It's possible to read values from lines requested for events", "[event][bulk]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	auto lines = chip.get_lines({ 0, 1, 2, 3, 4 });
-	::gpiod::line_request config;
-
-	config.consumer = consumer.c_str();
-	config.request_type = ::gpiod::line_request::EVENT_BOTH_EDGES;
-
-	mockup::instance().chip_set_pull(0, 5, 1);
-
-	SECTION("active-high (default)")
-	{
-		lines.request(config);
-		REQUIRE(lines.get_values() == ::std::vector<int>({ 0, 0, 0, 0, 0 }));
-		mockup::instance().chip_set_pull(0, 1, 1);
-		mockup::instance().chip_set_pull(0, 3, 1);
-		mockup::instance().chip_set_pull(0, 4, 1);
-		REQUIRE(lines.get_values() == ::std::vector<int>({ 0, 1, 0, 1, 1 }));
-	}
-
-	SECTION("active-low")
-	{
-		config.flags = ::gpiod::line_request::FLAG_ACTIVE_LOW;
-		lines.request(config);
-		REQUIRE(lines.get_values() == ::std::vector<int>({ 1, 1, 1, 1, 1 }));
-		mockup::instance().chip_set_pull(0, 1, 1);
-		mockup::instance().chip_set_pull(0, 3, 1);
-		mockup::instance().chip_set_pull(0, 4, 1);
-		REQUIRE(lines.get_values() == ::std::vector<int>({ 1, 0, 1, 0, 0 }));
-	}
-}
-
-TEST_CASE("It's possible to read more than one line event", "[event][line]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	auto line = chip.get_line(4);
-	::gpiod::line_request config;
-
-	config.consumer = consumer.c_str();
-	config.request_type = ::gpiod::line_request::EVENT_BOTH_EDGES;
-
-	line.request(config);
-
-	mockup::instance().chip_set_pull(0, 4, 1);
-	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
-	mockup::instance().chip_set_pull(0, 4, 0);
-	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
-	mockup::instance().chip_set_pull(0, 4, 1);
-	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
-
-	auto events = line.event_read_multiple();
-
-	REQUIRE(events.size() == 3);
-	REQUIRE(events.at(0).event_type == ::gpiod::line_event::RISING_EDGE);
-	REQUIRE(events.at(1).event_type == ::gpiod::line_event::FALLING_EDGE);
-	REQUIRE(events.at(2).event_type == ::gpiod::line_event::RISING_EDGE);
-	REQUIRE(events.at(0).source == line);
-	REQUIRE(events.at(1).source == line);
-	REQUIRE(events.at(2).source == line);
-}
diff --git a/bindings/cxx/tests/tests-info-event.cpp b/bindings/cxx/tests/tests-info-event.cpp
new file mode 100644
index 0000000..8c6a05b
--- /dev/null
+++ b/bindings/cxx/tests/tests-info-event.cpp
@@ -0,0 +1,198 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <chrono>
+#include <gpiod.hpp>
+#include <sstream>
+#include <thread>
+#include <utility>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using simprop = ::gpiosim::chip::property;
+using reqprop = ::gpiod::request_config::property;
+using lineprop = ::gpiod::line_config::property;
+using direction = ::gpiod::line::direction;
+using event_type = ::gpiod::info_event::event_type;
+
+namespace {
+
+void request_reconfigure_release_line(::gpiod::chip& chip)
+{
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+	auto request = chip.request_lines(
+		::gpiod::request_config({
+			{ reqprop::OFFSETS, ::gpiod::line::offsets({ 7 }) }
+		}),
+		::gpiod::line_config()
+	);
+
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+	request.reconfigure_lines(
+		::gpiod::line_config({
+			{ lineprop::DIRECTION, direction::OUTPUT }
+		})
+	);
+
+	::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+	request.release();
+}
+
+TEST_CASE("Lines can be watched", "[info-event][chip]")
+{
+	::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+	::gpiod::chip chip(sim.dev_path());
+
+	SECTION("watch_line_info() returns line info")
+	{
+		auto info = chip.watch_line_info(7);
+		REQUIRE(info.offset() == 7);
+	}
+
+	SECTION("watch_line_info() fails for offset out of range")
+	{
+		REQUIRE_THROWS_AS(chip.watch_line_info(8), ::std::invalid_argument);
+	}
+
+	SECTION("waiting for event timeout")
+	{
+		chip.watch_line_info(3);
+		REQUIRE_FALSE(chip.wait_info_event(::std::chrono::milliseconds(100)));
+	}
+
+	SECTION("request-reconfigure-release events")
+	{
+		auto info = chip.watch_line_info(7);
+		::std::uint64_t ts_req, ts_rec, ts_rel;
+
+		REQUIRE(info.direction() == direction::INPUT);
+
+		::std::thread thread(request_reconfigure_release_line, ::std::ref(chip));
+
+		REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+		auto event = chip.read_info_event();
+		REQUIRE(event.type() == event_type::LINE_REQUESTED);
+		REQUIRE(event.get_line_info().direction() == direction::INPUT);
+		ts_req = event.timestamp_ns();
+
+		REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+		event = chip.read_info_event();
+		REQUIRE(event.type() == event_type::LINE_CONFIG_CHANGED);
+		REQUIRE(event.get_line_info().direction() == direction::OUTPUT);
+		ts_rec = event.timestamp_ns();
+
+		REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+		event = chip.read_info_event();
+		REQUIRE(event.type() == event_type::LINE_RELEASED);
+		ts_rel = event.timestamp_ns();
+
+		/* No more events. */
+		REQUIRE_FALSE(chip.wait_info_event(::std::chrono::milliseconds(100)));
+		thread.join();
+
+		/* Check timestamps are really monotonic. */
+		REQUIRE(ts_rel > ts_rec);
+		REQUIRE(ts_rec > ts_req);
+	}
+}
+
+TEST_CASE("info_event can be copied and moved", "[info-event]")
+{
+	::gpiosim::chip sim;
+	::gpiod::chip chip(sim.dev_path());
+	::std::stringstream buf, expected;
+
+	chip.watch_line_info(0);
+
+	auto request = chip.request_lines(
+		::gpiod::request_config({
+			{ reqprop::OFFSETS, ::gpiod::line::offsets({ 0 }) }
+		}),
+		::gpiod::line_config()
+	);
+
+	REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+	auto event = chip.read_info_event();
+
+	request.release();
+
+	REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+	auto copy = chip.read_info_event();
+
+	SECTION("copy constructor works")
+	{
+		auto copy(event);
+
+		REQUIRE(copy.type() == event_type::LINE_REQUESTED);
+		REQUIRE(copy.get_line_info().offset() == 0);
+
+		REQUIRE(event.type() == event_type::LINE_REQUESTED);
+		REQUIRE(event.get_line_info().offset() == 0);
+	}
+
+	SECTION("assignment operator works")
+	{
+		copy = event;
+
+		REQUIRE(copy.type() == event_type::LINE_REQUESTED);
+		REQUIRE(copy.get_line_info().offset() == 0);
+
+		REQUIRE(event.type() == event_type::LINE_REQUESTED);
+		REQUIRE(event.get_line_info().offset() == 0);
+	}
+
+	SECTION("move constructor works")
+	{
+		auto copy(::std::move(event));
+
+		REQUIRE(copy.type() == event_type::LINE_REQUESTED);
+		REQUIRE(copy.get_line_info().offset() == 0);
+	}
+
+	SECTION("move assignment operator works")
+	{
+		copy = ::std::move(event);
+
+		REQUIRE(copy.type() == event_type::LINE_REQUESTED);
+		REQUIRE(copy.get_line_info().offset() == 0);
+	}
+}
+
+TEST_CASE("info_event stream insertion operator works", "[info-event][line-info]")
+{
+	/*
+	 * This tests the stream insertion operator for both the info_event
+	 * and line_info classes.
+	 */
+
+	::gpiosim::chip sim;
+	::gpiod::chip chip(sim.dev_path());
+	::std::stringstream buf, expected;
+
+	chip.watch_line_info(0);
+
+	auto request = chip.request_lines(
+		::gpiod::request_config({
+			{ reqprop::OFFSETS, ::gpiod::line::offsets({ 0 }) }
+		}),
+		::gpiod::line_config()
+	);
+
+	auto event = chip.read_info_event();
+
+	buf << event;
+
+	expected << "info_event\\(event_type='LINE_REQUESTED', timestamp=[1-9][0-9]+, " <<
+		    "line_info=line_info\\(offset=0, name=unnamed, used=true, consumer='', " <<
+		    "direction=INPUT, active_low=false, bias=UNKNOWN, drive=PUSH/PULL, " <<
+		    "edge_detection=NONE, event_clock=MONOTONIC, debounced=false\\)\\)";
+
+	REQUIRE_THAT(buf.str(), regex_matcher(expected.str()));
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-iter.cpp b/bindings/cxx/tests/tests-iter.cpp
deleted file mode 100644
index 848889b..0000000
--- a/bindings/cxx/tests/tests-iter.cpp
+++ /dev/null
@@ -1,21 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <catch2/catch.hpp>
-#include <gpiod.hpp>
-
-#include "gpio-mockup.hpp"
-
-using ::gpiod::test::mockup;
-
-TEST_CASE("Line iterator works", "[iter][line]")
-{
-	mockup::probe_guard mockup_chips({ 4 });
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	unsigned int count = 0;
-
-	for (auto& it: ::gpiod::line_iter(chip))
-		REQUIRE(it.offset() == count++);
-
-	REQUIRE(count == chip.num_lines());
-}
diff --git a/bindings/cxx/tests/tests-line-config.cpp b/bindings/cxx/tests/tests-line-config.cpp
new file mode 100644
index 0000000..f0ed997
--- /dev/null
+++ b/bindings/cxx/tests/tests-line-config.cpp
@@ -0,0 +1,270 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+#include <sstream>
+
+#include "helpers.hpp"
+
+using lineprop = ::gpiod::line_config::property;
+using value = ::gpiod::line::value;
+using direction = ::gpiod::line::direction;
+using edge = ::gpiod::line::edge;
+using bias = ::gpiod::line::bias;
+using drive = ::gpiod::line::drive;
+using clock_type = ::gpiod::line::clock;
+using mappings = ::gpiod::line::value_mappings;
+using offsets = ::gpiod::line::offsets;
+
+using namespace ::std::chrono_literals;
+
+namespace {
+
+TEST_CASE("line_config constructor works", "[line-config]")
+{
+	SECTION("no arguments - default values")
+	{
+		::gpiod::line_config cfg;
+
+		REQUIRE_NOTHROW(cfg.direction_default() == direction::INPUT);
+		REQUIRE(cfg.edge_detection_default() == edge::NONE);
+		REQUIRE(cfg.bias_default() == bias::AS_IS);
+		REQUIRE(cfg.drive_default() == drive::PUSH_PULL);
+		REQUIRE_FALSE(cfg.active_low_default());
+		REQUIRE(cfg.debounce_period_default() == 0us);
+		REQUIRE(cfg.event_clock_default() == clock_type::MONOTONIC);
+		REQUIRE(cfg.output_value_default() == value::INACTIVE);
+		REQUIRE(cfg.num_overrides() == 0);
+		REQUIRE(cfg.overrides().empty());
+	}
+
+	SECTION("default values set from constructor")
+	{
+		/*
+		 * These are wrong and the request would fail but we're just
+		 * testing the object's behavior.
+		 */
+		::gpiod::line_config cfg({
+			{ lineprop::DIRECTION, direction::OUTPUT },
+			{ lineprop::EDGE, edge::FALLING },
+			{ lineprop::BIAS, bias::DISABLED },
+			{ lineprop::DRIVE, drive::OPEN_DRAIN },
+			{ lineprop::ACTIVE_LOW, true },
+			{ lineprop::DEBOUNCE_PERIOD, 3000us },
+			{ lineprop::EVENT_CLOCK, clock_type::REALTIME },
+			{ lineprop::OUTPUT_VALUE, value::ACTIVE }
+		});
+
+		REQUIRE_NOTHROW(cfg.direction_default() == direction::OUTPUT);
+		REQUIRE(cfg.edge_detection_default() == edge::FALLING);
+		REQUIRE(cfg.bias_default() == bias::DISABLED);
+		REQUIRE(cfg.drive_default() == drive::OPEN_DRAIN);
+		REQUIRE(cfg.active_low_default());
+		/* Test implicit conversion between duration types. */
+		REQUIRE(cfg.debounce_period_default() == 3ms);
+		REQUIRE(cfg.event_clock_default() == clock_type::REALTIME);
+		REQUIRE(cfg.output_value_default() == value::ACTIVE);
+		REQUIRE(cfg.num_overrides() == 0);
+		REQUIRE(cfg.overrides().empty());
+	}
+
+	SECTION("output value overrides can be set from constructor")
+	{
+		::gpiod::line_config cfg({
+			{
+				lineprop::OUTPUT_VALUES, mappings({
+					{ 0, value::ACTIVE },
+					{ 3, value::INACTIVE },
+					{ 1, value::ACTIVE }
+				})
+			}
+		});
+
+		REQUIRE(cfg.num_overrides() == 3);
+		auto overrides = cfg.overrides();
+		REQUIRE(overrides[0].first == 0);
+		REQUIRE(overrides[0].second == lineprop::OUTPUT_VALUE);
+		REQUIRE(overrides[1].first == 3);
+		REQUIRE(overrides[1].second == lineprop::OUTPUT_VALUE);
+		REQUIRE(overrides[2].first == 1);
+		REQUIRE(overrides[2].second == lineprop::OUTPUT_VALUE);
+	}
+}
+
+TEST_CASE("line_config overrides work")
+{
+	::gpiod::line_config cfg;
+
+	SECTION("direction")
+	{
+		cfg.set_direction_default(direction::AS_IS);
+		cfg.set_direction_override(direction::INPUT, 3);
+
+		REQUIRE(cfg.direction_is_overridden(3));
+		REQUIRE(cfg.direction_offset(3) == direction::INPUT);
+		cfg.clear_direction_override(3);
+		REQUIRE_FALSE(cfg.direction_is_overridden(3));
+		REQUIRE(cfg.direction_offset(3) == direction::AS_IS);
+	}
+
+	SECTION("edge detection")
+	{
+		cfg.set_edge_detection_default(edge::NONE);
+		cfg.set_edge_detection_override(edge::BOTH, 0);
+
+		REQUIRE(cfg.edge_detection_is_overridden(0));
+		REQUIRE(cfg.edge_detection_offset(0) == edge::BOTH);
+		cfg.clear_edge_detection_override(0);
+		REQUIRE_FALSE(cfg.edge_detection_is_overridden(0));
+		REQUIRE(cfg.edge_detection_offset(0) == edge::NONE);
+	}
+
+	SECTION("bias")
+	{
+		cfg.set_bias_default(bias::AS_IS);
+		cfg.set_bias_override(bias::PULL_DOWN, 3);
+
+		REQUIRE(cfg.bias_is_overridden(3));
+		REQUIRE(cfg.bias_offset(3) == bias::PULL_DOWN);
+		cfg.clear_bias_override(3);
+		REQUIRE_FALSE(cfg.bias_is_overridden(3));
+		REQUIRE(cfg.bias_offset(3) == bias::AS_IS);
+	}
+
+	SECTION("drive")
+	{
+		cfg.set_drive_default(drive::PUSH_PULL);
+		cfg.set_drive_override(drive::OPEN_DRAIN, 4);
+
+		REQUIRE(cfg.drive_is_overridden(4));
+		REQUIRE(cfg.drive_offset(4) == drive::OPEN_DRAIN);
+		cfg.clear_drive_override(4);
+		REQUIRE_FALSE(cfg.drive_is_overridden(4));
+		REQUIRE(cfg.drive_offset(4) == drive::PUSH_PULL);
+	}
+
+	SECTION("active-low")
+	{
+		cfg.set_active_low_default(false);
+		cfg.set_active_low_override(true, 16);
+
+		REQUIRE(cfg.active_low_is_overridden(16));
+		REQUIRE(cfg.active_low_offset(16));
+		cfg.clear_active_low_override(16);
+		REQUIRE_FALSE(cfg.active_low_is_overridden(16));
+		REQUIRE_FALSE(cfg.active_low_offset(16));
+	}
+
+	SECTION("debounce period")
+	{
+		/*
+		 * Test the chrono literals and implicit duration conversions
+		 * too.
+		 */
+
+		cfg.set_debounce_period_default(5000us);
+		cfg.set_debounce_period_override(3ms, 1);
+
+		REQUIRE(cfg.debounce_period_is_overridden(1));
+		REQUIRE(cfg.debounce_period_offset(1) == 3ms);
+		cfg.clear_debounce_period_override(1);
+		REQUIRE_FALSE(cfg.debounce_period_is_overridden(1));
+		REQUIRE(cfg.debounce_period_offset(1) == 5ms);
+	}
+
+	SECTION("event clock")
+	{
+		cfg.set_event_clock_default(clock_type::MONOTONIC);
+		cfg.set_event_clock_override(clock_type::REALTIME, 4);
+
+		REQUIRE(cfg.event_clock_is_overridden(4));
+		REQUIRE(cfg.event_clock_offset(4) == clock_type::REALTIME);
+		cfg.clear_event_clock_override(4);
+		REQUIRE_FALSE(cfg.event_clock_is_overridden(4));
+		REQUIRE(cfg.event_clock_offset(4) == clock_type::MONOTONIC);
+	}
+
+	SECTION("output value")
+	{
+		cfg.set_output_value_default(value::INACTIVE);
+		cfg.set_output_value_override(value::ACTIVE, 0);
+		cfg.set_output_values({ 1, 2, 8 }, { value::ACTIVE, value::ACTIVE, value::ACTIVE });
+		cfg.set_output_values({ { 17, value::ACTIVE }, { 21, value::ACTIVE } });
+
+		for (const auto& off: offsets({ 0, 1, 2, 8, 17, 21 })) {
+			REQUIRE(cfg.output_value_is_overridden(off));
+			REQUIRE(cfg.output_value_offset(off) == value::ACTIVE);
+			cfg.clear_output_value_override(off);
+			REQUIRE_FALSE(cfg.output_value_is_overridden(off));
+			REQUIRE(cfg.output_value_offset(off) == value::INACTIVE);
+		}
+	}
+}
+
+TEST_CASE("line_config can be moved", "[line-config]")
+{
+	::gpiod::line_config cfg({
+		{ lineprop::DIRECTION, direction::INPUT },
+		{ lineprop::EDGE, edge::BOTH },
+		{ lineprop::DEBOUNCE_PERIOD, 3000us },
+		{ lineprop::EVENT_CLOCK, clock_type::REALTIME },
+	});
+
+	cfg.set_direction_override(direction::OUTPUT, 2);
+	cfg.set_edge_detection_override(edge::NONE, 2);
+
+	SECTION("move constructor works")
+	{
+		auto moved(::std::move(cfg));
+
+		REQUIRE(moved.direction_default() == direction::INPUT);
+		REQUIRE(moved.edge_detection_default() == edge::BOTH);
+		REQUIRE(moved.debounce_period_default() == 3000us);
+		REQUIRE(moved.event_clock_default() == clock_type::REALTIME);
+		REQUIRE(moved.direction_offset(2) == direction::OUTPUT);
+		REQUIRE(moved.edge_detection_offset(2) == edge::NONE);
+	}
+
+	SECTION("move constructor works")
+	{
+		::gpiod::line_config moved;
+
+		moved = ::std::move(cfg);
+
+		REQUIRE(moved.direction_default() == direction::INPUT);
+		REQUIRE(moved.edge_detection_default() == edge::BOTH);
+		REQUIRE(moved.debounce_period_default() == 3000us);
+		REQUIRE(moved.event_clock_default() == clock_type::REALTIME);
+		REQUIRE(moved.direction_offset(2) == direction::OUTPUT);
+		REQUIRE(moved.edge_detection_offset(2) == edge::NONE);
+	}
+}
+
+TEST_CASE("line_config stream insertion operator works", "[line-config]")
+{
+	::gpiod::line_config cfg({
+		{ lineprop::DIRECTION, direction::INPUT },
+		{ lineprop::EDGE, edge::BOTH },
+		{ lineprop::DEBOUNCE_PERIOD, 3000us },
+		{ lineprop::EVENT_CLOCK, clock_type::REALTIME },
+	});
+
+	cfg.set_direction_override(direction::OUTPUT, 2);
+	cfg.set_edge_detection_override(edge::NONE, 2);
+
+	::std::stringstream buf;
+
+	buf << cfg;
+
+	::std::string expected(
+		"line_config(defaults=(direction=INPUT, edge_detection=BOTH_EDGES, bias="
+		"AS_IS, drive=PUSH/PULL, active-high, debounce_period=3000us, event_clock="
+		"REALTIME, default_output_value=INACTIVE), overrides=[(offset=2 -> direction="
+		"OUTPUT), (offset=2 -> edge_detection=NONE)])"
+	);
+
+	REQUIRE_THAT(buf.str(), Catch::Equals(expected));
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-line-info.cpp b/bindings/cxx/tests/tests-line-info.cpp
new file mode 100644
index 0000000..2276c63
--- /dev/null
+++ b/bindings/cxx/tests/tests-line-info.cpp
@@ -0,0 +1,140 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+#include <string>
+
+#include "helpers.hpp"
+#include "gpiosim.hpp"
+
+using property = ::gpiosim::chip::property;
+using line_name = ::gpiosim::chip::line_name;
+using line_hog = ::gpiosim::chip::line_hog;
+using hog_dir = ::gpiosim::chip::hog_direction;
+using direction = ::gpiod::line::direction;
+using edge = ::gpiod::line::edge;
+using bias = ::gpiod::line::bias;
+using drive = ::gpiod::line::drive;
+using event_clock = ::gpiod::line::clock;
+
+using namespace ::std::chrono_literals;
+
+namespace {
+
+TEST_CASE("get_line_info() works", "[chip][line-info]")
+{
+	::gpiosim::chip sim({
+		{ property::NUM_LINES, 8 },
+		{ property::LINE_NAME, line_name(0, "foobar") },
+		{ property::HOG, line_hog(0, "hog", hog_dir::OUTPUT_HIGH ) }
+	});
+
+	::gpiod::chip chip(sim.dev_path());
+
+	SECTION("line_info can be retrieved from chip")
+	{
+		auto info = chip.get_line_info(0);
+
+		REQUIRE(info.offset() == 0);
+		REQUIRE_THAT(info.name(), Catch::Equals("foobar"));
+		REQUIRE(info.used());
+		REQUIRE_THAT(info.consumer(), Catch::Equals("hog"));
+		REQUIRE(info.direction() == ::gpiod::line::direction::OUTPUT);
+		REQUIRE_FALSE(info.active_low());
+		REQUIRE(info.bias() == ::gpiod::line::bias::UNKNOWN);
+		REQUIRE(info.drive() == ::gpiod::line::drive::PUSH_PULL);
+		REQUIRE(info.edge_detection() == ::gpiod::line::edge::NONE);
+		REQUIRE(info.event_clock() == ::gpiod::line::clock::MONOTONIC);
+		REQUIRE_FALSE(info.debounced());
+		REQUIRE(info.debounce_period() == 0us);
+	}
+
+	SECTION("offset out of range")
+	{
+		REQUIRE_THROWS_AS(chip.get_line_info(8), ::std::invalid_argument);
+	}
+}
+
+TEST_CASE("line properties can be retrieved", "[line-info]")
+{
+	::gpiosim::chip sim({
+		{ property::NUM_LINES, 8 },
+		{ property::LINE_NAME, line_name(1, "foo") },
+		{ property::LINE_NAME, line_name(2, "bar") },
+		{ property::LINE_NAME, line_name(4, "baz") },
+		{ property::LINE_NAME, line_name(5, "xyz") },
+		{ property::HOG, line_hog(3, "hog3", hog_dir::OUTPUT_HIGH) },
+		{ property::HOG, line_hog(4, "hog4", hog_dir::OUTPUT_LOW) }
+	});
+
+	::gpiod::chip chip(sim.dev_path());
+
+	SECTION("basic properties")
+	{
+		auto info4 = chip.get_line_info(4);
+		auto info6 = chip.get_line_info(6);
+
+		REQUIRE(info4.offset() == 4);
+		REQUIRE_THAT(info4.name(), Catch::Equals("baz"));
+		REQUIRE(info4.used());
+		REQUIRE_THAT(info4.consumer(), Catch::Equals("hog4"));
+		REQUIRE(info4.direction() == direction::OUTPUT);
+		REQUIRE(info4.edge_detection() == edge::NONE);
+		REQUIRE_FALSE(info4.active_low());
+		REQUIRE(info4.bias() == bias::UNKNOWN);
+		REQUIRE(info4.drive() == drive::PUSH_PULL);
+		REQUIRE(info4.event_clock() == event_clock::MONOTONIC);
+		REQUIRE_FALSE(info4.debounced());
+		REQUIRE(info4.debounce_period() == 0us);
+	}
+}
+
+TEST_CASE("line_info can be copied and moved")
+{
+	::gpiosim::chip sim({
+		{ property::NUM_LINES, 4 },
+		{ property::LINE_NAME, line_name(2, "foobar") }
+	});
+
+	::gpiod::chip chip(sim.dev_path());
+	auto info = chip.get_line_info(2);
+
+	SECTION("copy constructor works")
+	{
+		auto copy(info);
+		REQUIRE(copy.offset() == 2);
+		REQUIRE_THAT(copy.name(), Catch::Equals("foobar"));
+		/* info can still be used */
+		REQUIRE(info.offset() == 2);
+		REQUIRE_THAT(info.name(), Catch::Equals("foobar"));
+	}
+
+	SECTION("assignment operator works")
+	{
+		auto copy = chip.get_line_info(0);
+		copy = info;
+		REQUIRE(copy.offset() == 2);
+		REQUIRE_THAT(copy.name(), Catch::Equals("foobar"));
+		/* info can still be used */
+		REQUIRE(info.offset() == 2);
+		REQUIRE_THAT(info.name(), Catch::Equals("foobar"));
+	}
+
+	SECTION("move constructor works")
+	{
+		auto copy(::std::move(info));
+		REQUIRE(copy.offset() == 2);
+		REQUIRE_THAT(copy.name(), Catch::Equals("foobar"));
+	}
+
+	SECTION("move assignment operator works")
+	{
+		auto copy = chip.get_line_info(0);
+		copy = ::std::move(info);
+		REQUIRE(copy.offset() == 2);
+		REQUIRE_THAT(copy.name(), Catch::Equals("foobar"));
+	}
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-line-request.cpp b/bindings/cxx/tests/tests-line-request.cpp
new file mode 100644
index 0000000..4827b0b
--- /dev/null
+++ b/bindings/cxx/tests/tests-line-request.cpp
@@ -0,0 +1,494 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+#include <sstream>
+#include <stdexcept>
+#include <vector>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using simprop = ::gpiosim::chip::property;
+using reqprop = ::gpiod::request_config::property;
+using lineprop = ::gpiod::line_config::property;
+using offsets = ::gpiod::line::offsets;
+using values = ::gpiod::line::values;
+using direction = ::gpiod::line::direction;
+using value = ::gpiod::line::value;
+using simval = ::gpiosim::chip::value;
+using pull = ::gpiosim::chip::pull;
+
+namespace {
+
+class value_matcher : public Catch::MatcherBase<value>
+{
+public:
+	value_matcher(pull pull, bool active_low = false)
+		: _m_pull(pull),
+		  _m_active_low(active_low)
+	{
+
+	}
+
+	::std::string describe(void) const override
+	{
+		::std::string repr(this->_m_pull == pull::PULL_UP ? "PULL_UP" : "PULL_DOWN");
+		::std::string active_low = this->_m_active_low ? "(active-low) " : "";
+
+		return active_low + "corresponds with " + repr;
+	}
+
+	bool match(const value& val) const override
+	{
+		if (this->_m_active_low) {
+			if ((val == value::ACTIVE && this->_m_pull == pull::PULL_DOWN) ||
+			    (val == value::INACTIVE && this->_m_pull == pull::PULL_UP))
+				return true;
+		} else {
+			if ((val == value::ACTIVE && this->_m_pull == pull::PULL_UP) ||
+			    (val == value::INACTIVE && this->_m_pull == pull::PULL_DOWN))
+				return true;
+		}
+
+		return false;
+	}
+
+private:
+	pull _m_pull;
+	bool _m_active_low;
+};
+
+TEST_CASE("requesting lines fails with invalid arguments", "[line-request][chip]")
+{
+	::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+	::gpiod::chip chip(sim.dev_path());
+
+	SECTION("no offsets")
+	{
+		REQUIRE_THROWS_AS(chip.request_lines(::gpiod::request_config(),
+						     ::gpiod::line_config()),
+				  ::std::invalid_argument);
+	}
+
+	SECTION("duplicate offsets")
+	{
+		REQUIRE_THROWS_MATCHES(chip.request_lines(
+			::gpiod::request_config({
+				{ reqprop::OFFSETS, offsets({ 2, 0, 0, 4 }) }
+			}),
+			::gpiod::line_config()),
+			::std::system_error,
+			 system_error_matcher(EBUSY)
+		);
+	}
+
+	SECTION("offset out of bounds")
+	{
+		REQUIRE_THROWS_AS(chip.request_lines(
+			::gpiod::request_config({
+				{ reqprop::OFFSETS, offsets({ 2, 0, 8, 4 }) }
+			}),
+			::gpiod::line_config()),
+			::std::invalid_argument
+		);
+	}
+}
+
+TEST_CASE("consumer string is set correctly", "[line-request]")
+{
+	::gpiosim::chip sim({{ simprop::NUM_LINES, 4 }});
+	::gpiod::chip chip(sim.dev_path());
+	offsets offs({ 3, 0, 2 });
+
+	SECTION("set custom consumer")
+	{
+		auto request = chip.request_lines(
+			::gpiod::request_config({
+				{ reqprop::OFFSETS, offsets({ 2 }) },
+				{ reqprop::CONSUMER, "foobar" }
+			}),
+			::gpiod::line_config()
+		);
+
+		auto info = chip.get_line_info(2);
+
+		REQUIRE(info.used());
+		REQUIRE_THAT(info.consumer(), Catch::Equals("foobar"));
+	}
+
+	SECTION("empty consumer")
+	{
+		auto request = chip.request_lines(
+			::gpiod::request_config({
+				{ reqprop::OFFSETS, offsets({ 2 }) },
+			}),
+			::gpiod::line_config()
+		);
+
+		auto info = chip.get_line_info(2);
+
+		REQUIRE(info.used());
+		REQUIRE_THAT(info.consumer(), Catch::Equals("?"));
+	}
+}
+
+TEST_CASE("values can be read", "[line-request]")
+{
+	::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+	::gpiod::chip chip(sim.dev_path());
+	const offsets offs({ 7, 1, 0, 6, 2 });
+
+	const ::std::vector<pull> pulls({
+		pull::PULL_UP,
+		pull::PULL_UP,
+		pull::PULL_DOWN,
+		pull::PULL_UP,
+		pull::PULL_DOWN
+	});
+
+	for (unsigned int i = 0; i < offs.size(); i++)
+		sim.set_pull(offs[i], pulls[i]);
+
+	auto request = chip.request_lines(
+		::gpiod::request_config({
+			{ reqprop::OFFSETS, offs }
+		}),
+		::gpiod::line_config({
+			{ lineprop::DIRECTION, direction::INPUT }
+		})
+	);
+
+	SECTION("get all values (returning variant)")
+	{
+		auto vals = request.get_values();
+
+		REQUIRE_THAT(vals[0], value_matcher(pull::PULL_UP));
+		REQUIRE_THAT(vals[1], value_matcher(pull::PULL_UP));
+		REQUIRE_THAT(vals[2], value_matcher(pull::PULL_DOWN));
+		REQUIRE_THAT(vals[3], value_matcher(pull::PULL_UP));
+		REQUIRE_THAT(vals[4], value_matcher(pull::PULL_DOWN));
+	}
+
+	SECTION("get all values (passed buffer variant)")
+	{
+		values vals(5);
+
+		request.get_values(vals);
+
+		REQUIRE_THAT(vals[0], value_matcher(pull::PULL_UP));
+		REQUIRE_THAT(vals[1], value_matcher(pull::PULL_UP));
+		REQUIRE_THAT(vals[2], value_matcher(pull::PULL_DOWN));
+		REQUIRE_THAT(vals[3], value_matcher(pull::PULL_UP));
+		REQUIRE_THAT(vals[4], value_matcher(pull::PULL_DOWN));
+	}
+
+	SECTION("get_values(buffer) throws for invalid buffer size")
+	{
+		values vals(4);
+		REQUIRE_THROWS_AS(request.get_values(vals), ::std::invalid_argument);
+		vals.resize(6);
+		REQUIRE_THROWS_AS(request.get_values(vals), ::std::invalid_argument);
+	}
+
+	SECTION("get a single value")
+	{
+		auto val = request.get_value(7);
+
+		REQUIRE_THAT(val, value_matcher(pull::PULL_UP));
+	}
+
+	SECTION("get a single value (active-low)")
+	{
+		request.reconfigure_lines(
+			::gpiod::line_config({
+				{ lineprop::ACTIVE_LOW, true }
+			})
+		);
+
+		auto val = request.get_value(7);
+
+		REQUIRE_THAT(val, value_matcher(pull::PULL_UP, true));
+	}
+
+	SECTION("get a subset of values (returning variant)")
+	{
+		auto vals = request.get_values(offsets({ 2, 0, 6 }));
+
+		REQUIRE_THAT(vals[0], value_matcher(pull::PULL_DOWN));
+		REQUIRE_THAT(vals[1], value_matcher(pull::PULL_DOWN));
+		REQUIRE_THAT(vals[2], value_matcher(pull::PULL_UP));
+	}
+
+	SECTION("get a subset of values (passed buffer variant)")
+	{
+		values vals(3);
+
+		request.get_values(offsets({ 2, 0, 6 }), vals);
+
+		REQUIRE_THAT(vals[0], value_matcher(pull::PULL_DOWN));
+		REQUIRE_THAT(vals[1], value_matcher(pull::PULL_DOWN));
+		REQUIRE_THAT(vals[2], value_matcher(pull::PULL_UP));
+	}
+}
+
+TEST_CASE("output values can be set at request time", "[line-request]")
+{
+	::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+	::gpiod::chip chip(sim.dev_path());
+	const offsets offs({ 0, 1, 3, 4 });
+
+	::gpiod::request_config req_cfg({
+		{ reqprop::OFFSETS, offs }
+	});
+
+	::gpiod::line_config line_cfg({
+		{ lineprop::DIRECTION, direction::OUTPUT },
+		{ lineprop::OUTPUT_VALUE, value::ACTIVE }
+	});
+
+	SECTION("default output value")
+	{
+		auto request = chip.request_lines(req_cfg, line_cfg);
+
+		for (const auto& off: offs)
+			REQUIRE(sim.get_value(off) == simval::ACTIVE);
+
+		REQUIRE(sim.get_value(2) == simval::INACTIVE);
+	}
+
+	SECTION("overridden output value")
+	{
+		line_cfg.set_output_value_override(value::INACTIVE, 1);
+
+		auto request = chip.request_lines(req_cfg, line_cfg);
+
+		REQUIRE(sim.get_value(0) == simval::ACTIVE);
+		REQUIRE(sim.get_value(1) == simval::INACTIVE);
+		REQUIRE(sim.get_value(2) == simval::INACTIVE);
+		REQUIRE(sim.get_value(3) == simval::ACTIVE);
+		REQUIRE(sim.get_value(4) == simval::ACTIVE);
+	}
+}
+
+TEST_CASE("values can be set after requesting lines", "[line-request]")
+{
+	::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+	::gpiod::chip chip(sim.dev_path());
+	const offsets offs({ 0, 1, 3, 4 });
+
+	::gpiod::request_config req_cfg({
+		{ reqprop::OFFSETS, offs }
+	});
+
+	::gpiod::line_config line_cfg({
+		{ lineprop::DIRECTION, direction::OUTPUT },
+		{ lineprop::OUTPUT_VALUE, value::INACTIVE }
+	});
+
+	auto request = chip.request_lines(req_cfg, line_cfg);
+
+	SECTION("set single value")
+	{
+		request.set_value(1, value::ACTIVE);
+
+		REQUIRE(sim.get_value(0) == simval::INACTIVE);
+		REQUIRE(sim.get_value(1) == simval::ACTIVE);
+		REQUIRE(sim.get_value(3) == simval::INACTIVE);
+		REQUIRE(sim.get_value(4) == simval::INACTIVE);
+	}
+
+	SECTION("set all values")
+	{
+		request.set_values({
+			value::ACTIVE,
+			value::INACTIVE,
+			value::ACTIVE,
+			value::INACTIVE
+		});
+
+		REQUIRE(sim.get_value(0) == simval::ACTIVE);
+		REQUIRE(sim.get_value(1) == simval::INACTIVE);
+		REQUIRE(sim.get_value(3) == simval::ACTIVE);
+		REQUIRE(sim.get_value(4) == simval::INACTIVE);
+	}
+
+	SECTION("set a subset of values")
+	{
+		request.set_values({ 4, 3 }, { value::ACTIVE, value::INACTIVE });
+
+		REQUIRE(sim.get_value(0) == simval::INACTIVE);
+		REQUIRE(sim.get_value(1) == simval::INACTIVE);
+		REQUIRE(sim.get_value(3) == simval::INACTIVE);
+		REQUIRE(sim.get_value(4) == simval::ACTIVE);
+	}
+
+	SECTION("set a subset of values with mappings")
+	{
+		request.set_values({
+			{ 0, value::ACTIVE },
+			{ 4, value::INACTIVE },
+			{ 1, value::ACTIVE}
+		});
+
+		REQUIRE(sim.get_value(0) == simval::ACTIVE);
+		REQUIRE(sim.get_value(1) == simval::ACTIVE);
+		REQUIRE(sim.get_value(3) == simval::INACTIVE);
+		REQUIRE(sim.get_value(4) == simval::INACTIVE);
+	}
+}
+
+TEST_CASE("line_request can be moved", "[line-request]")
+{
+	::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+	::gpiod::chip chip(sim.dev_path());
+	const offsets offs({ 3, 1, 0, 2 });
+
+	auto request = chip.request_lines(
+		::gpiod::request_config({
+			{ reqprop::OFFSETS, offs }
+		}),
+		::gpiod::line_config()
+	);
+
+	auto fd = request.fd();
+
+	auto another = chip.request_lines(
+		::gpiod::request_config({
+			{ reqprop::OFFSETS, offsets({ 6 }) }
+		}),
+		::gpiod::line_config()
+	);
+
+	SECTION("move constructor works")
+	{
+		auto moved(::std::move(request));
+
+		REQUIRE(moved.fd() == fd);
+		REQUIRE_THAT(moved.offsets(), Catch::Equals(offs));
+	}
+
+	SECTION("move assignment operator works")
+	{
+		another = ::std::move(request);
+
+		REQUIRE(another.fd() == fd);
+		REQUIRE_THAT(another.offsets(), Catch::Equals(offs));
+	}
+}
+
+TEST_CASE("released request can no longer be used", "[line-request]")
+{
+	::gpiosim::chip sim;
+	::gpiod::chip chip(sim.dev_path());
+
+	auto request = chip.request_lines(
+		::gpiod::request_config({
+			{ reqprop::OFFSETS, offsets({ 0 }) }
+		}),
+		::gpiod::line_config()
+	);
+
+	request.release();
+
+	REQUIRE_THROWS_AS(request.offsets(), ::gpiod::request_released);
+}
+
+TEST_CASE("line_request survives parent chip", "[line-request][chip]")
+{
+	::gpiosim::chip sim;
+
+	sim.set_pull(0, pull::PULL_UP);
+
+	SECTION("chip is released")
+	{
+		::gpiod::chip chip(sim.dev_path());
+
+		auto request = chip.request_lines(
+			::gpiod::request_config({
+				{ reqprop::OFFSETS, offsets({ 0 }) }
+			}),
+			::gpiod::line_config({
+				{ lineprop::DIRECTION, direction::INPUT }
+			})
+		);
+
+		REQUIRE_THAT(request.get_value(0), value_matcher(pull::PULL_UP));
+
+		chip.close();
+
+		REQUIRE_THAT(request.get_value(0), value_matcher(pull::PULL_UP));
+	}
+
+	SECTION("chip goes out of scope")
+	{
+		/* Need to get the request object somehow. */
+		::gpiod::chip dummy(sim.dev_path());
+
+		auto request = dummy.request_lines(
+			::gpiod::request_config({
+				{ reqprop::OFFSETS, offsets({ 0 }) }
+			}),
+			::gpiod::line_config({
+				{ lineprop::DIRECTION, direction::INPUT }
+			})
+		);
+
+		request.release();
+		dummy.close();
+
+		{
+			::gpiod::chip chip(sim.dev_path());
+
+			request = chip.request_lines(
+				::gpiod::request_config({
+					{ reqprop::OFFSETS, offsets({ 0 }) }
+				}),
+				::gpiod::line_config({
+					{ lineprop::DIRECTION, direction::INPUT }
+				})
+			);
+
+			REQUIRE_THAT(request.get_value(0), value_matcher(pull::PULL_UP));
+		}
+
+		REQUIRE_THAT(request.get_value(0), value_matcher(pull::PULL_UP));
+	}
+}
+
+TEST_CASE("line_request stream insertion operator works", "[line-request]")
+{
+	::gpiosim::chip sim({{ simprop::NUM_LINES, 4 }});
+	::gpiod::chip chip(sim.dev_path());
+
+	auto request = chip.request_lines(
+		::gpiod::request_config({
+			{ reqprop::OFFSETS, offsets({ 3, 1, 0, 2 }) }
+		}),
+		::gpiod::line_config()
+	);
+
+	::std::stringstream buf, expected;
+
+	expected << "line_request(num_lines=4, line_offsets=[offsets([3, 1, 0, 2])], fd=" <<
+		    request.fd() << ")";
+
+	SECTION("active request")
+	{
+		buf << request;
+
+		REQUIRE_THAT(buf.str(), Catch::Equals(expected.str()));
+	}
+
+	SECTION("request released")
+	{
+		request.release();
+
+		buf << request;
+
+		REQUIRE_THAT(buf.str(), Catch::Equals("line_request(released)"));
+	}
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-line.cpp b/bindings/cxx/tests/tests-line.cpp
deleted file mode 100644
index ababf8b..0000000
--- a/bindings/cxx/tests/tests-line.cpp
+++ /dev/null
@@ -1,467 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <catch2/catch.hpp>
-#include <gpiod.hpp>
-
-#include "gpio-mockup.hpp"
-
-using ::gpiod::test::mockup;
-
-namespace {
-
-const ::std::string consumer = "line-test";
-
-} /* namespace */
-
-TEST_CASE("Line information can be correctly retrieved", "[line]")
-{
-	mockup::probe_guard mockup_chips({ 8 }, mockup::FLAG_NAMED_LINES);
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	auto line = chip.get_line(4);
-
-	SECTION("unexported line")
-	{
-		REQUIRE(line.offset() == 4);
-		REQUIRE(line.name() == "gpio-mockup-A-4");
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_INPUT);
-		REQUIRE_FALSE(line.is_active_low());
-		REQUIRE(line.consumer().empty());
-		REQUIRE_FALSE(line.is_used());
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_PUSH_PULL);
-		REQUIRE(line.bias() == ::gpiod::line::BIAS_UNKNOWN);
-	}
-
-	SECTION("exported line")
-	{
-		::gpiod::line_request config;
-
-		config.consumer = consumer.c_str();
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		line.request(config);
-
-		REQUIRE(line.offset() == 4);
-		REQUIRE(line.name() == "gpio-mockup-A-4");
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE_FALSE(line.is_active_low());
-		REQUIRE(line.is_used());
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_PUSH_PULL);
-		REQUIRE(line.bias() == ::gpiod::line::BIAS_UNKNOWN);
-	}
-
-	SECTION("exported line with flags")
-	{
-		::gpiod::line_request config;
-
-		config.consumer = consumer.c_str();
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		config.flags = ::gpiod::line_request::FLAG_ACTIVE_LOW |
-			       ::gpiod::line_request::FLAG_OPEN_DRAIN;
-		line.request(config);
-
-		REQUIRE(line.offset() == 4);
-		REQUIRE(line.name() == "gpio-mockup-A-4");
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE(line.is_active_low());
-		REQUIRE(line.is_used());
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_OPEN_DRAIN);
-		REQUIRE(line.bias() == ::gpiod::line::BIAS_UNKNOWN);
-	}
-
-	SECTION("exported open source line")
-	{
-		::gpiod::line_request config;
-
-		config.consumer = consumer.c_str();
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		config.flags = ::gpiod::line_request::FLAG_OPEN_SOURCE;
-		line.request(config);
-
-		REQUIRE(line.offset() == 4);
-		REQUIRE(line.name() == "gpio-mockup-A-4");
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE_FALSE(line.is_active_low());
-		REQUIRE(line.is_used());
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_OPEN_SOURCE);
-		REQUIRE(line.bias() == ::gpiod::line::BIAS_UNKNOWN);
-	}
-
-	SECTION("exported bias disable line")
-	{
-		::gpiod::line_request config;
-
-		config.consumer = consumer.c_str();
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		config.flags = ::gpiod::line_request::FLAG_BIAS_DISABLED;
-		line.request(config);
-
-		REQUIRE(line.offset() == 4);
-		REQUIRE(line.name() == "gpio-mockup-A-4");
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE_FALSE(line.is_active_low());
-		REQUIRE(line.is_used());
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_PUSH_PULL);
-		REQUIRE(line.bias() == ::gpiod::line::BIAS_DISABLED);
-	}
-
-	SECTION("exported pull-down line")
-	{
-		::gpiod::line_request config;
-
-		config.consumer = consumer.c_str();
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		config.flags = ::gpiod::line_request::FLAG_BIAS_PULL_DOWN;
-		line.request(config);
-
-		REQUIRE(line.offset() == 4);
-		REQUIRE(line.name() == "gpio-mockup-A-4");
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE_FALSE(line.is_active_low());;
-		REQUIRE(line.is_used());
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_PUSH_PULL);
-		REQUIRE(line.bias() == ::gpiod::line::BIAS_PULL_DOWN);
-	}
-
-	SECTION("exported pull-up line")
-	{
-		::gpiod::line_request config;
-
-		config.consumer = consumer.c_str();
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		config.flags = ::gpiod::line_request::FLAG_BIAS_PULL_UP;
-		line.request(config);
-
-		REQUIRE(line.offset() == 4);
-		REQUIRE(line.name() == "gpio-mockup-A-4");
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE_FALSE(line.is_active_low());
-		REQUIRE(line.is_used());
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_PUSH_PULL);
-		REQUIRE(line.bias() == ::gpiod::line::BIAS_PULL_UP);
-	}
-}
-
-TEST_CASE("Line values can be set and read", "[line]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	::gpiod::line_request config;
-
-	config.consumer = consumer.c_str();
-
-	SECTION("get value (single line)")
-	{
-		auto line = chip.get_line(3);
-		config.request_type = ::gpiod::line_request::DIRECTION_INPUT;
-		line.request(config);
-		REQUIRE(line.get_value() == 0);
-		mockup::instance().chip_set_pull(0, 3, 1);
-		REQUIRE(line.get_value() == 1);
-	}
-
-	SECTION("set value (single line)")
-	{
-		auto line = chip.get_line(3);
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		line.request(config);
-		line.set_value(1);
-		REQUIRE(mockup::instance().chip_get_value(0, 3) == 1);
-		line.set_value(0);
-		REQUIRE(mockup::instance().chip_get_value(0, 3) == 0);
-	}
-
-	SECTION("set value with default value parameter")
-	{
-		auto line = chip.get_line(3);
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		line.request(config, 1);
-		REQUIRE(mockup::instance().chip_get_value(0, 3) == 1);
-	}
-
-	SECTION("get multiple values at once")
-	{
-		auto lines = chip.get_lines({ 0, 1, 2, 3, 4 });
-		config.request_type = ::gpiod::line_request::DIRECTION_INPUT;
-		lines.request(config);
-		REQUIRE(lines.get_values() == ::std::vector<int>({ 0, 0, 0, 0, 0 }));
-		mockup::instance().chip_set_pull(0, 1, 1);
-		mockup::instance().chip_set_pull(0, 3, 1);
-		mockup::instance().chip_set_pull(0, 4, 1);
-		REQUIRE(lines.get_values() == ::std::vector<int>({ 0, 1, 0, 1, 1 }));
-	}
-
-	SECTION("set multiple values at once")
-	{
-		auto lines = chip.get_lines({ 0, 1, 2, 6, 7 });
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		lines.request(config);
-		lines.set_values({ 1, 1, 0, 1, 0 });
-		REQUIRE(mockup::instance().chip_get_value(0, 0) == 1);
-		REQUIRE(mockup::instance().chip_get_value(0, 1) == 1);
-		REQUIRE(mockup::instance().chip_get_value(0, 2) == 0);
-		REQUIRE(mockup::instance().chip_get_value(0, 6) == 1);
-		REQUIRE(mockup::instance().chip_get_value(0, 7) == 0);
-	}
-
-	SECTION("set multiple values with default values parameter")
-	{
-		auto lines = chip.get_lines({ 1, 2, 4, 6, 7 });
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		lines.request(config, { 1, 1, 0, 1, 0 });
-		REQUIRE(mockup::instance().chip_get_value(0, 1) == 1);
-		REQUIRE(mockup::instance().chip_get_value(0, 2) == 1);
-		REQUIRE(mockup::instance().chip_get_value(0, 4) == 0);
-		REQUIRE(mockup::instance().chip_get_value(0, 6) == 1);
-		REQUIRE(mockup::instance().chip_get_value(0, 7) == 0);
-	}
-
-	SECTION("get value (single line, active-low")
-	{
-		auto line = chip.get_line(4);
-		config.request_type = ::gpiod::line_request::DIRECTION_INPUT;
-		config.flags = ::gpiod::line_request::FLAG_ACTIVE_LOW;
-		line.request(config);
-		REQUIRE(line.get_value() == 1);
-		mockup::instance().chip_set_pull(0, 4, 1);
-		REQUIRE(line.get_value() == 0);
-	}
-
-	SECTION("set value (single line, active-low)")
-	{
-		auto line = chip.get_line(3);
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		config.flags = ::gpiod::line_request::FLAG_ACTIVE_LOW;
-		line.request(config);
-		line.set_value(1);
-		REQUIRE(mockup::instance().chip_get_value(0, 3) == 0);
-		line.set_value(0);
-		REQUIRE(mockup::instance().chip_get_value(0, 3) == 1);
-	}
-}
-
-TEST_CASE("Line can be reconfigured", "[line]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	::gpiod::line_request config;
-
-	config.consumer = consumer.c_str();
-
-	SECTION("set config (single line, active-state)")
-	{
-		auto line = chip.get_line(3);
-		config.request_type = ::gpiod::line_request::DIRECTION_INPUT;
-		config.flags = 0;
-		line.request(config);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_INPUT);
-		REQUIRE_FALSE(line.is_active_low());
-
-		line.set_config(::gpiod::line_request::DIRECTION_OUTPUT,
-			::gpiod::line_request::FLAG_ACTIVE_LOW,1);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE(line.is_active_low());
-		REQUIRE(mockup::instance().chip_get_value(0, 3) == 0);
-		line.set_value(0);
-		REQUIRE(mockup::instance().chip_get_value(0, 3) == 1);
-
-		line.set_config(::gpiod::line_request::DIRECTION_OUTPUT, 0);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE_FALSE(line.is_active_low());
-		REQUIRE(mockup::instance().chip_get_value(0, 3) == 0);
-		line.set_value(1);
-		REQUIRE(mockup::instance().chip_get_value(0, 3) == 1);
-	}
-
-	SECTION("set flags (single line, active-state)")
-	{
-		auto line = chip.get_line(3);
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		config.flags = 0;
-		line.request(config,1);
-		REQUIRE(mockup::instance().chip_get_value(0, 3) == 1);
-
-		line.set_flags(::gpiod::line_request::FLAG_ACTIVE_LOW);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE(line.is_active_low());
-		REQUIRE(mockup::instance().chip_get_value(0, 3) == 0);
-
-		line.set_flags(0);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE_FALSE(line.is_active_low());
-		REQUIRE(mockup::instance().chip_get_value(0, 3) == 1);
-	}
-
-	SECTION("set flags (single line, drive)")
-	{
-		auto line = chip.get_line(3);
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		config.flags = 0;
-		line.request(config);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_PUSH_PULL);
-
-		line.set_flags(::gpiod::line_request::FLAG_OPEN_DRAIN);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_OPEN_DRAIN);
-
-		line.set_flags(::gpiod::line_request::FLAG_OPEN_SOURCE);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_OPEN_SOURCE);
-
-		line.set_flags(0);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_PUSH_PULL);
-	}
-
-	SECTION("set flags (single line, bias)")
-	{
-		auto line = chip.get_line(3);
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		config.flags = 0;
-		line.request(config);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_PUSH_PULL);
-
-		line.set_flags(::gpiod::line_request::FLAG_OPEN_DRAIN);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_OPEN_DRAIN);
-
-		line.set_flags(::gpiod::line_request::FLAG_OPEN_SOURCE);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_OPEN_SOURCE);
-
-		line.set_flags(0);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE(line.drive() == ::gpiod::line::DRIVE_PUSH_PULL);
-	}
-
-	SECTION("set direction input (single line)")
-	{
-		auto line = chip.get_line(3);
-		config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
-		config.flags = 0;
-		line.request(config);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		line.set_direction_input();
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_INPUT);
-	}
-
-	SECTION("set direction output (single line)")
-	{
-		auto line = chip.get_line(3);
-		config.request_type = ::gpiod::line_request::DIRECTION_INPUT;
-		config.flags = 0;
-		line.request(config);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_INPUT);
-		line.set_direction_output(1);
-		REQUIRE(line.direction() == ::gpiod::line::DIRECTION_OUTPUT);
-		REQUIRE(mockup::instance().chip_get_value(0, 3) == 1);
-	}
-}
-
-TEST_CASE("Exported line can be released", "[line]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	auto line = chip.get_line(4);
-	::gpiod::line_request config;
-
-	config.consumer = consumer.c_str();
-	config.request_type = ::gpiod::line_request::DIRECTION_INPUT;
-
-	line.request(config);
-
-	REQUIRE(line.get_value() == 0);
-
-	line.release();
-
-	REQUIRE_THROWS_AS(line.get_value(), ::std::system_error);
-}
-
-TEST_CASE("Uninitialized GPIO line behaves correctly", "[line]")
-{
-	::gpiod::line line;
-
-	SECTION("uninitialized line is 'false'")
-	{
-		REQUIRE_FALSE(line);
-	}
-
-	SECTION("using uninitialized line throws logic_error")
-	{
-		REQUIRE_THROWS_AS(line.name(), ::std::logic_error);
-	}
-}
-
-TEST_CASE("Uninitialized GPIO line_bulk behaves correctly", "[line][bulk]")
-{
-	::gpiod::line_bulk bulk;
-
-	SECTION("uninitialized line_bulk is 'false'")
-	{
-		REQUIRE_FALSE(bulk);
-	}
-
-	SECTION("using uninitialized line_bulk throws logic_error")
-	{
-		REQUIRE_THROWS_AS(bulk.get(0), ::std::logic_error);
-	}
-}
-
-TEST_CASE("Cannot request the same line twice", "[line]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	::gpiod::line_request config;
-
-	config.consumer = consumer.c_str();
-	config.request_type = ::gpiod::line_request::DIRECTION_INPUT;
-
-	SECTION("two separate calls to request()")
-	{
-		auto line = chip.get_line(3);
-
-		REQUIRE_NOTHROW(line.request(config));
-		REQUIRE_THROWS_AS(line.request(config), ::std::system_error);
-	}
-
-	SECTION("request the same line twice in line_bulk")
-	{
-		/*
-		 * While a line_bulk object can hold two or more line objects
-		 * representing the same line - requesting it will fail.
-		 */
-		auto lines = chip.get_lines({ 2, 3, 4, 4 });
-
-		REQUIRE_THROWS_AS(lines.request(config), ::std::system_error);
-	}
-}
-
-TEST_CASE("Cannot get/set values of unrequested lines", "[line]")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	auto line = chip.get_line(3);
-
-	SECTION("get value")
-	{
-		REQUIRE_THROWS_AS(line.get_value(), ::std::system_error);
-	}
-
-	SECTION("set value")
-	{
-		REQUIRE_THROWS_AS(line.set_value(1), ::std::system_error);
-	}
-}
-
-TEST_CASE("Line objects can be compared")
-{
-	mockup::probe_guard mockup_chips({ 8 });
-	::gpiod::chip chip(mockup::instance().chip_path(0));
-	auto line1 = chip.get_line(3);
-	auto line2 = chip.get_line(3);
-	auto line3 = chip.get_line(4);
-
-	REQUIRE(line1 == line2);
-	REQUIRE(line2 != line3);
-}
diff --git a/bindings/cxx/tests/tests-misc.cpp b/bindings/cxx/tests/tests-misc.cpp
new file mode 100644
index 0000000..ba4920f
--- /dev/null
+++ b/bindings/cxx/tests/tests-misc.cpp
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <filesystem>
+#include <gpiod.hpp>
+#include <string>
+#include <regex>
+#include <unistd.h>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using property = ::gpiosim::chip::property;
+
+namespace {
+
+class symlink_guard
+{
+public:
+	symlink_guard(const ::std::filesystem::path& target,
+		      const ::std::filesystem::path& link)
+		: _m_link(link)
+	{
+		::std::filesystem::create_symlink(target, this->_m_link);
+	}
+
+	~symlink_guard(void)
+	{
+		::std::filesystem::remove(this->_m_link);
+	}
+
+private:
+	::std::filesystem::path _m_link;
+};
+
+TEST_CASE("is_gpiochip_device() works", "[misc][chip]")
+{
+	SECTION("is_gpiochip_device() returns false for /dev/null")
+	{
+		REQUIRE_FALSE(::gpiod::is_gpiochip_device("/dev/null"));
+	}
+
+	SECTION("is_gpiochip_device() returns false for nonexistent file")
+	{
+		REQUIRE_FALSE(::gpiod::is_gpiochip_device("/dev/nonexistent"));
+	}
+
+	SECTION("is_gpiochip_device() returns true for a GPIO chip")
+	{
+		::gpiosim::chip sim;
+
+		REQUIRE(::gpiod::is_gpiochip_device(sim.dev_path()));
+	}
+
+	SECTION("is_gpiochip_device() can resolve a symlink")
+	{
+		::gpiosim::chip sim;
+		::std::string link("/tmp/gpiod-cxx-tmp-link.");
+
+		link += ::std::to_string(::getpid());
+
+		symlink_guard link_guard(sim.dev_path(), link);
+
+		REQUIRE(::gpiod::is_gpiochip_device(link));
+	}
+}
+
+TEST_CASE("version_string() returns a valid API version", "[misc]")
+{
+	SECTION("check version_string() format")
+	{
+		REQUIRE_THAT(::gpiod::version_string(),
+			     regex_matcher("^[0-9][1-9]?\\.[0-9][1-9]?([\\.0-9]?|\\-devel)$"));
+	}
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-request-config.cpp b/bindings/cxx/tests/tests-request-config.cpp
new file mode 100644
index 0000000..d1005b6
--- /dev/null
+++ b/bindings/cxx/tests/tests-request-config.cpp
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <cstddef>
+#include <gpiod.hpp>
+#include <string>
+#include <sstream>
+
+#include "helpers.hpp"
+
+using property = ::gpiod::request_config::property;
+using offsets = ::gpiod::line::offsets;
+
+namespace {
+
+TEST_CASE("request_config constructor works", "[request-config]")
+{
+	SECTION("no arguments")
+	{
+		::gpiod::request_config cfg;
+
+		REQUIRE(cfg.consumer().empty());
+		REQUIRE(cfg.offsets().empty());
+		REQUIRE(cfg.event_buffer_size() == 0);
+	}
+
+	SECTION("constructor with default settings")
+	{
+		offsets offsets({ 0, 1, 2, 3 });
+
+		::gpiod::request_config cfg({
+			{ property::CONSUMER, "foobar" },
+			{ property::OFFSETS, offsets},
+			{ property::EVENT_BUFFER_SIZE, 64 }
+		});
+
+		REQUIRE_THAT(cfg.consumer(), Catch::Equals("foobar"));
+		REQUIRE_THAT(cfg.offsets(), Catch::Equals(offsets));
+		REQUIRE(cfg.event_buffer_size() == 64);
+	}
+
+	SECTION("invalid default value types passed to constructor")
+	{
+		REQUIRE_THROWS_AS(::gpiod::request_config({
+			{ property::CONSUMER, 42 }
+		}), ::std::invalid_argument);
+
+		REQUIRE_THROWS_AS(::gpiod::request_config({
+			{ property::OFFSETS, 42 }
+		}), ::std::invalid_argument);
+
+		REQUIRE_THROWS_AS(::gpiod::request_config({
+			{ property::EVENT_BUFFER_SIZE, "foobar" }
+		}), ::std::invalid_argument);
+	}
+}
+
+TEST_CASE("request_config can be moved", "[request-config]")
+{
+	offsets offsets({ 0, 1, 2, 3 });
+
+	::gpiod::request_config cfg({
+		{ property::CONSUMER, "foobar" },
+		{ property::OFFSETS, offsets },
+		{ property::EVENT_BUFFER_SIZE, 64 }
+	});
+
+	SECTION("move constructor works")
+	{
+		auto moved(::std::move(cfg));
+		REQUIRE_THAT(moved.consumer(), Catch::Equals("foobar"));
+		REQUIRE_THAT(moved.offsets(), Catch::Equals(offsets));
+		REQUIRE(moved.event_buffer_size() == 64);
+	}
+
+	SECTION("move assignment operator works")
+	{
+		::gpiod::request_config moved;
+
+		moved = ::std::move(cfg);
+
+		REQUIRE_THAT(moved.consumer(), Catch::Equals("foobar"));
+		REQUIRE_THAT(moved.offsets(), Catch::Equals(offsets));
+		REQUIRE(moved.event_buffer_size() == 64);
+	}
+}
+
+TEST_CASE("request_config mutators work", "[request-config]")
+{
+	::gpiod::request_config cfg;
+
+	SECTION("set consumer")
+	{
+		cfg.set_consumer("foobar");
+		REQUIRE_THAT(cfg.consumer(), Catch::Equals("foobar"));
+	}
+
+	SECTION("set offsets")
+	{
+		offsets offsets({ 3, 1, 2, 7, 5 });
+		cfg.set_offsets(offsets);
+		REQUIRE_THAT(cfg.offsets(), Catch::Equals(offsets));
+	}
+
+	SECTION("set event_buffer_size")
+	{
+		cfg.set_event_buffer_size(128);
+		REQUIRE(cfg.event_buffer_size() == 128);
+	}
+}
+
+TEST_CASE("request_config generic property setting works", "[request-config]")
+{
+	::gpiod::request_config cfg;
+
+	SECTION("set consumer")
+	{
+		cfg.set_property(property::CONSUMER, "foobar");
+		REQUIRE_THAT(cfg.consumer(), Catch::Equals("foobar"));
+	}
+
+	SECTION("set offsets")
+	{
+		offsets offsets({ 3, 1, 2, 7, 5 });
+		cfg.set_property(property::OFFSETS, offsets);
+		REQUIRE_THAT(cfg.offsets(), Catch::Equals(offsets));
+	}
+
+	SECTION("set event_buffer_size")
+	{
+		cfg.set_property(property::EVENT_BUFFER_SIZE, 128);
+		REQUIRE(cfg.event_buffer_size() == 128);
+	}
+}
+
+TEST_CASE("request_config stream insertion operator works", "[request-config]")
+{
+	::gpiod::request_config cfg({
+		{ property::CONSUMER, "foobar" },
+		{ property::OFFSETS, offsets({ 0, 1, 2, 3 })},
+		{ property::EVENT_BUFFER_SIZE, 32 }
+	});
+
+	::std::stringstream buf;
+
+	buf << cfg;
+
+	::std::string expected("request_config(consumer='foobar', num_offsets=4, "
+			       "offsets=(offsets([0, 1, 2, 3])), event_buffer_size=32)");
+
+	REQUIRE_THAT(buf.str(), Catch::Equals(expected));
+}
+
+} /* namespace */
diff --git a/configure.ac b/configure.ac
index f8d34ed..ab03673 100644
--- a/configure.ac
+++ b/configure.ac
@@ -239,6 +239,7 @@ AC_CONFIG_FILES([Makefile
 		 bindings/cxx/libgpiodcxx.pc
 		 bindings/Makefile
 		 bindings/cxx/Makefile
+		 bindings/cxx/gpiodcxx/Makefile
 		 bindings/cxx/examples/Makefile
 		 bindings/cxx/tests/Makefile
 		 bindings/python/Makefile
-- 
2.30.1


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [libgpiod v2][PATCH v5] bindings: cxx: implement C++ bindings for libgpiod v2.0
  2022-03-23 14:22 [libgpiod v2][PATCH v5] bindings: cxx: implement C++ bindings for libgpiod v2.0 Bartosz Golaszewski
@ 2022-03-27 12:21 ` Kent Gibson
  2022-04-02 15:13   ` Bartosz Golaszewski
  2022-04-25 14:48   ` Bartosz Golaszewski
  0 siblings, 2 replies; 6+ messages in thread
From: Kent Gibson @ 2022-03-27 12:21 UTC (permalink / raw)
  To: Bartosz Golaszewski; +Cc: Linus Walleij, Andy Shevchenko, linux-gpio

On Wed, Mar 23, 2022 at 03:22:36PM +0100, Bartosz Golaszewski wrote:
> This rewrites the C++ bindings for libgpiod in order to work with v2.0
> version of the C API. The C++ standard use is C++17 which is well
> supported in GCC. The documentation covers the entire API so for details
> please refer to it, the tests and example programs.
> 

So C++17 for cxx bindings, but still C89 for core?
Maybe time to switch that to C99?

> Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>

Would've been nice to split this into several patches to make it more
manageable.  As it is I'm reluctant to suggest any changes as I really
don't want to see this go to a v6 :-(.

Maybe a patch each for headers, impl, tests and examples?
If nothing else that would be a better order than what we find below.
My comments below may be misordered for the same reason.

Alternatively, I don't have any major issues with this patch, so I would
also be ok with applying it as is and refining it from there.

Btw, I no longer actively use C++, so I'm not a good judge of what is
idomatic, I'm just pointing out what looks odd to me.

Anyway, enough waffle, let's dive right in...

> ---
> v4 -> v5:
> 
> While this is technically the fifth iteration of C++ bindings I'm posting, the
> change are so many, it doesn't make sense to list them here - especially since
> the C API changed in the meantime too. This time the tests have been rewritten
> as well so the bindings can actually be tested using gpio-sim.
> 
>  Doxyfile.in                                 |   4 +-
>  bindings/cxx/Makefile.am                    |  23 +-
>  bindings/cxx/chip-info.cpp                  |  74 ++
>  bindings/cxx/chip.cpp                       | 213 +++--
>  bindings/cxx/edge-event-buffer.cpp          | 115 +++
>  bindings/cxx/edge-event.cpp                 | 135 +++
>  bindings/cxx/examples/Makefile.am           |  12 +-
>  bindings/cxx/examples/gpiodetectcxx.cpp     |  10 +-
>  bindings/cxx/examples/gpiofindcxx.cpp       |   2 +-
>  bindings/cxx/examples/gpiogetcxx.cpp        |  19 +-
>  bindings/cxx/examples/gpioinfocxx.cpp       |  64 +-
>  bindings/cxx/examples/gpiomoncxx.cpp        |  53 +-
>  bindings/cxx/examples/gpiosetcxx.cpp        |  33 +-
>  bindings/cxx/exception.cpp                  | 119 +++
>  bindings/cxx/gpiod.hpp                      | 944 +-------------------
>  bindings/cxx/gpiodcxx/Makefile.am           |  18 +
>  bindings/cxx/gpiodcxx/chip-info.hpp         | 105 +++
>  bindings/cxx/gpiodcxx/chip.hpp              | 179 ++++
>  bindings/cxx/gpiodcxx/edge-event-buffer.hpp | 129 +++
>  bindings/cxx/gpiodcxx/edge-event.hpp        | 137 +++
>  bindings/cxx/gpiodcxx/exception.hpp         | 158 ++++
>  bindings/cxx/gpiodcxx/info-event.hpp        | 123 +++
>  bindings/cxx/gpiodcxx/line-config.hpp       | 564 ++++++++++++
>  bindings/cxx/gpiodcxx/line-info.hpp         | 176 ++++
>  bindings/cxx/gpiodcxx/line-request.hpp      | 221 +++++
>  bindings/cxx/gpiodcxx/line.hpp              | 274 ++++++
>  bindings/cxx/gpiodcxx/misc.hpp              |  44 +
>  bindings/cxx/gpiodcxx/request-config.hpp    | 163 ++++
>  bindings/cxx/gpiodcxx/timestamp.hpp         | 122 +++
>  bindings/cxx/info-event.cpp                 | 102 +++
>  bindings/cxx/internal.cpp                   |  28 +
>  bindings/cxx/internal.hpp                   | 208 ++++-
>  bindings/cxx/iter.cpp                       |  60 --
>  bindings/cxx/line-config.cpp                | 685 ++++++++++++++
>  bindings/cxx/line-info.cpp                  | 189 ++++
>  bindings/cxx/line-request.cpp               | 224 +++++
>  bindings/cxx/line.cpp                       | 331 ++-----
>  bindings/cxx/line_bulk.cpp                  | 366 --------
>  bindings/cxx/misc.cpp                       |  20 +
>  bindings/cxx/request-config.cpp             | 174 ++++
>  bindings/cxx/tests/Makefile.am              |  27 +-
>  bindings/cxx/tests/check-kernel.cpp         |  48 +
>  bindings/cxx/tests/gpio-mockup.cpp          | 153 ----
>  bindings/cxx/tests/gpio-mockup.hpp          |  94 --
>  bindings/cxx/tests/gpiod-cxx-test.cpp       |  55 --
>  bindings/cxx/tests/gpiosim.cpp              | 264 ++++++
>  bindings/cxx/tests/gpiosim.hpp              |  69 ++
>  bindings/cxx/tests/helpers.cpp              |  37 +
>  bindings/cxx/tests/helpers.hpp              |  36 +
>  bindings/cxx/tests/tests-chip-info.cpp      |  91 ++
>  bindings/cxx/tests/tests-chip.cpp           | 219 +++--
>  bindings/cxx/tests/tests-edge-event.cpp     | 417 +++++++++
>  bindings/cxx/tests/tests-event.cpp          | 280 ------
>  bindings/cxx/tests/tests-info-event.cpp     | 198 ++++
>  bindings/cxx/tests/tests-iter.cpp           |  21 -
>  bindings/cxx/tests/tests-line-config.cpp    | 270 ++++++
>  bindings/cxx/tests/tests-line-info.cpp      | 140 +++
>  bindings/cxx/tests/tests-line-request.cpp   | 494 ++++++++++
>  bindings/cxx/tests/tests-line.cpp           | 467 ----------
>  bindings/cxx/tests/tests-misc.cpp           |  78 ++
>  bindings/cxx/tests/tests-request-config.cpp | 155 ++++
>  configure.ac                                |   1 +
>  62 files changed, 7270 insertions(+), 2964 deletions(-)
>  create mode 100644 bindings/cxx/chip-info.cpp
>  create mode 100644 bindings/cxx/edge-event-buffer.cpp
>  create mode 100644 bindings/cxx/edge-event.cpp
>  create mode 100644 bindings/cxx/exception.cpp
>  create mode 100644 bindings/cxx/gpiodcxx/Makefile.am
>  create mode 100644 bindings/cxx/gpiodcxx/chip-info.hpp
>  create mode 100644 bindings/cxx/gpiodcxx/chip.hpp
>  create mode 100644 bindings/cxx/gpiodcxx/edge-event-buffer.hpp
>  create mode 100644 bindings/cxx/gpiodcxx/edge-event.hpp
>  create mode 100644 bindings/cxx/gpiodcxx/exception.hpp
>  create mode 100644 bindings/cxx/gpiodcxx/info-event.hpp
>  create mode 100644 bindings/cxx/gpiodcxx/line-config.hpp
>  create mode 100644 bindings/cxx/gpiodcxx/line-info.hpp
>  create mode 100644 bindings/cxx/gpiodcxx/line-request.hpp
>  create mode 100644 bindings/cxx/gpiodcxx/line.hpp
>  create mode 100644 bindings/cxx/gpiodcxx/misc.hpp
>  create mode 100644 bindings/cxx/gpiodcxx/request-config.hpp
>  create mode 100644 bindings/cxx/gpiodcxx/timestamp.hpp
>  create mode 100644 bindings/cxx/info-event.cpp
>  create mode 100644 bindings/cxx/internal.cpp
>  delete mode 100644 bindings/cxx/iter.cpp
>  create mode 100644 bindings/cxx/line-config.cpp
>  create mode 100644 bindings/cxx/line-info.cpp
>  create mode 100644 bindings/cxx/line-request.cpp
>  delete mode 100644 bindings/cxx/line_bulk.cpp
>  create mode 100644 bindings/cxx/misc.cpp
>  create mode 100644 bindings/cxx/request-config.cpp
>  create mode 100644 bindings/cxx/tests/check-kernel.cpp
>  delete mode 100644 bindings/cxx/tests/gpio-mockup.cpp
>  delete mode 100644 bindings/cxx/tests/gpio-mockup.hpp
>  delete mode 100644 bindings/cxx/tests/gpiod-cxx-test.cpp
>  create mode 100644 bindings/cxx/tests/gpiosim.cpp
>  create mode 100644 bindings/cxx/tests/gpiosim.hpp
>  create mode 100644 bindings/cxx/tests/helpers.cpp
>  create mode 100644 bindings/cxx/tests/helpers.hpp
>  create mode 100644 bindings/cxx/tests/tests-chip-info.cpp
>  create mode 100644 bindings/cxx/tests/tests-edge-event.cpp
>  delete mode 100644 bindings/cxx/tests/tests-event.cpp
>  create mode 100644 bindings/cxx/tests/tests-info-event.cpp
>  delete mode 100644 bindings/cxx/tests/tests-iter.cpp
>  create mode 100644 bindings/cxx/tests/tests-line-config.cpp
>  create mode 100644 bindings/cxx/tests/tests-line-info.cpp
>  create mode 100644 bindings/cxx/tests/tests-line-request.cpp
>  delete mode 100644 bindings/cxx/tests/tests-line.cpp

No tests for line.cpp?
Should still be some tests for the streaming operators, even if it is
just to provide an example of what to expect.

>  create mode 100644 bindings/cxx/tests/tests-misc.cpp
>  create mode 100644 bindings/cxx/tests/tests-request-config.cpp
> 

[snip]

> diff --git a/bindings/cxx/examples/gpiomoncxx.cpp b/bindings/cxx/examples/gpiomoncxx.cpp
> index 4d6ac6e..db053dd 100644
> --- a/bindings/cxx/examples/gpiomoncxx.cpp
> +++ b/bindings/cxx/examples/gpiomoncxx.cpp
> @@ -3,29 +3,27 @@
>  
>  /* Simplified C++ reimplementation of the gpiomon tool. */
>  
> -#include <gpiod.hpp>
> -
> +#include <chrono>
>  #include <cstdlib>
> +#include <gpiod.hpp>
>  #include <iostream>
>  
>  namespace {
>  
> -void print_event(const ::gpiod::line_event& event)
> +void print_event(const ::gpiod::edge_event& event)
>  {
> -	if (event.event_type == ::gpiod::line_event::RISING_EDGE)
> +	if (event.type() == ::gpiod::edge_event::event_type::RISING_EDGE)
>  		::std::cout << " RISING EDGE";
> -	else if (event.event_type == ::gpiod::line_event::FALLING_EDGE)
> -		::std::cout << "FALLING EDGE";
>  	else
> -		throw ::std::logic_error("invalid event type");
> +		::std::cout << "FALLING EDGE";
>  
>  	::std::cout << " ";
>  
> -	::std::cout << ::std::chrono::duration_cast<::std::chrono::seconds>(event.timestamp).count();
> +	::std::cout << event.timestamp_ns() / 1000000000;
>  	::std::cout << ".";
> -	::std::cout << event.timestamp.count() % 1000000000;
> +	::std::cout << event.timestamp_ns() % 1000000000;
>  
> -	::std::cout << " line: " << event.source.offset();
> +	::std::cout << " line: " << event.line_offset();
>  
>  	::std::cout << ::std::endl;
>  }
> @@ -39,25 +37,36 @@ int main(int argc, char **argv)
>  		return EXIT_FAILURE;
>  	}
>  
> -	::std::vector<unsigned int> offsets;
> +	::gpiod::line::offsets offsets;
>  	offsets.reserve(argc);
>  	for (int i = 2; i < argc; i++)
>  		offsets.push_back(::std::stoul(argv[i]));
>  
>  	::gpiod::chip chip(argv[1]);
> -	auto lines = chip.get_lines(offsets);
> -
> -	lines.request({
> -		argv[0],
> -		::gpiod::line_request::EVENT_BOTH_EDGES,
> -		0,
> -	});
> +	auto request = chip.request_lines(
> +			::gpiod::request_config({
> +				{ ::gpiod::request_config::property::OFFSETS, offsets},
> +				{ ::gpiod::request_config::property::CONSUMER, "gpiomoncxx"},
> +			}),
> +			::gpiod::line_config({
> +				{
> +					::gpiod::line_config::property::DIRECTION,
> +					::gpiod::line::direction::INPUT
> +				},
> +				{
> +					::gpiod::line_config::property::EDGE,
> +					::gpiod::line::edge::BOTH
> +				}
> +			}));
> +

Would be good to close the chip to highlight the fact that the chip is not
required once the lines are requested.

Or use ::gpiod::request_lines(path,request_config,line_config) to
request the lines.

> +	::gpiod::edge_event_buffer buffer;
>  
>  	for (;;) {
> -		auto events = lines.event_wait(::std::chrono::seconds(1));
> -		if (events) {
> -			for (auto& it: events)
> -				print_event(it.event_read());
> +		if (request.wait_edge_event(::std::chrono::seconds(5))) {
> +			request.read_edge_event(buffer);
> +
> +			for (const auto& event: buffer)
> +				print_event(event);
>  		}
>  	}
>  

What is the purpose of the wait_edge_event() here?
Wouldn't read_edge_event() block until the next event?

This example should be minimal and demonstrate how the code should
normally be used. e.g.

        for (const auto& event: request.events_iter())
                  print_event(event);

(assuming the addition of an iterator wrapping request and event buffer)

If you want to showcase more complex examples then provide them separately.

[snip]

> diff --git a/bindings/cxx/gpiodcxx/chip-info.hpp b/bindings/cxx/gpiodcxx/chip-info.hpp
> new file mode 100644
> index 0000000..9313e88

[snip]

> +
> +/**
> + * @brief Stream insertion operator for GPIO chip objects.

chip info objects

> + * @param out Output stream to write to.
> + * @param chip GPIO chip to insert into the output stream.
> + * @return Reference to out.
> + */
> +::std::ostream& operator<<(::std::ostream& out, const chip_info& chip);
> +
> +/**
> + * @}
> + */
> +
> +} /* namespace gpiod */
> +
> +#endif /* __LIBGPIOD_CXX_CHIP_INFO_HPP__ */
> diff --git a/bindings/cxx/gpiodcxx/chip.hpp b/bindings/cxx/gpiodcxx/chip.hpp
> new file mode 100644
> index 0000000..7cc2156
> --- /dev/null
> +++ b/bindings/cxx/gpiodcxx/chip.hpp
> @@ -0,0 +1,179 @@
> +/* SPDX-License-Identifier: LGPL-3.0-or-later */
> +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
> +
> +/**
> + * @file chip.hpp
> + */
> +
> +#ifndef __LIBGPIOD_CXX_CHIP_HPP__
> +#define __LIBGPIOD_CXX_CHIP_HPP__
> +
> +#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
> +#error "Only gpiod.hpp can be included directly."
> +#endif
> +
> +#include <chrono>
> +#include <cstddef>
> +#include <iostream>
> +#include <filesystem>
> +#include <memory>
> +
> +#include "line.hpp"
> +
> +namespace gpiod {
> +
> +class chip_info;
> +class info_event;
> +class line_config;
> +class line_info;
> +class line_request;
> +class request_config;
> +
> +/**
> + * @ingroup gpiod_cxx
> + * @{
> + */
> +
> +/**
> + * @brief Represents a GPIO chip.
> + */
> +class chip
> +{
> +public:
> +
> +	/**
> +	 * @brief Instantiates a new chip object by opening the device file
> +	 *        indicated by the path argument.
> +	 * @param path Path to the device file to open.
> +	 */
> +	explicit chip(const ::std::filesystem::path& path);
> +
> +	chip(const chip& other) = delete;
> +
> +	/**
> +	 * @brief Move constructor.
> +	 * @param other Object to move.
> +	 */
> +	chip(chip&& other) noexcept;
> +
> +	~chip(void);
> +
> +	chip& operator=(const chip& other) = delete;
> +
> +	/**
> +	 * @brief Move assignment operator.
> +	 * @param other Object to move.
> +	 * @return Reference to self.
> +	 */
> +	chip& operator=(chip&& other) noexcept;
> +
> +	/**
> +	 * @brief Check if this object is valid.
> +	 * @return True if this object's methods can be used, false otherwise.
> +	 *         False usually means the chip was closed. If the user calls
> +	 *         any of the methods of this class on an object for which this
> +	 *         operator returned false, a logic_error will be thrown.
> +	 */
> +	explicit operator bool(void) const noexcept;
> +
> +	/**
> +	 * @brief Close the GPIO chip device file and free associated resources.
> +	 * @note The chip object can live after calling this method but any of
> +	 *       the chip's mutators will throw a logic_error exception.
> +	 */
> +	void close(void);
> +
> +	/**
> +	 * @brief Get the filesystem path that was used to open this GPIO chip.
> +	 * @return Path to the underlying character device file.
> +	 */
> +	::std::filesystem::path path(void) const;
> +
> +	/**
> +	 * @brief Get information about the chip.
> +	 * @return New chip_info object.
> +	 */
> +	chip_info get_info(void) const;
> +
> +	/**
> +	 * @brief Retrieve the current snapshot of line information for a
> +	 *        single line.
> +	 * @param offset Offset of the line to get the info for.
> +	 * @return New ::gpiod::line_info object.
> +	 */
> +	line_info get_line_info(line::offset offset) const;
> +
> +	/**
> +	 * @brief Wrapper around ::gpiod::chip::get_line_info that retrieves
> +	 *        the line info and starts watching the line for changes.
> +	 * @param offset Offset of the line to get the info for.
> +	 * @return New ::gpiod::line_info object.
> +	 */
> +	line_info watch_line_info(line::offset offset) const;
> +
> +	/**
> +	 * @brief Stop watching the line at given offset for info events.
> +	 * @param offset Offset of the line to get the info for.
> +	 */
> +	void unwatch_line_info(line::offset offset) const;
> +
> +	/**
> +	 * @brief Get the file descriptor associated with this chip.
> +	 * @return File descriptor number.
> +	 */
> +	int fd(void) const;
> +
> +	/**
> +	 * @brief Wait for line status events on any of the watched lines
> +	 *        exposed by this chip.
> +	 * @param timeout Wait time limit in nanoseconds.
> +	 * @return True if at least one event is ready to be read. False if the
> +	 *         wait timed out.
> +	 */
> +	bool wait_info_event(const ::std::chrono::nanoseconds& timeout) const;
> +
> +	/**
> +	 * @brief Read a single line status change event from this chip.
> +	 * @return New info_event object.
> +	 */
> +	info_event read_info_event(void) const;
> +
> +	/**
> +	 * @brief Map a GPIO line's name to its offset within the chip.
> +	 * @param name Name of the GPIO line to map.
> +	 * @return Offset of the line within the chip or -1 if the line with
> +	 *         given name is not exposed by this chip.
> +	 */
> +	int find_line(const ::std::string& name) const;
> +
> +	/**
> +	 * @brief Request a set of lines for exclusive usage.
> +	 * @param req_cfg Request config object.
> +	 * @param line_cfg Line config object.
> +	 * @return New line_request object.
> +	 */
> +	line_request request_lines(const request_config& req_cfg,
> +				   const line_config& line_cfg);
> +

A common use case is to just want to request a line, so a top-level
version of this that hides the chip object from the user might be nice.
i.e. 

    line_request request_lines(const ::std::filesystem::path& path,
                               const request_config& req_cfg,
                               const line_config& line_cfg);

Also applies to C API.

> +private:
> +
> +	struct impl;
> +
> +	::std::unique_ptr<impl> _m_priv;
> +};
> +
> +/**
> + * @brief Stream insertion operator for GPIO chip objects.
> + * @param out Output stream to write to.
> + * @param chip GPIO chip to insert into the output stream.
> + * @return Reference to out.
> + */
> +::std::ostream& operator<<(::std::ostream& out, const chip& chip);
> +
> +/**
> + * @}
> + */
> +
> +} /* namespace gpiod */
> +
> +#endif /* __LIBGPIOD_CXX_CHIP_HPP__ */

[snip]

> +++ b/bindings/cxx/gpiodcxx/line-request.hpp
> @@ -0,0 +1,221 @@
> +/* SPDX-License-Identifier: LGPL-3.0-or-later */
> +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
> +
> +/**
> + * @file line-request.hpp
> + */
> +
> +#ifndef __LIBGPIOD_CXX_LINE_REQUEST_HPP__
> +#define __LIBGPIOD_CXX_LINE_REQUEST_HPP__
> +
> +#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
> +#error "Only gpiod.hpp can be included directly."
> +#endif
> +
> +#include <chrono>
> +#include <cstddef>
> +#include <iostream>
> +#include <memory>
> +
> +#include "misc.hpp"
> +
> +namespace gpiod {
> +
> +class chip;
> +class edge_event;
> +class edge_event_buffer;
> +class line_config;
> +
> +/**
> + * @ingroup gpiod_cxx
> + * @{
> + */
> +
> +/**
> + * @brief Stores the context of a set of requested GPIO lines.
> + */
> +class line_request
> +{
> +public:
> +
> +	line_request(const line_request& other) = delete;
> +
> +	/**
> +	 * @brief Move constructor.
> +	 * @param other Object to move.
> +	 */
> +	line_request(line_request&& other) noexcept;
> +
> +	~line_request(void);
> +
> +	line_request& operator=(const line_request& other) = delete;
> +
> +	/**
> +	 * @brief Move assignment operator.
> +	 * @param other Object to move.
> +	 */
> +	line_request& operator=(line_request&& other) noexcept;
> +
> +	/**
> +	 * @brief Check if this object is valid.
> +	 * @return True if this object's methods can be used, false otherwise.
> +	 *         False usually means the request was released. If the user
> +	 *         calls any of the methods of this class on an object for
> +	 *         which this operator returned false, a logic_error will be
> +	 *         thrown.
> +	 */
> +	explicit operator bool(void) const noexcept;
> +
> +	/**
> +	 * @brief Release the GPIO chip and free all associated resources.
> +	 * @note The object can still be used after this method is called but
> +	 *       using any of the mutators will result in throwing
> +	 *       a logic_error exception.
> +	 */
> +	void release(void);
> +
> +	/**
> +	 * @brief Get the number of requested lines.
> +	 * @return Number of lines in this request.
> +	 */
> +	::std::size_t num_lines(void) const;
> +
> +	/**
> +	 * @brief Get the list of offsets of requested lines.
> +	 * @return List of hardware offsets of the lines in this request.
> +	 */
> +	line::offsets offsets(void) const;
> +
> +	/**
> +	 * @brief Get the value of a single requested line.
> +	 * @param offset Offset of the line to read within the chip.
> +	 * @return Current line value.
> +	 */
> +	line::value get_value(line::offset offset);
> +
> +	/**
> +	 * @brief Get the values of a subset of requested lines.
> +	 * @param offsets Vector of line offsets
> +	 * @return Vector of lines values with indexes of values corresponding
> +	 *         to those of the offsets.
> +	 */
> +	line::values get_values(const line::offsets& offsets);
> +
> +	/**
> +	 * @brief Get the values of all requested lines.
> +	 * @return List of read values.
> +	 */
> +	line::values get_values(void);
> +
> +	/**
> +	 * @brief Get the values of a subset of requested lines into a vector
> +	 *        supplied by the caller.
> +	 * @param offsets Vector of line offsets.
> +	 * @param values Vector for storing the values. Its size must be at
> +	 *               least that of the offsets vector. The indexes of read
> +	 *               values will correspond with those in the offsets
> +	 *               vector.
> +	 */
> +	void get_values(const line::offsets& offsets, line::values& values);
> +
> +	/**
> +	 * @brief Get the values of all requested lines.
> +	 * @param values Array in which the values will be stored. Must hold
> +	 *               at least the number of elements returned by
> +	 *               line_request::num_lines.
> +	 */
> +	void get_values(line::values& values);
> +
> +	/**
> +	 * @brief Set the value of a single requested line.
> +	 * @param offset Offset of the line to set within the chip.
> +	 * @param value New line value.
> +	 */
> +	void set_value(line::offset offset, line::value value);
> +
> +	/**
> +	 * @brief Set the values of a subset of requested lines.
> +	 * @param values Vector containing a set of offset->value mappings.
> +	 */
> +	void set_values(const line::value_mappings& values);
> +
> +	/**
> +	 * @brief Set the values of a subset of requested lines.
> +	 * @param offsets Vector containing the offsets of lines to set.
> +	 * @param values Vector containing new values with indexes
> +	 *               corresponding with those in the offsets vector.
> +	 */
> +	void set_values(const line::offsets& offsets, const line::values& values);
> +
> +	/**
> +	 * @brief Set the values of all requested lines.
> +	 * @param values Array of new line values. The size must be equal to
> +	 *               the value returned by line_request::num_lines.
> +	 */
> +	void set_values(const line::values& values);
> +
> +	/**
> +	 * @brief Apply new config options to requested lines.
> +	 * @param config New configuration.
> +	 */
> +	void reconfigure_lines(const line_config& config);
> +
> +	/**
> +	 * @brief Get the file descriptor number associated with this line
> +	 *        request.
> +	 * @return File descriptor number.
> +	 */
> +	int fd(void) const;
> +
> +	/**
> +	 * @brief Wait for edge events on any of the lines requested with edge
> +	 *        detection enabled.
> +	 * @param timeout Wait time limit in nanoseconds.
> +	 * @return True if at least one event is ready to be read. False if the
> +	 *         wait timed out.
> +	 */
> +	bool wait_edge_event(const ::std::chrono::nanoseconds& timeout) const;
> +
> +	/**
> +	 * @brief Read a number of edge events from this request up to the
> +	 *        maximum capacity of the buffer.
> +	 * @param buffer Edge event buffer to read events into.
> +	 * @return Number of events read.
> +	 */
> +	::std::size_t read_edge_event(edge_event_buffer& buffer);
> +
> +	/**
> +	 * @brief Read a number of edge events from this request.
> +	 * @param buffer Edge event buffer to read events into.
> +	 * @param max_events Maximum number of events to read. Limited by the
> +	 *                   capacity of the buffer.
> +	 * @return Number of events read.
> +	 */
> +	::std::size_t read_edge_event(edge_event_buffer& buffer, ::std::size_t max_events);
> +
> +private:
> +
> +	line_request(void);
> +
> +	struct impl;
> +
> +	::std::unique_ptr<impl> _m_priv;
> +
> +	friend chip;
> +};

Just wanting to get events from a request is a common use case, and
having to use an edge_event_buffer is tedious (refer to the gpiomoncxx
example)
How about an iterator that wraps the request and an edge_event_buffer
and provides events?
I refer to this elsewhere as request.events_iter().

> +
> +/**
> + * @brief Stream insertion operator for line requests.
> + * @param out Output stream to write to.
> + * @param request Line request object to insert into the output stream.
> + * @return Reference to out.
> + */
> +::std::ostream& operator<<(::std::ostream& out, const line_request& request);
> +
> +/**
> + * @}
> + */
> +
> +} /* namespace gpiod */
> +
> +#endif /* __LIBGPIOD_CXX_LINE_REQUEST_HPP__ */
> diff --git a/bindings/cxx/gpiodcxx/line.hpp b/bindings/cxx/gpiodcxx/line.hpp
> new file mode 100644
> index 0000000..8e8a984
> --- /dev/null
> +++ b/bindings/cxx/gpiodcxx/line.hpp
> @@ -0,0 +1,274 @@
> +/* SPDX-License-Identifier: LGPL-3.0-or-later */
> +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
> +
> +/**
> + * @file line.hpp
> + */
> +
> +#ifndef __LIBGPIOD_CXX_LINE_HPP__
> +#define __LIBGPIOD_CXX_LINE_HPP__
> +
> +#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
> +#error "Only gpiod.hpp can be included directly."
> +#endif
> +
> +#include <ostream>
> +#include <utility>
> +#include <vector>
> +
> +namespace gpiod {
> +
> +/**
> + * @brief Namespace containing various type definitions for GPIO lines.
> + */
> +namespace line {
> +
> +/**
> + * @ingroup gpiod_cxx
> + * @{
> + */
> +
> +/**
> + * @brief Wrapper around unsigned int for representing line offsets.
> + */
> +class offset
> +{
> +public:
> +	/**
> +	 * @brief Constructor with implicit conversion from unsigned int.
> +	 * @param off Line offset.
> +	 */
> +	offset(unsigned int off = 0) : _m_offset(off) {	}
> +
> +	/**
> +	 * @brief Copy constructor.
> +	 * @param other Object to copy.
> +	 */
> +	offset(const offset& other) = default;
> +
> +	/**
> +	 * @brief Move constructor.
> +	 * @param other Object to move.
> +	 */
> +	offset(offset&& other) = default;
> +
> +	~offset(void) = default;
> +
> +	/**
> +	 * @brief Assignment operator.
> +	 * @param other Object to copy.
> +	 * @return Reference to self.
> +	 */
> +	offset& operator=(const offset& other) = default;
> +
> +	/**
> +	 * @brief Move assignment operator.
> +	 * @param other Object to move.
> +	 * @return Reference to self.
> +	 */
> +	offset& operator=(offset&& other) noexcept = default;
> +
> +	/**
> +	 * @brief Conversion operator to `unsigned int`.
> +	 */
> +	operator unsigned int(void) const noexcept
> +	{
> +		return this->_m_offset;
> +	}
> +
> +private:
> +	unsigned int _m_offset;
> +};
> +

Wrapping unsigned int in a class seems like overkill.
Is this just to get the streaming operators for offsets to work?

[snip]

> +GPIOD_CXX_API line::offsets line_request::offsets(void) const
> +{
> +	this->_m_priv->throw_if_released();

redundant as this->num_lines() also does it.

> +
> +	::std::vector<unsigned int> buf(this->num_lines());
> +	line::offsets offsets(this->num_lines());
> +
> +	::gpiod_line_request_get_offsets(this->_m_priv->request.get(), buf.data());
> +
> +	for (unsigned int i = 0; i < this->num_lines(); i++)
> +		offsets[i] = buf[i];
> +

Cache num_lines locally rather than calling num_lines() several times.


[snip]

> +template<typename T>
> +::std::ostream& insert_vector(::std::ostream& out,
> +			      const ::std::string& name, const ::std::vector<T>& vec)
>  {
> -	this->_m_line = nullptr;
> -	this->_m_owner.reset();
> -}
> +	out << name << "([";
> +	::std::copy(vec.begin(), ::std::prev(vec.end()),
> +		    ::std::ostream_iterator<T>(out, ", "));
> +	out << vec.back();
> +	out << "])";
>  

What formatting are you after for vectors?
Is the double bracketing necessary?

[snip]

> +TEST_CASE("line_request stream insertion operator works", "[line-request]")
> +{
> +	::gpiosim::chip sim({{ simprop::NUM_LINES, 4 }});
> +	::gpiod::chip chip(sim.dev_path());
> +
> +	auto request = chip.request_lines(
> +		::gpiod::request_config({
> +			{ reqprop::OFFSETS, offsets({ 3, 1, 0, 2 }) }
> +		}),
> +		::gpiod::line_config()
> +	);
> +
> +	::std::stringstream buf, expected;
> +
> +	expected << "line_request(num_lines=4, line_offsets=[offsets([3, 1, 0, 2])], fd=" <<
> +		    request.fd() << ")";
> +

Still not sure what formatting you are going for with the vectors.
And the line_offsets=[] is not consistent with the offsets=() used in
request_config.

[snip]

> +TEST_CASE("request_config stream insertion operator works", "[request-config]")
> +{
> +	::gpiod::request_config cfg({
> +		{ property::CONSUMER, "foobar" },
> +		{ property::OFFSETS, offsets({ 0, 1, 2, 3 })},
> +		{ property::EVENT_BUFFER_SIZE, 32 }
> +	});
> +
> +	::std::stringstream buf;
> +
> +	buf << cfg;
> +
> +	::std::string expected("request_config(consumer='foobar', num_offsets=4, "
> +			       "offsets=(offsets([0, 1, 2, 3])), event_buffer_size=32)");
> +

The streaming output stutters.
"offsets=(offsets([0, 1, 2, 3]))" should be "offsets=([0, 1, 2, 3])"?
And even then one of the sets of brackets is redundant.

> +	REQUIRE_THAT(buf.str(), Catch::Equals(expected));
> +}
> +
> +} /* namespace */
> diff --git a/configure.ac b/configure.ac
> index f8d34ed..ab03673 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -239,6 +239,7 @@ AC_CONFIG_FILES([Makefile
>  		 bindings/cxx/libgpiodcxx.pc
>  		 bindings/Makefile
>  		 bindings/cxx/Makefile
> +		 bindings/cxx/gpiodcxx/Makefile
>  		 bindings/cxx/examples/Makefile
>  		 bindings/cxx/tests/Makefile
>  		 bindings/python/Makefile
> -- 
> 2.30.1
> 

Phew.  So nothing major.

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [libgpiod v2][PATCH v5] bindings: cxx: implement C++ bindings for libgpiod v2.0
  2022-03-27 12:21 ` Kent Gibson
@ 2022-04-02 15:13   ` Bartosz Golaszewski
  2022-04-04  9:54     ` Kent Gibson
  2022-04-25 14:48   ` Bartosz Golaszewski
  1 sibling, 1 reply; 6+ messages in thread
From: Bartosz Golaszewski @ 2022-04-02 15:13 UTC (permalink / raw)
  To: Kent Gibson; +Cc: Linus Walleij, Andy Shevchenko, open list:GPIO SUBSYSTEM

On Sun, Mar 27, 2022 at 2:22 PM Kent Gibson <warthog618@gmail.com> wrote:
>
> On Wed, Mar 23, 2022 at 03:22:36PM +0100, Bartosz Golaszewski wrote:
> > This rewrites the C++ bindings for libgpiod in order to work with v2.0
> > version of the C API. The C++ standard use is C++17 which is well
> > supported in GCC. The documentation covers the entire API so for details
> > please refer to it, the tests and example programs.
> >
>
> So C++17 for cxx bindings, but still C89 for core?
> Maybe time to switch that to C99?
>

I'm not saying no, but what would we gain from that in C that isn't
already provided by the gnu89 standard we're using? Declaring
variables in for loops if all I can think of and that's not really
useful for us as we're not providing lots of for_each type macros.

> > Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
>
> Would've been nice to split this into several patches to make it more
> manageable.  As it is I'm reluctant to suggest any changes as I really
> don't want to see this go to a v6 :-(.
>
> Maybe a patch each for headers, impl, tests and examples?
> If nothing else that would be a better order than what we find below.
> My comments below may be misordered for the same reason.
>

Will do in the next iteration.

> Alternatively, I don't have any major issues with this patch, so I would
> also be ok with applying it as is and refining it from there.
>
> Btw, I no longer actively use C++, so I'm not a good judge of what is
> idomatic, I'm just pointing out what looks odd to me.
>

Yeah I need to make a list of C++ people who contributed code to
libgpiod before and Cc them next time.

[snip]

>
> No tests for line.cpp?
> Should still be some tests for the streaming operators, even if it is
> just to provide an example of what to expect.
>

I ran the code with gcov and all those stream operators are already
tested in other places, no need to be that redundant. I even skipped
the tests for stream operators on purpose for objects logically
"embedded" in other objects (like chip_info).

> >  create mode 100644 bindings/cxx/tests/tests-misc.cpp
> >  create mode 100644 bindings/cxx/tests/tests-request-config.cpp
> >
>
> [snip]
>
> > diff --git a/bindings/cxx/examples/gpiomoncxx.cpp b/bindings/cxx/examples/gpiomoncxx.cpp
> > index 4d6ac6e..db053dd 100644
> > --- a/bindings/cxx/examples/gpiomoncxx.cpp
> > +++ b/bindings/cxx/examples/gpiomoncxx.cpp
> > @@ -3,29 +3,27 @@
> >
> >  /* Simplified C++ reimplementation of the gpiomon tool. */
> >
> > -#include <gpiod.hpp>
> > -
> > +#include <chrono>
> >  #include <cstdlib>
> > +#include <gpiod.hpp>
> >  #include <iostream>
> >
> >  namespace {
> >
> > -void print_event(const ::gpiod::line_event& event)
> > +void print_event(const ::gpiod::edge_event& event)
> >  {
> > -     if (event.event_type == ::gpiod::line_event::RISING_EDGE)
> > +     if (event.type() == ::gpiod::edge_event::event_type::RISING_EDGE)
> >               ::std::cout << " RISING EDGE";
> > -     else if (event.event_type == ::gpiod::line_event::FALLING_EDGE)
> > -             ::std::cout << "FALLING EDGE";
> >       else
> > -             throw ::std::logic_error("invalid event type");
> > +             ::std::cout << "FALLING EDGE";
> >
> >       ::std::cout << " ";
> >
> > -     ::std::cout << ::std::chrono::duration_cast<::std::chrono::seconds>(event.timestamp).count();
> > +     ::std::cout << event.timestamp_ns() / 1000000000;
> >       ::std::cout << ".";
> > -     ::std::cout << event.timestamp.count() % 1000000000;
> > +     ::std::cout << event.timestamp_ns() % 1000000000;
> >
> > -     ::std::cout << " line: " << event.source.offset();
> > +     ::std::cout << " line: " << event.line_offset();
> >
> >       ::std::cout << ::std::endl;
> >  }
> > @@ -39,25 +37,36 @@ int main(int argc, char **argv)
> >               return EXIT_FAILURE;
> >       }
> >
> > -     ::std::vector<unsigned int> offsets;
> > +     ::gpiod::line::offsets offsets;
> >       offsets.reserve(argc);
> >       for (int i = 2; i < argc; i++)
> >               offsets.push_back(::std::stoul(argv[i]));
> >
> >       ::gpiod::chip chip(argv[1]);
> > -     auto lines = chip.get_lines(offsets);
> > -
> > -     lines.request({
> > -             argv[0],
> > -             ::gpiod::line_request::EVENT_BOTH_EDGES,
> > -             0,
> > -     });
> > +     auto request = chip.request_lines(
> > +                     ::gpiod::request_config({
> > +                             { ::gpiod::request_config::property::OFFSETS, offsets},
> > +                             { ::gpiod::request_config::property::CONSUMER, "gpiomoncxx"},
> > +                     }),
> > +                     ::gpiod::line_config({
> > +                             {
> > +                                     ::gpiod::line_config::property::DIRECTION,
> > +                                     ::gpiod::line::direction::INPUT
> > +                             },
> > +                             {
> > +                                     ::gpiod::line_config::property::EDGE,
> > +                                     ::gpiod::line::edge::BOTH
> > +                             }
> > +                     }));
> > +
>
> Would be good to close the chip to highlight the fact that the chip is not
> required once the lines are requested.
>

There's a special test case for that already elsewhere.

> Or use ::gpiod::request_lines(path,request_config,line_config) to
> request the lines.
>
> > +     ::gpiod::edge_event_buffer buffer;
> >
> >       for (;;) {
> > -             auto events = lines.event_wait(::std::chrono::seconds(1));
> > -             if (events) {
> > -                     for (auto& it: events)
> > -                             print_event(it.event_read());
> > +             if (request.wait_edge_event(::std::chrono::seconds(5))) {
> > +                     request.read_edge_event(buffer);
> > +
> > +                     for (const auto& event: buffer)
> > +                             print_event(event);
> >               }
> >       }
> >
>
> What is the purpose of the wait_edge_event() here?
> Wouldn't read_edge_event() block until the next event?
>

Indeed and it would block forever if the event doesn't arrive. The
call is to make sure there's an even available.

> This example should be minimal and demonstrate how the code should
> normally be used. e.g.
>
>         for (const auto& event: request.events_iter())
>                   print_event(event);
>
> (assuming the addition of an iterator wrapping request and event buffer)
>
> If you want to showcase more complex examples then provide them separately.
>
> [snip]
>

I'm not how that would work - would it create a new buffer every time
we read events? Doesn't that defeat the entire purpose of the event
buffer of preallocating memory for the events?

> > +     /**
> > +      * @brief Request a set of lines for exclusive usage.
> > +      * @param req_cfg Request config object.
> > +      * @param line_cfg Line config object.
> > +      * @return New line_request object.
> > +      */
> > +     line_request request_lines(const request_config& req_cfg,
> > +                                const line_config& line_cfg);
> > +
>
> A common use case is to just want to request a line, so a top-level
> version of this that hides the chip object from the user might be nice.
> i.e.
>
>     line_request request_lines(const ::std::filesystem::path& path,
>                                const request_config& req_cfg,
>                                const line_config& line_cfg);
>
> Also applies to C API.

I replied about this under the C patch series.

>
> > +private:
> > +
> > +     struct impl;
> > +
> > +     ::std::unique_ptr<impl> _m_priv;
> > +};
> > +
> > +/**
> > + * @brief Stream insertion operator for GPIO chip objects.
> > + * @param out Output stream to write to.
> > + * @param chip GPIO chip to insert into the output stream.
> > + * @return Reference to out.
> > + */
> > +::std::ostream& operator<<(::std::ostream& out, const chip& chip);
> > +
> > +/**
> > + * @}
> > + */
> > +
> > +} /* namespace gpiod */
> > +
> > +#endif /* __LIBGPIOD_CXX_CHIP_HPP__ */
>
> [snip]
>
> > +++ b/bindings/cxx/gpiodcxx/line-request.hpp
> > @@ -0,0 +1,221 @@
> > +/* SPDX-License-Identifier: LGPL-3.0-or-later */
> > +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
> > +
> > +/**
> > + * @file line-request.hpp
> > + */
> > +
> > +#ifndef __LIBGPIOD_CXX_LINE_REQUEST_HPP__
> > +#define __LIBGPIOD_CXX_LINE_REQUEST_HPP__
> > +
> > +#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
> > +#error "Only gpiod.hpp can be included directly."
> > +#endif
> > +
> > +#include <chrono>
> > +#include <cstddef>
> > +#include <iostream>
> > +#include <memory>
> > +
> > +#include "misc.hpp"
> > +
> > +namespace gpiod {
> > +
> > +class chip;
> > +class edge_event;
> > +class edge_event_buffer;
> > +class line_config;
> > +
> > +/**
> > + * @ingroup gpiod_cxx
> > + * @{
> > + */
> > +
> > +/**
> > + * @brief Stores the context of a set of requested GPIO lines.
> > + */
> > +class line_request
> > +{
> > +public:
> > +
> > +     line_request(const line_request& other) = delete;
> > +
> > +     /**
> > +      * @brief Move constructor.
> > +      * @param other Object to move.
> > +      */
> > +     line_request(line_request&& other) noexcept;
> > +
> > +     ~line_request(void);
> > +
> > +     line_request& operator=(const line_request& other) = delete;
> > +
> > +     /**
> > +      * @brief Move assignment operator.
> > +      * @param other Object to move.
> > +      */
> > +     line_request& operator=(line_request&& other) noexcept;
> > +
> > +     /**
> > +      * @brief Check if this object is valid.
> > +      * @return True if this object's methods can be used, false otherwise.
> > +      *         False usually means the request was released. If the user
> > +      *         calls any of the methods of this class on an object for
> > +      *         which this operator returned false, a logic_error will be
> > +      *         thrown.
> > +      */
> > +     explicit operator bool(void) const noexcept;
> > +
> > +     /**
> > +      * @brief Release the GPIO chip and free all associated resources.
> > +      * @note The object can still be used after this method is called but
> > +      *       using any of the mutators will result in throwing
> > +      *       a logic_error exception.
> > +      */
> > +     void release(void);
> > +
> > +     /**
> > +      * @brief Get the number of requested lines.
> > +      * @return Number of lines in this request.
> > +      */
> > +     ::std::size_t num_lines(void) const;
> > +
> > +     /**
> > +      * @brief Get the list of offsets of requested lines.
> > +      * @return List of hardware offsets of the lines in this request.
> > +      */
> > +     line::offsets offsets(void) const;
> > +
> > +     /**
> > +      * @brief Get the value of a single requested line.
> > +      * @param offset Offset of the line to read within the chip.
> > +      * @return Current line value.
> > +      */
> > +     line::value get_value(line::offset offset);
> > +
> > +     /**
> > +      * @brief Get the values of a subset of requested lines.
> > +      * @param offsets Vector of line offsets
> > +      * @return Vector of lines values with indexes of values corresponding
> > +      *         to those of the offsets.
> > +      */
> > +     line::values get_values(const line::offsets& offsets);
> > +
> > +     /**
> > +      * @brief Get the values of all requested lines.
> > +      * @return List of read values.
> > +      */
> > +     line::values get_values(void);
> > +
> > +     /**
> > +      * @brief Get the values of a subset of requested lines into a vector
> > +      *        supplied by the caller.
> > +      * @param offsets Vector of line offsets.
> > +      * @param values Vector for storing the values. Its size must be at
> > +      *               least that of the offsets vector. The indexes of read
> > +      *               values will correspond with those in the offsets
> > +      *               vector.
> > +      */
> > +     void get_values(const line::offsets& offsets, line::values& values);
> > +
> > +     /**
> > +      * @brief Get the values of all requested lines.
> > +      * @param values Array in which the values will be stored. Must hold
> > +      *               at least the number of elements returned by
> > +      *               line_request::num_lines.
> > +      */
> > +     void get_values(line::values& values);
> > +
> > +     /**
> > +      * @brief Set the value of a single requested line.
> > +      * @param offset Offset of the line to set within the chip.
> > +      * @param value New line value.
> > +      */
> > +     void set_value(line::offset offset, line::value value);
> > +
> > +     /**
> > +      * @brief Set the values of a subset of requested lines.
> > +      * @param values Vector containing a set of offset->value mappings.
> > +      */
> > +     void set_values(const line::value_mappings& values);
> > +
> > +     /**
> > +      * @brief Set the values of a subset of requested lines.
> > +      * @param offsets Vector containing the offsets of lines to set.
> > +      * @param values Vector containing new values with indexes
> > +      *               corresponding with those in the offsets vector.
> > +      */
> > +     void set_values(const line::offsets& offsets, const line::values& values);
> > +
> > +     /**
> > +      * @brief Set the values of all requested lines.
> > +      * @param values Array of new line values. The size must be equal to
> > +      *               the value returned by line_request::num_lines.
> > +      */
> > +     void set_values(const line::values& values);
> > +
> > +     /**
> > +      * @brief Apply new config options to requested lines.
> > +      * @param config New configuration.
> > +      */
> > +     void reconfigure_lines(const line_config& config);
> > +
> > +     /**
> > +      * @brief Get the file descriptor number associated with this line
> > +      *        request.
> > +      * @return File descriptor number.
> > +      */
> > +     int fd(void) const;
> > +
> > +     /**
> > +      * @brief Wait for edge events on any of the lines requested with edge
> > +      *        detection enabled.
> > +      * @param timeout Wait time limit in nanoseconds.
> > +      * @return True if at least one event is ready to be read. False if the
> > +      *         wait timed out.
> > +      */
> > +     bool wait_edge_event(const ::std::chrono::nanoseconds& timeout) const;
> > +
> > +     /**
> > +      * @brief Read a number of edge events from this request up to the
> > +      *        maximum capacity of the buffer.
> > +      * @param buffer Edge event buffer to read events into.
> > +      * @return Number of events read.
> > +      */
> > +     ::std::size_t read_edge_event(edge_event_buffer& buffer);
> > +
> > +     /**
> > +      * @brief Read a number of edge events from this request.
> > +      * @param buffer Edge event buffer to read events into.
> > +      * @param max_events Maximum number of events to read. Limited by the
> > +      *                   capacity of the buffer.
> > +      * @return Number of events read.
> > +      */
> > +     ::std::size_t read_edge_event(edge_event_buffer& buffer, ::std::size_t max_events);
> > +
> > +private:
> > +
> > +     line_request(void);
> > +
> > +     struct impl;
> > +
> > +     ::std::unique_ptr<impl> _m_priv;
> > +
> > +     friend chip;
> > +};
>
> Just wanting to get events from a request is a common use case, and
> having to use an edge_event_buffer is tedious (refer to the gpiomoncxx
> example)
> How about an iterator that wraps the request and an edge_event_buffer
> and provides events?
> I refer to this elsewhere as request.events_iter().
>
> > +
> > +/**
> > + * @brief Stream insertion operator for line requests.
> > + * @param out Output stream to write to.
> > + * @param request Line request object to insert into the output stream.
> > + * @return Reference to out.
> > + */
> > +::std::ostream& operator<<(::std::ostream& out, const line_request& request);
> > +
> > +/**
> > + * @}
> > + */
> > +
> > +} /* namespace gpiod */
> > +
> > +#endif /* __LIBGPIOD_CXX_LINE_REQUEST_HPP__ */
> > diff --git a/bindings/cxx/gpiodcxx/line.hpp b/bindings/cxx/gpiodcxx/line.hpp
> > new file mode 100644
> > index 0000000..8e8a984
> > --- /dev/null
> > +++ b/bindings/cxx/gpiodcxx/line.hpp
> > @@ -0,0 +1,274 @@
> > +/* SPDX-License-Identifier: LGPL-3.0-or-later */
> > +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
> > +
> > +/**
> > + * @file line.hpp
> > + */
> > +
> > +#ifndef __LIBGPIOD_CXX_LINE_HPP__
> > +#define __LIBGPIOD_CXX_LINE_HPP__
> > +
> > +#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
> > +#error "Only gpiod.hpp can be included directly."
> > +#endif
> > +
> > +#include <ostream>
> > +#include <utility>
> > +#include <vector>
> > +
> > +namespace gpiod {
> > +
> > +/**
> > + * @brief Namespace containing various type definitions for GPIO lines.
> > + */
> > +namespace line {
> > +
> > +/**
> > + * @ingroup gpiod_cxx
> > + * @{
> > + */
> > +
> > +/**
> > + * @brief Wrapper around unsigned int for representing line offsets.
> > + */
> > +class offset
> > +{
> > +public:
> > +     /**
> > +      * @brief Constructor with implicit conversion from unsigned int.
> > +      * @param off Line offset.
> > +      */
> > +     offset(unsigned int off = 0) : _m_offset(off) { }
> > +
> > +     /**
> > +      * @brief Copy constructor.
> > +      * @param other Object to copy.
> > +      */
> > +     offset(const offset& other) = default;
> > +
> > +     /**
> > +      * @brief Move constructor.
> > +      * @param other Object to move.
> > +      */
> > +     offset(offset&& other) = default;
> > +
> > +     ~offset(void) = default;
> > +
> > +     /**
> > +      * @brief Assignment operator.
> > +      * @param other Object to copy.
> > +      * @return Reference to self.
> > +      */
> > +     offset& operator=(const offset& other) = default;
> > +
> > +     /**
> > +      * @brief Move assignment operator.
> > +      * @param other Object to move.
> > +      * @return Reference to self.
> > +      */
> > +     offset& operator=(offset&& other) noexcept = default;
> > +
> > +     /**
> > +      * @brief Conversion operator to `unsigned int`.
> > +      */
> > +     operator unsigned int(void) const noexcept
> > +     {
> > +             return this->_m_offset;
> > +     }
> > +
> > +private:
> > +     unsigned int _m_offset;
> > +};
> > +
>
> Wrapping unsigned int in a class seems like overkill.
> Is this just to get the streaming operators for offsets to work?
>

Yes. Otherwise we'd be providing a generic stream operator for all
vector specializations. And this wrapping doesn't really do much other
than providing a new type within the gpiod namespace. The constructor
with implicit conversion and the operator unsigned int() make sure of
this.

> [snip]
>
> > +GPIOD_CXX_API line::offsets line_request::offsets(void) const
> > +{
> > +     this->_m_priv->throw_if_released();
>
> redundant as this->num_lines() also does it.
>
> > +
> > +     ::std::vector<unsigned int> buf(this->num_lines());
> > +     line::offsets offsets(this->num_lines());
> > +
> > +     ::gpiod_line_request_get_offsets(this->_m_priv->request.get(), buf.data());
> > +
> > +     for (unsigned int i = 0; i < this->num_lines(); i++)
> > +             offsets[i] = buf[i];
> > +
>
> Cache num_lines locally rather than calling num_lines() several times.
>
>
> [snip]
>
> > +template<typename T>
> > +::std::ostream& insert_vector(::std::ostream& out,
> > +                           const ::std::string& name, const ::std::vector<T>& vec)
> >  {
> > -     this->_m_line = nullptr;
> > -     this->_m_owner.reset();
> > -}
> > +     out << name << "([";
> > +     ::std::copy(vec.begin(), ::std::prev(vec.end()),
> > +                 ::std::ostream_iterator<T>(out, ", "));
> > +     out << vec.back();
> > +     out << "])";
> >
>
> What formatting are you after for vectors?
> Is the double bracketing necessary?
>
> [snip]
>
> > +TEST_CASE("line_request stream insertion operator works", "[line-request]")
> > +{
> > +     ::gpiosim::chip sim({{ simprop::NUM_LINES, 4 }});
> > +     ::gpiod::chip chip(sim.dev_path());
> > +
> > +     auto request = chip.request_lines(
> > +             ::gpiod::request_config({
> > +                     { reqprop::OFFSETS, offsets({ 3, 1, 0, 2 }) }
> > +             }),
> > +             ::gpiod::line_config()
> > +     );
> > +
> > +     ::std::stringstream buf, expected;
> > +
> > +     expected << "line_request(num_lines=4, line_offsets=[offsets([3, 1, 0, 2])], fd=" <<
> > +                 request.fd() << ")";
> > +
>
> Still not sure what formatting you are going for with the vectors.
> And the line_offsets=[] is not consistent with the offsets=() used in
> request_config.
>

Yep need to make that consistent, thanks.

> [snip]
>
> > +TEST_CASE("request_config stream insertion operator works", "[request-config]")
> > +{
> > +     ::gpiod::request_config cfg({
> > +             { property::CONSUMER, "foobar" },
> > +             { property::OFFSETS, offsets({ 0, 1, 2, 3 })},
> > +             { property::EVENT_BUFFER_SIZE, 32 }
> > +     });
> > +
> > +     ::std::stringstream buf;
> > +
> > +     buf << cfg;
> > +
> > +     ::std::string expected("request_config(consumer='foobar', num_offsets=4, "
> > +                            "offsets=(offsets([0, 1, 2, 3])), event_buffer_size=32)");
> > +
>
> The streaming output stutters.
> "offsets=(offsets([0, 1, 2, 3]))" should be "offsets=([0, 1, 2, 3])"?
> And even then one of the sets of brackets is redundant.
>

Actually maybe it should be offsets=gpiod::offsets(0, 1, 2 ,3)? I
would like to indicate the type and not make it a generic streaming
operator for vectors like what Qt provides.

> > +     REQUIRE_THAT(buf.str(), Catch::Equals(expected));
> > +}
> > +
> > +} /* namespace */
> > diff --git a/configure.ac b/configure.ac
> > index f8d34ed..ab03673 100644
> > --- a/configure.ac
> > +++ b/configure.ac
> > @@ -239,6 +239,7 @@ AC_CONFIG_FILES([Makefile
> >                bindings/cxx/libgpiodcxx.pc
> >                bindings/Makefile
> >                bindings/cxx/Makefile
> > +              bindings/cxx/gpiodcxx/Makefile
> >                bindings/cxx/examples/Makefile
> >                bindings/cxx/tests/Makefile
> >                bindings/python/Makefile
> > --
> > 2.30.1
> >
>
> Phew.  So nothing major.
>
> Cheers,
> Kent.

Thanks Kent! Wherever there's no comment, I will address the issue.

Bart

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [libgpiod v2][PATCH v5] bindings: cxx: implement C++ bindings for libgpiod v2.0
  2022-04-02 15:13   ` Bartosz Golaszewski
@ 2022-04-04  9:54     ` Kent Gibson
  0 siblings, 0 replies; 6+ messages in thread
From: Kent Gibson @ 2022-04-04  9:54 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: Linus Walleij, Andy Shevchenko, open list:GPIO SUBSYSTEM

On Sat, Apr 02, 2022 at 05:13:34PM +0200, Bartosz Golaszewski wrote:
> On Sun, Mar 27, 2022 at 2:22 PM Kent Gibson <warthog618@gmail.com> wrote:
> >
> > On Wed, Mar 23, 2022 at 03:22:36PM +0100, Bartosz Golaszewski wrote:
> > > This rewrites the C++ bindings for libgpiod in order to work with v2.0
> > > version of the C API. The C++ standard use is C++17 which is well
> > > supported in GCC. The documentation covers the entire API so for details
> > > please refer to it, the tests and example programs.
> > >
> >
> > So C++17 for cxx bindings, but still C89 for core?
> > Maybe time to switch that to C99?
> >
> 
> I'm not saying no, but what would we gain from that in C that isn't
> already provided by the gnu89 standard we're using? Declaring
> variables in for loops if all I can think of and that's not really
> useful for us as we're not providing lots of for_each type macros.
> 

It is just for the loop vars - I prefer to have them declared with the
loop.

> > > Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
> >
> > Would've been nice to split this into several patches to make it more
> > manageable.  As it is I'm reluctant to suggest any changes as I really
> > don't want to see this go to a v6 :-(.
> >
> > Maybe a patch each for headers, impl, tests and examples?
> > If nothing else that would be a better order than what we find below.
> > My comments below may be misordered for the same reason.
> >
> 
> Will do in the next iteration.
> 
> > Alternatively, I don't have any major issues with this patch, so I would
> > also be ok with applying it as is and refining it from there.
> >
> > Btw, I no longer actively use C++, so I'm not a good judge of what is
> > idomatic, I'm just pointing out what looks odd to me.
> >
> 
> Yeah I need to make a list of C++ people who contributed code to
> libgpiod before and Cc them next time.
> 
> [snip]
> 
> >
> > No tests for line.cpp?
> > Should still be some tests for the streaming operators, even if it is
> > just to provide an example of what to expect.
> >
> 
> I ran the code with gcov and all those stream operators are already
> tested in other places, no need to be that redundant. I even skipped
> the tests for stream operators on purpose for objects logically
> "embedded" in other objects (like chip_info).
> 

Ok, we have a very different philosophy on testing.
In my book there is no implicit testing.
If it isn't tested explicitly then it isn't tested.
And it should be straight forward to locate the test for a function.
That can be challenging if it is tested implicitly while testing
something else.

And I use coverage tools to catch where I may have inadvertently missed
tests - not to indicate that I've tested enough.  You have never tested
enough.  Redundancy in tests is a good thing, and so not something I
avoid.

Plus is is good to have an example of the code in use - even if it is a
trivial test case.

> > >  create mode 100644 bindings/cxx/tests/tests-misc.cpp
> > >  create mode 100644 bindings/cxx/tests/tests-request-config.cpp
> > >
> >
> > [snip]
> >
> > > diff --git a/bindings/cxx/examples/gpiomoncxx.cpp b/bindings/cxx/examples/gpiomoncxx.cpp
> > > index 4d6ac6e..db053dd 100644
> > > --- a/bindings/cxx/examples/gpiomoncxx.cpp
> > > +++ b/bindings/cxx/examples/gpiomoncxx.cpp
> > > @@ -3,29 +3,27 @@
> > >
> > >  /* Simplified C++ reimplementation of the gpiomon tool. */
> > >
> > > -#include <gpiod.hpp>
> > > -
> > > +#include <chrono>
> > >  #include <cstdlib>
> > > +#include <gpiod.hpp>
> > >  #include <iostream>
> > >
> > >  namespace {
> > >
> > > -void print_event(const ::gpiod::line_event& event)
> > > +void print_event(const ::gpiod::edge_event& event)
> > >  {
> > > -     if (event.event_type == ::gpiod::line_event::RISING_EDGE)
> > > +     if (event.type() == ::gpiod::edge_event::event_type::RISING_EDGE)
> > >               ::std::cout << " RISING EDGE";
> > > -     else if (event.event_type == ::gpiod::line_event::FALLING_EDGE)
> > > -             ::std::cout << "FALLING EDGE";
> > >       else
> > > -             throw ::std::logic_error("invalid event type");
> > > +             ::std::cout << "FALLING EDGE";
> > >
> > >       ::std::cout << " ";
> > >
> > > -     ::std::cout << ::std::chrono::duration_cast<::std::chrono::seconds>(event.timestamp).count();
> > > +     ::std::cout << event.timestamp_ns() / 1000000000;
> > >       ::std::cout << ".";
> > > -     ::std::cout << event.timestamp.count() % 1000000000;
> > > +     ::std::cout << event.timestamp_ns() % 1000000000;
> > >
> > > -     ::std::cout << " line: " << event.source.offset();
> > > +     ::std::cout << " line: " << event.line_offset();
> > >
> > >       ::std::cout << ::std::endl;
> > >  }
> > > @@ -39,25 +37,36 @@ int main(int argc, char **argv)
> > >               return EXIT_FAILURE;
> > >       }
> > >
> > > -     ::std::vector<unsigned int> offsets;
> > > +     ::gpiod::line::offsets offsets;
> > >       offsets.reserve(argc);
> > >       for (int i = 2; i < argc; i++)
> > >               offsets.push_back(::std::stoul(argv[i]));
> > >
> > >       ::gpiod::chip chip(argv[1]);
> > > -     auto lines = chip.get_lines(offsets);
> > > -
> > > -     lines.request({
> > > -             argv[0],
> > > -             ::gpiod::line_request::EVENT_BOTH_EDGES,
> > > -             0,
> > > -     });
> > > +     auto request = chip.request_lines(
> > > +                     ::gpiod::request_config({
> > > +                             { ::gpiod::request_config::property::OFFSETS, offsets},
> > > +                             { ::gpiod::request_config::property::CONSUMER, "gpiomoncxx"},
> > > +                     }),
> > > +                     ::gpiod::line_config({
> > > +                             {
> > > +                                     ::gpiod::line_config::property::DIRECTION,
> > > +                                     ::gpiod::line::direction::INPUT
> > > +                             },
> > > +                             {
> > > +                                     ::gpiod::line_config::property::EDGE,
> > > +                                     ::gpiod::line::edge::BOTH
> > > +                             }
> > > +                     }));
> > > +
> >
> > Would be good to close the chip to highlight the fact that the chip is not
> > required once the lines are requested.
> >
> 
> There's a special test case for that already elsewhere.
> 

I'm not asking if it is tested or not, I'm suggesting that example code
should keep object lifetimes minimal so as not to give a false impression
that an object is required when in fact it is not.

> > Or use ::gpiod::request_lines(path,request_config,line_config) to
> > request the lines.
> >
> > > +     ::gpiod::edge_event_buffer buffer;
> > >
> > >       for (;;) {
> > > -             auto events = lines.event_wait(::std::chrono::seconds(1));
> > > -             if (events) {
> > > -                     for (auto& it: events)
> > > -                             print_event(it.event_read());
> > > +             if (request.wait_edge_event(::std::chrono::seconds(5))) {
> > > +                     request.read_edge_event(buffer);
> > > +
> > > +                     for (const auto& event: buffer)
> > > +                             print_event(event);
> > >               }
> > >       }
> > >
> >
> > What is the purpose of the wait_edge_event() here?
> > Wouldn't read_edge_event() block until the next event?
> >
> 
> Indeed and it would block forever if the event doesn't arrive. The
> call is to make sure there's an even available.
> 

I still don't see what that adds.  If there are no events then every 5
seconds the thread will wake, see there is no event and sleep again.
Why not just block on the read?

Again, writing the example like this can make users think that it
is necessary to wait_edge_event() then read_edge_event().
If the wait_edge_event() doesn't add anything then drop it.

> > This example should be minimal and demonstrate how the code should
> > normally be used. e.g.
> >
> >         for (const auto& event: request.events_iter())
> >                   print_event(event);
> >
> > (assuming the addition of an iterator wrapping request and event buffer)
> >
> > If you want to showcase more complex examples then provide them separately.
> >
> > [snip]
> >
> 
> I'm not how that would work - would it create a new buffer every time
> we read events? Doesn't that defeat the entire purpose of the event
> buffer of preallocating memory for the events?
> 

The buffer is wrapped in the iterator, so a buffer would be created when
you create the iterator.  You only need to do that once and, as the
iterator only terminates when the file is closed, you keep going back
to the same iterator instance for the next event.

And this would not replace using the event buffer explicitly, just
provide the buffer in a more convenient form for simple use cases.
So the casual user doesn't need to know about buffers.

> > > +     /**
> > > +      * @brief Request a set of lines for exclusive usage.
> > > +      * @param req_cfg Request config object.
> > > +      * @param line_cfg Line config object.
> > > +      * @return New line_request object.
> > > +      */
> > > +     line_request request_lines(const request_config& req_cfg,
> > > +                                const line_config& line_cfg);
> > > +
> >
> > A common use case is to just want to request a line, so a top-level
> > version of this that hides the chip object from the user might be nice.
> > i.e.
> >
> >     line_request request_lines(const ::std::filesystem::path& path,
> >                                const request_config& req_cfg,
> >                                const line_config& line_cfg);
> >
> > Also applies to C API.
> 
> I replied about this under the C patch series.
> 

Indeed.

> >
> > > +private:
> > > +
> > > +     struct impl;
> > > +
> > > +     ::std::unique_ptr<impl> _m_priv;
> > > +};
> > > +
> > > +/**
> > > + * @brief Stream insertion operator for GPIO chip objects.
> > > + * @param out Output stream to write to.
> > > + * @param chip GPIO chip to insert into the output stream.
> > > + * @return Reference to out.
> > > + */
> > > +::std::ostream& operator<<(::std::ostream& out, const chip& chip);
> > > +
> > > +/**
> > > + * @}
> > > + */
> > > +
> > > +} /* namespace gpiod */
> > > +
> > > +#endif /* __LIBGPIOD_CXX_CHIP_HPP__ */
> >
> > [snip]
> >
> > > +++ b/bindings/cxx/gpiodcxx/line-request.hpp
> > > @@ -0,0 +1,221 @@
> > > +/* SPDX-License-Identifier: LGPL-3.0-or-later */
> > > +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
> > > +
> > > +/**
> > > + * @file line-request.hpp
> > > + */
> > > +
> > > +#ifndef __LIBGPIOD_CXX_LINE_REQUEST_HPP__
> > > +#define __LIBGPIOD_CXX_LINE_REQUEST_HPP__
> > > +
> > > +#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
> > > +#error "Only gpiod.hpp can be included directly."
> > > +#endif
> > > +
> > > +#include <chrono>
> > > +#include <cstddef>
> > > +#include <iostream>
> > > +#include <memory>
> > > +
> > > +#include "misc.hpp"
> > > +
> > > +namespace gpiod {
> > > +
> > > +class chip;
> > > +class edge_event;
> > > +class edge_event_buffer;
> > > +class line_config;
> > > +
> > > +/**
> > > + * @ingroup gpiod_cxx
> > > + * @{
> > > + */
> > > +
> > > +/**
> > > + * @brief Stores the context of a set of requested GPIO lines.
> > > + */
> > > +class line_request
> > > +{
> > > +public:
> > > +
> > > +     line_request(const line_request& other) = delete;
> > > +
> > > +     /**
> > > +      * @brief Move constructor.
> > > +      * @param other Object to move.
> > > +      */
> > > +     line_request(line_request&& other) noexcept;
> > > +
> > > +     ~line_request(void);
> > > +
> > > +     line_request& operator=(const line_request& other) = delete;
> > > +
> > > +     /**
> > > +      * @brief Move assignment operator.
> > > +      * @param other Object to move.
> > > +      */
> > > +     line_request& operator=(line_request&& other) noexcept;
> > > +
> > > +     /**
> > > +      * @brief Check if this object is valid.
> > > +      * @return True if this object's methods can be used, false otherwise.
> > > +      *         False usually means the request was released. If the user
> > > +      *         calls any of the methods of this class on an object for
> > > +      *         which this operator returned false, a logic_error will be
> > > +      *         thrown.
> > > +      */
> > > +     explicit operator bool(void) const noexcept;
> > > +
> > > +     /**
> > > +      * @brief Release the GPIO chip and free all associated resources.
> > > +      * @note The object can still be used after this method is called but
> > > +      *       using any of the mutators will result in throwing
> > > +      *       a logic_error exception.
> > > +      */
> > > +     void release(void);
> > > +
> > > +     /**
> > > +      * @brief Get the number of requested lines.
> > > +      * @return Number of lines in this request.
> > > +      */
> > > +     ::std::size_t num_lines(void) const;
> > > +
> > > +     /**
> > > +      * @brief Get the list of offsets of requested lines.
> > > +      * @return List of hardware offsets of the lines in this request.
> > > +      */
> > > +     line::offsets offsets(void) const;
> > > +
> > > +     /**
> > > +      * @brief Get the value of a single requested line.
> > > +      * @param offset Offset of the line to read within the chip.
> > > +      * @return Current line value.
> > > +      */
> > > +     line::value get_value(line::offset offset);
> > > +
> > > +     /**
> > > +      * @brief Get the values of a subset of requested lines.
> > > +      * @param offsets Vector of line offsets
> > > +      * @return Vector of lines values with indexes of values corresponding
> > > +      *         to those of the offsets.
> > > +      */
> > > +     line::values get_values(const line::offsets& offsets);
> > > +
> > > +     /**
> > > +      * @brief Get the values of all requested lines.
> > > +      * @return List of read values.
> > > +      */
> > > +     line::values get_values(void);
> > > +
> > > +     /**
> > > +      * @brief Get the values of a subset of requested lines into a vector
> > > +      *        supplied by the caller.
> > > +      * @param offsets Vector of line offsets.
> > > +      * @param values Vector for storing the values. Its size must be at
> > > +      *               least that of the offsets vector. The indexes of read
> > > +      *               values will correspond with those in the offsets
> > > +      *               vector.
> > > +      */
> > > +     void get_values(const line::offsets& offsets, line::values& values);
> > > +
> > > +     /**
> > > +      * @brief Get the values of all requested lines.
> > > +      * @param values Array in which the values will be stored. Must hold
> > > +      *               at least the number of elements returned by
> > > +      *               line_request::num_lines.
> > > +      */
> > > +     void get_values(line::values& values);
> > > +
> > > +     /**
> > > +      * @brief Set the value of a single requested line.
> > > +      * @param offset Offset of the line to set within the chip.
> > > +      * @param value New line value.
> > > +      */
> > > +     void set_value(line::offset offset, line::value value);
> > > +
> > > +     /**
> > > +      * @brief Set the values of a subset of requested lines.
> > > +      * @param values Vector containing a set of offset->value mappings.
> > > +      */
> > > +     void set_values(const line::value_mappings& values);
> > > +
> > > +     /**
> > > +      * @brief Set the values of a subset of requested lines.
> > > +      * @param offsets Vector containing the offsets of lines to set.
> > > +      * @param values Vector containing new values with indexes
> > > +      *               corresponding with those in the offsets vector.
> > > +      */
> > > +     void set_values(const line::offsets& offsets, const line::values& values);
> > > +
> > > +     /**
> > > +      * @brief Set the values of all requested lines.
> > > +      * @param values Array of new line values. The size must be equal to
> > > +      *               the value returned by line_request::num_lines.
> > > +      */
> > > +     void set_values(const line::values& values);
> > > +
> > > +     /**
> > > +      * @brief Apply new config options to requested lines.
> > > +      * @param config New configuration.
> > > +      */
> > > +     void reconfigure_lines(const line_config& config);
> > > +
> > > +     /**
> > > +      * @brief Get the file descriptor number associated with this line
> > > +      *        request.
> > > +      * @return File descriptor number.
> > > +      */
> > > +     int fd(void) const;
> > > +
> > > +     /**
> > > +      * @brief Wait for edge events on any of the lines requested with edge
> > > +      *        detection enabled.
> > > +      * @param timeout Wait time limit in nanoseconds.
> > > +      * @return True if at least one event is ready to be read. False if the
> > > +      *         wait timed out.
> > > +      */
> > > +     bool wait_edge_event(const ::std::chrono::nanoseconds& timeout) const;
> > > +
> > > +     /**
> > > +      * @brief Read a number of edge events from this request up to the
> > > +      *        maximum capacity of the buffer.
> > > +      * @param buffer Edge event buffer to read events into.
> > > +      * @return Number of events read.
> > > +      */
> > > +     ::std::size_t read_edge_event(edge_event_buffer& buffer);
> > > +
> > > +     /**
> > > +      * @brief Read a number of edge events from this request.
> > > +      * @param buffer Edge event buffer to read events into.
> > > +      * @param max_events Maximum number of events to read. Limited by the
> > > +      *                   capacity of the buffer.
> > > +      * @return Number of events read.
> > > +      */
> > > +     ::std::size_t read_edge_event(edge_event_buffer& buffer, ::std::size_t max_events);
> > > +
> > > +private:
> > > +
> > > +     line_request(void);
> > > +
> > > +     struct impl;
> > > +
> > > +     ::std::unique_ptr<impl> _m_priv;
> > > +
> > > +     friend chip;
> > > +};
> >
> > Just wanting to get events from a request is a common use case, and
> > having to use an edge_event_buffer is tedious (refer to the gpiomoncxx
> > example)
> > How about an iterator that wraps the request and an edge_event_buffer
> > and provides events?
> > I refer to this elsewhere as request.events_iter().
> >
> > > +
> > > +/**
> > > + * @brief Stream insertion operator for line requests.
> > > + * @param out Output stream to write to.
> > > + * @param request Line request object to insert into the output stream.
> > > + * @return Reference to out.
> > > + */
> > > +::std::ostream& operator<<(::std::ostream& out, const line_request& request);
> > > +
> > > +/**
> > > + * @}
> > > + */
> > > +
> > > +} /* namespace gpiod */
> > > +
> > > +#endif /* __LIBGPIOD_CXX_LINE_REQUEST_HPP__ */
> > > diff --git a/bindings/cxx/gpiodcxx/line.hpp b/bindings/cxx/gpiodcxx/line.hpp
> > > new file mode 100644
> > > index 0000000..8e8a984
> > > --- /dev/null
> > > +++ b/bindings/cxx/gpiodcxx/line.hpp
> > > @@ -0,0 +1,274 @@
> > > +/* SPDX-License-Identifier: LGPL-3.0-or-later */
> > > +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
> > > +
> > > +/**
> > > + * @file line.hpp
> > > + */
> > > +
> > > +#ifndef __LIBGPIOD_CXX_LINE_HPP__
> > > +#define __LIBGPIOD_CXX_LINE_HPP__
> > > +
> > > +#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
> > > +#error "Only gpiod.hpp can be included directly."
> > > +#endif
> > > +
> > > +#include <ostream>
> > > +#include <utility>
> > > +#include <vector>
> > > +
> > > +namespace gpiod {
> > > +
> > > +/**
> > > + * @brief Namespace containing various type definitions for GPIO lines.
> > > + */
> > > +namespace line {
> > > +
> > > +/**
> > > + * @ingroup gpiod_cxx
> > > + * @{
> > > + */
> > > +
> > > +/**
> > > + * @brief Wrapper around unsigned int for representing line offsets.
> > > + */
> > > +class offset
> > > +{
> > > +public:
> > > +     /**
> > > +      * @brief Constructor with implicit conversion from unsigned int.
> > > +      * @param off Line offset.
> > > +      */
> > > +     offset(unsigned int off = 0) : _m_offset(off) { }
> > > +
> > > +     /**
> > > +      * @brief Copy constructor.
> > > +      * @param other Object to copy.
> > > +      */
> > > +     offset(const offset& other) = default;
> > > +
> > > +     /**
> > > +      * @brief Move constructor.
> > > +      * @param other Object to move.
> > > +      */
> > > +     offset(offset&& other) = default;
> > > +
> > > +     ~offset(void) = default;
> > > +
> > > +     /**
> > > +      * @brief Assignment operator.
> > > +      * @param other Object to copy.
> > > +      * @return Reference to self.
> > > +      */
> > > +     offset& operator=(const offset& other) = default;
> > > +
> > > +     /**
> > > +      * @brief Move assignment operator.
> > > +      * @param other Object to move.
> > > +      * @return Reference to self.
> > > +      */
> > > +     offset& operator=(offset&& other) noexcept = default;
> > > +
> > > +     /**
> > > +      * @brief Conversion operator to `unsigned int`.
> > > +      */
> > > +     operator unsigned int(void) const noexcept
> > > +     {
> > > +             return this->_m_offset;
> > > +     }
> > > +
> > > +private:
> > > +     unsigned int _m_offset;
> > > +};
> > > +
> >
> > Wrapping unsigned int in a class seems like overkill.
> > Is this just to get the streaming operators for offsets to work?
> >
> 
> Yes. Otherwise we'd be providing a generic stream operator for all
> vector specializations. And this wrapping doesn't really do much other
> than providing a new type within the gpiod namespace. The constructor
> with implicit conversion and the operator unsigned int() make sure of
> this.
> 

Indeed.  Can't say I'm happy with the class solution, but I tried
replacing the class with an alias and rapidly got bogged down trying to
satisfy all the streaming operators, so I don't have a better
alternative to offer :-(.

> > [snip]
> >
> > > +GPIOD_CXX_API line::offsets line_request::offsets(void) const
> > > +{
> > > +     this->_m_priv->throw_if_released();
> >
> > redundant as this->num_lines() also does it.
> >
> > > +
> > > +     ::std::vector<unsigned int> buf(this->num_lines());
> > > +     line::offsets offsets(this->num_lines());
> > > +
> > > +     ::gpiod_line_request_get_offsets(this->_m_priv->request.get(), buf.data());
> > > +
> > > +     for (unsigned int i = 0; i < this->num_lines(); i++)
> > > +             offsets[i] = buf[i];
> > > +
> >
> > Cache num_lines locally rather than calling num_lines() several times.
> >
> >
> > [snip]
> >
> > > +template<typename T>
> > > +::std::ostream& insert_vector(::std::ostream& out,
> > > +                           const ::std::string& name, const ::std::vector<T>& vec)
> > >  {
> > > -     this->_m_line = nullptr;
> > > -     this->_m_owner.reset();
> > > -}
> > > +     out << name << "([";
> > > +     ::std::copy(vec.begin(), ::std::prev(vec.end()),
> > > +                 ::std::ostream_iterator<T>(out, ", "));
> > > +     out << vec.back();
> > > +     out << "])";
> > >
> >
> > What formatting are you after for vectors?
> > Is the double bracketing necessary?
> >
> > [snip]
> >
> > > +TEST_CASE("line_request stream insertion operator works", "[line-request]")
> > > +{
> > > +     ::gpiosim::chip sim({{ simprop::NUM_LINES, 4 }});
> > > +     ::gpiod::chip chip(sim.dev_path());
> > > +
> > > +     auto request = chip.request_lines(
> > > +             ::gpiod::request_config({
> > > +                     { reqprop::OFFSETS, offsets({ 3, 1, 0, 2 }) }
> > > +             }),
> > > +             ::gpiod::line_config()
> > > +     );
> > > +
> > > +     ::std::stringstream buf, expected;
> > > +
> > > +     expected << "line_request(num_lines=4, line_offsets=[offsets([3, 1, 0, 2])], fd=" <<
> > > +                 request.fd() << ")";
> > > +
> >
> > Still not sure what formatting you are going for with the vectors.
> > And the line_offsets=[] is not consistent with the offsets=() used in
> > request_config.
> >
> 
> Yep need to make that consistent, thanks.
> 
> > [snip]
> >
> > > +TEST_CASE("request_config stream insertion operator works", "[request-config]")
> > > +{
> > > +     ::gpiod::request_config cfg({
> > > +             { property::CONSUMER, "foobar" },
> > > +             { property::OFFSETS, offsets({ 0, 1, 2, 3 })},
> > > +             { property::EVENT_BUFFER_SIZE, 32 }
> > > +     });
> > > +
> > > +     ::std::stringstream buf;
> > > +
> > > +     buf << cfg;
> > > +
> > > +     ::std::string expected("request_config(consumer='foobar', num_offsets=4, "
> > > +                            "offsets=(offsets([0, 1, 2, 3])), event_buffer_size=32)");
> > > +
> >
> > The streaming output stutters.
> > "offsets=(offsets([0, 1, 2, 3]))" should be "offsets=([0, 1, 2, 3])"?
> > And even then one of the sets of brackets is redundant.
> >
> 
> Actually maybe it should be offsets=gpiod::offsets(0, 1, 2 ,3)? I
> would like to indicate the type and not make it a generic streaming
> operator for vectors like what Qt provides.
> 

Ok, I tend to go generic.
What is the benefit of indicating the type?

Cheers,
Kent.

> > > +     REQUIRE_THAT(buf.str(), Catch::Equals(expected));
> > > +}
> > > +
> > > +} /* namespace */
> > > diff --git a/configure.ac b/configure.ac
> > > index f8d34ed..ab03673 100644
> > > --- a/configure.ac
> > > +++ b/configure.ac
> > > @@ -239,6 +239,7 @@ AC_CONFIG_FILES([Makefile
> > >                bindings/cxx/libgpiodcxx.pc
> > >                bindings/Makefile
> > >                bindings/cxx/Makefile
> > > +              bindings/cxx/gpiodcxx/Makefile
> > >                bindings/cxx/examples/Makefile
> > >                bindings/cxx/tests/Makefile
> > >                bindings/python/Makefile
> > > --
> > > 2.30.1
> > >
> >
> > Phew.  So nothing major.
> >
> > Cheers,
> > Kent.
> 
> Thanks Kent! Wherever there's no comment, I will address the issue.
> 
> Bart


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [libgpiod v2][PATCH v5] bindings: cxx: implement C++ bindings for libgpiod v2.0
  2022-03-27 12:21 ` Kent Gibson
  2022-04-02 15:13   ` Bartosz Golaszewski
@ 2022-04-25 14:48   ` Bartosz Golaszewski
  2022-04-26  4:24     ` Kent Gibson
  1 sibling, 1 reply; 6+ messages in thread
From: Bartosz Golaszewski @ 2022-04-25 14:48 UTC (permalink / raw)
  To: Kent Gibson; +Cc: Linus Walleij, Andy Shevchenko, open list:GPIO SUBSYSTEM

On Sun, Mar 27, 2022 at 2:22 PM Kent Gibson <warthog618@gmail.com> wrote:
>

[snip]

>
> > +     ::gpiod::edge_event_buffer buffer;
> >
> >       for (;;) {
> > -             auto events = lines.event_wait(::std::chrono::seconds(1));
> > -             if (events) {
> > -                     for (auto& it: events)
> > -                             print_event(it.event_read());
> > +             if (request.wait_edge_event(::std::chrono::seconds(5))) {
> > +                     request.read_edge_event(buffer);
> > +
> > +                     for (const auto& event: buffer)
> > +                             print_event(event);
> >               }
> >       }
> >
>
> What is the purpose of the wait_edge_event() here?
> Wouldn't read_edge_event() block until the next event?
>
> This example should be minimal and demonstrate how the code should
> normally be used. e.g.
>
>         for (const auto& event: request.events_iter())
>                   print_event(event);
>

We're making the request's file descriptor non-blocking in the C
library. Do you think we should keep it in blocking mode?

I'm no longer sure why I did that honestly.

Maybe a request config flag for that?

Bart

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [libgpiod v2][PATCH v5] bindings: cxx: implement C++ bindings for libgpiod v2.0
  2022-04-25 14:48   ` Bartosz Golaszewski
@ 2022-04-26  4:24     ` Kent Gibson
  0 siblings, 0 replies; 6+ messages in thread
From: Kent Gibson @ 2022-04-26  4:24 UTC (permalink / raw)
  To: Bartosz Golaszewski
  Cc: Linus Walleij, Andy Shevchenko, open list:GPIO SUBSYSTEM

On Mon, Apr 25, 2022 at 04:48:40PM +0200, Bartosz Golaszewski wrote:
> On Sun, Mar 27, 2022 at 2:22 PM Kent Gibson <warthog618@gmail.com> wrote:
> >
> 
> [snip]
> 
> >
> > > +     ::gpiod::edge_event_buffer buffer;
> > >
> > >       for (;;) {
> > > -             auto events = lines.event_wait(::std::chrono::seconds(1));
> > > -             if (events) {
> > > -                     for (auto& it: events)
> > > -                             print_event(it.event_read());
> > > +             if (request.wait_edge_event(::std::chrono::seconds(5))) {
> > > +                     request.read_edge_event(buffer);
> > > +
> > > +                     for (const auto& event: buffer)
> > > +                             print_event(event);
> > >               }
> > >       }
> > >
> >
> > What is the purpose of the wait_edge_event() here?
> > Wouldn't read_edge_event() block until the next event?
> >
> > This example should be minimal and demonstrate how the code should
> > normally be used. e.g.
> >
> >         for (const auto& event: request.events_iter())
> >                   print_event(event);
> >
> 
> We're making the request's file descriptor non-blocking in the C
> library. Do you think we should keep it in blocking mode?
> 

Ok, didn't realise that.

The function documentation for gpiod_line_request_read_edge_event()
says:

@note This function will block if no event was queued for the line request.

and I was assuming everything was built on that - so blocking.

But in gpiod_chip_request_lines() you do fcntl() the request fd to
non-blocking. So that documentation is wrong - or you should not be
setting the NONBLOCK.

If there are no events available, gpiod_line_request_read_edge_event()
will in fact return -1 (EIO), as returned by
gpiod_edge_event_buffer_read_fd().
If you want to go non-blocking, that should return 0?

Also, the line_request::read_edge_event() methods don't specify if they
block or return 0 if no events are available.  Whichever way you go,
document it.

> I'm no longer sure why I did that honestly.
> 

Hmmm, the only reason I can see is so gpiod_edge_event_buffer_read_fd()
can read up to max_events events in one read and not block if there were
no events available?
It could poll() first to check if there are events available - but it
doesn't.  Keeping syscalls to a minimum?

> Maybe a request config flag for that?
> 

I'd rather not - then you need to explain that the functions mentioned
earlier may block or return 0, depending.
I would rather make the wait/read the standard approach, with the read
blocking if no events are available.  That is fine for the vast majority
of cases.

Having said that, given you expose the fd, the user can always fcntl()
it themselves - in which case libgpiod should not assume that
gpiod_line_request_read_edge_event() will block - it may return -1 (EIO)
instead, as that is how gpiod_edge_event_buffer_read_fd() currently
behaves. So libgpiod should handle both cases, even if not explicitly
supporting a non-blocking read.

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2022-04-26  4:24 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-03-23 14:22 [libgpiod v2][PATCH v5] bindings: cxx: implement C++ bindings for libgpiod v2.0 Bartosz Golaszewski
2022-03-27 12:21 ` Kent Gibson
2022-04-02 15:13   ` Bartosz Golaszewski
2022-04-04  9:54     ` Kent Gibson
2022-04-25 14:48   ` Bartosz Golaszewski
2022-04-26  4:24     ` Kent Gibson

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.