diff --git a/bindings/cxx/Makefile.am b/bindings/cxx/Makefile.am
new file mode 100644
index 0000000..29d4f2d
--- /dev/null
+++ b/bindings/cxx/Makefile.am
@@ -0,0 +1,45 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+lib_LTLIBRARIES = libgpiodcxx.la
+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 = gpiodcxx .
+
+if WITH_TESTS
+
+SUBDIRS += tests
+
+endif
+
+if WITH_EXAMPLES
+
+SUBDIRS += examples
+
+endif
diff --git a/bindings/cxx/chip-info.cpp b/bindings/cxx/chip-info.cpp
new file mode 100644
index 0000000..b6d5cb7
--- /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 << "gpiod::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
new file mode 100644
index 0000000..4b5e3d7
--- /dev/null
+++ b/bindings/cxx/chip.cpp
@@ -0,0 +1,218 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+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)
+{
+	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 */
+
+struct chip::impl
+{
+	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");
+	}
+
+	chip_ptr chip;
+};
+
+GPIOD_CXX_API chip::chip(const ::std::filesystem::path& path)
+	: _m_priv(new impl(path))
+{
+
+}
+
+GPIOD_CXX_API chip::chip(chip&& other) noexcept
+	: _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API chip::~chip(void)
+{
+
+}
+
+GPIOD_CXX_API chip& chip::operator=(chip&& other) noexcept
+{
+	this->_m_priv = ::std::move(other._m_priv);
+
+	return *this;
+}
+
+GPIOD_CXX_API chip::operator bool(void) const noexcept
+{
+	return this->_m_priv->chip.get() != nullptr;
+}
+
+GPIOD_CXX_API void chip::close(void)
+{
+	this->_m_priv->throw_if_closed();
+
+	this->_m_priv->chip.reset();
+}
+
+GPIOD_CXX_API ::std::filesystem::path chip::path(void) const
+{
+	this->_m_priv->throw_if_closed();
+
+	return ::gpiod_chip_get_path(this->_m_priv->chip.get());
+}
+
+GPIOD_CXX_API chip_info chip::get_info(void) const
+{
+	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");
+
+	chip_info ret;
+
+	ret._m_priv->set_info_ptr(info);
+
+	return ret;
+}
+
+GPIOD_CXX_API line_info chip::get_line_info(line::offset offset) const
+{
+	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");
+
+	line_info ret;
+
+	ret._m_priv->set_info_ptr(info);
+
+	return ret;
+}
+
+GPIOD_CXX_API line_info chip::watch_line_info(line::offset offset) const
+{
+	this->_m_priv->throw_if_closed();
+
+	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");
+
+	line_info ret;
+
+	ret._m_priv->set_info_ptr(info);
+
+	return ret;
+}
+
+GPIOD_CXX_API void chip::unwatch_line_info(line::offset offset) const
+{
+	this->_m_priv->throw_if_closed();
+
+	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 int chip::fd(void) const
+{
+	this->_m_priv->throw_if_closed();
+
+	return ::gpiod_chip_get_fd(this->_m_priv->chip.get());
+}
+
+GPIOD_CXX_API bool chip::wait_info_event(const ::std::chrono::nanoseconds& timeout) const
+{
+	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 info_event chip::read_info_event(void) const
+{
+	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 int chip::get_line_offset_from_name(const ::std::string& name) const
+{
+	this->_m_priv->throw_if_closed();
+
+	int ret = ::gpiod_chip_get_line_offset_from_name(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 line_request chip::request_lines(const request_config& req_cfg,
+					       const line_config& line_cfg)
+{
+	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 ::std::ostream& operator<<(::std::ostream& out, const chip& chip)
+{
+	if (!chip)
+		out << "gpiod::chip(closed)";
+	else
+		out << "gpiod::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..5519c33
--- /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 << "gpiod::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..5797e88
--- /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 << "gpiod::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/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
new file mode 100644
index 0000000..dc7e938
--- /dev/null
+++ b/bindings/cxx/gpiod.hpp
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file gpiod.hpp
+ */
+
+#ifndef __LIBGPIOD_GPIOD_CXX_HPP__
+#define __LIBGPIOD_GPIOD_CXX_HPP__
+
+/**
+ * @defgroup gpiod_cxx C++ bindings
+ *
+ * C++ bindings for libgpiod.
+ */
+
+/**
+ * @cond
+ */
+
+/*
+ * 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.
+ */
+#define GPIOD_CXX_API __attribute__((visibility("default")))
+
+#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
+ */
+
+#endif /* __LIBGPIOD_GPIOD_CXX_HPP__ */
diff --git a/bindings/cxx/info-event.cpp b/bindings/cxx/info-event.cpp
new file mode 100644
index 0000000..a98c261
--- /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 << "gpiod::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
new file mode 100644
index 0000000..c4b5d34
--- /dev/null
+++ b/bindings/cxx/internal.hpp
@@ -0,0 +1,207 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __LIBGPIOD_CXX_INTERNAL_HPP__
+#define __LIBGPIOD_CXX_INTERNAL_HPP__
+
+#include <gpiod.h>
+#include <iostream>
+#include <iterator>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#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/libgpiodcxx.pc.in b/bindings/cxx/libgpiodcxx.pc.in
new file mode 100644
index 0000000..731227c
--- /dev/null
+++ b/bindings/cxx/libgpiodcxx.pc.in
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: libgpiodcxx
+Description: C++ bindings for libgpiod
+URL: @PACKAGE_URL@
+Version: @PACKAGE_VERSION@
+Libs: -L${libdir} -lgpiodcxx
+Cflags: -I${includedir}
diff --git a/bindings/cxx/line-config.cpp b/bindings/cxx/line-config.cpp
new file mode 100644
index 0000000..d6469cb
--- /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 << "gpiod::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..a5109b2
--- /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 << "gpiod::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..e573d23
--- /dev/null
+++ b/bindings/cxx/line-request.cpp
@@ -0,0 +1,225 @@
+// 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());
+
+	auto num_lines = this->num_lines();
+	for (unsigned int i = 0; i < 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 << "gpiod::line_request(released)";
+	else
+		out << "gpiod::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
new file mode 100644
index 0000000..fc20c62
--- /dev/null
+++ b/bindings/cxx/line.cpp
@@ -0,0 +1,128 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.hpp"
+
+namespace gpiod {
+namespace line {
+
+namespace {
+
+const ::std::map<line::value, ::std::string> value_names = {
+	{ line::value::INACTIVE,	"INACTIVE" },
+	{ line::value::ACTIVE,		"ACTIVE" }
+};
+
+const ::std::map<line::direction, ::std::string> direction_names = {
+	{ line::direction::AS_IS,	"AS_IS" },
+	{ line::direction::INPUT,	"INPUT" },
+	{ line::direction::OUTPUT,	"OUTPUT" }
+};
+
+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" }
+};
+
+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" }
+};
+
+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" }
+};
+
+const ::std::map<line::clock, ::std::string> clock_names = {
+	{ line::clock::MONOTONIC,	"MONOTONIC" },
+	{ line::clock::REALTIME,	"REALTIME" }
+};
+
+} /* namespace */
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::value val)
+{
+	out << value_names.at(val);
+
+	return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::direction dir)
+{
+	out << direction_names.at(dir);
+
+	return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::edge edge)
+{
+	out << edge_names.at(edge);
+
+	return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::bias bias)
+{
+	out << bias_names.at(bias);
+
+	return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::drive drive)
+{
+	out << drive_names.at(drive);
+
+	return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::clock clock)
+{
+	out << clock_names.at(clock);
+
+	return out;
+}
+
+template<typename T>
+::std::ostream& insert_vector(::std::ostream& out,
+			      const ::std::string& name, const ::std::vector<T>& vec)
+{
+	out << name << "(";
+	::std::copy(vec.begin(), ::std::prev(vec.end()),
+		    ::std::ostream_iterator<T>(out, ", "));
+	out << vec.back();
+	out << ")";
+
+	return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const offsets& offs)
+{
+	return insert_vector(out, "gpiod::offsets", offs);
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line::values& vals)
+{
+	return insert_vector(out, "gpiod::values", vals);
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line::value_mapping& mapping)
+{
+	out << "gpiod::value_mapping(" << mapping.first << ": " << mapping.second << ")";
+
+	return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line::value_mappings& mappings)
+{
+	return insert_vector(out, "gpiod::value_mappings", mappings);
+}
+
+} /* namespace line */
+} /* 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..9f77533
--- /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 << "gpiod::request_config(consumer=" << consumer <<
+	       ", num_offsets=" << config.num_offsets() <<
+	       ", offsets=(" << config.offsets() << ")" <<
+	       ", event_buffer_size=" << config.event_buffer_size() <<
+	       ")";
+
+	return out;
+}
+
+} /* namespace gpiod */
