diff mbox series

[libgpiod,v2,v6,5/5] bindings: cxx: add implementation

Message ID 20220426125023.2664623-6-brgl@bgdev.pl
State New
Headers show
Series bindings: cxx: implement C++ bindings for libgpiod v2.0 | expand

Commit Message

Bartosz Golaszewski April 26, 2022, 12:50 p.m. UTC
This contains the actual implementation of the v2 C++ bindings.

Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
---
 bindings/cxx/Makefile.am           |  45 ++
 bindings/cxx/chip-info.cpp         |  74 ++++
 bindings/cxx/chip.cpp              | 218 +++++++++
 bindings/cxx/edge-event-buffer.cpp | 115 +++++
 bindings/cxx/edge-event.cpp        | 135 ++++++
 bindings/cxx/exception.cpp         | 119 +++++
 bindings/cxx/gpiod.hpp             |  46 ++
 bindings/cxx/info-event.cpp        | 102 +++++
 bindings/cxx/internal.cpp          |  28 ++
 bindings/cxx/internal.hpp          | 207 +++++++++
 bindings/cxx/libgpiodcxx.pc.in     |  14 +
 bindings/cxx/line-config.cpp       | 685 +++++++++++++++++++++++++++++
 bindings/cxx/line-info.cpp         | 189 ++++++++
 bindings/cxx/line-request.cpp      | 225 ++++++++++
 bindings/cxx/line.cpp              | 128 ++++++
 bindings/cxx/misc.cpp              |  20 +
 bindings/cxx/request-config.cpp    | 174 ++++++++
 17 files changed, 2524 insertions(+)
 create mode 100644 bindings/cxx/Makefile.am
 create mode 100644 bindings/cxx/chip-info.cpp
 create mode 100644 bindings/cxx/chip.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/gpiod.hpp
 create mode 100644 bindings/cxx/info-event.cpp
 create mode 100644 bindings/cxx/internal.cpp
 create mode 100644 bindings/cxx/internal.hpp
 create mode 100644 bindings/cxx/libgpiodcxx.pc.in
 create mode 100644 bindings/cxx/line-config.cpp
 create mode 100644 bindings/cxx/line-info.cpp
 create mode 100644 bindings/cxx/line-request.cpp
 create mode 100644 bindings/cxx/line.cpp
 create mode 100644 bindings/cxx/misc.cpp
 create mode 100644 bindings/cxx/request-config.cpp

Comments

Kent Gibson April 27, 2022, 6:01 a.m. UTC | #1
On Tue, Apr 26, 2022 at 02:50:23PM +0200, Bartosz Golaszewski wrote:
> This contains the actual implementation of the v2 C++ bindings.
> 
> Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>

[snip]

> +
> +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;
> +}
> +

My previous comment was "Cache num_lines locally rather than calling
num_lines() several times."

You cached it in the wrong place - it should be first thing in the
function, so you only call it once, not three times.

And the throw_if_released() is still "redundant as this->num_lines()
also does it", and it is the first thing called here - after the
throw_if_released().

And I would've made this patch 3/5, not 5/5.

But I'm fine with this set going in either way - in fact give its size
I'd rather see minor tweaks applied later than go through another round
of review.

For the series:

Reviewed-by: Kent Gibson <warthog618@gmail.com>

I really would also like to see some feedback from C++ developers that
will actually be using it, as they have a bigger stake in it than I do.
But that is up to them.

Cheers,
Kent.
Bartosz Golaszewski May 2, 2022, 12:34 p.m. UTC | #2
On Wed, Apr 27, 2022 at 8:02 AM Kent Gibson <warthog618@gmail.com> wrote:
>
> On Tue, Apr 26, 2022 at 02:50:23PM +0200, Bartosz Golaszewski wrote:
> > This contains the actual implementation of the v2 C++ bindings.
> >
> > Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
>
> [snip]
>
> > +
> > +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;
> > +}
> > +
>
> My previous comment was "Cache num_lines locally rather than calling
> num_lines() several times."
>
> You cached it in the wrong place - it should be first thing in the
> function, so you only call it once, not three times.
>

I fixed it when applying.

> And the throw_if_released() is still "redundant as this->num_lines()
> also does it", and it is the first thing called here - after the
> throw_if_released().
>

I think it's still worth having it here because at least the code
makes it clear, we need to have a valid request here. It's not like
it's a hot path where this additional function call would matter IMO.

> And I would've made this patch 3/5, not 5/5.
>
> But I'm fine with this set going in either way - in fact give its size
> I'd rather see minor tweaks applied later than go through another round
> of review.
>
> For the series:
>
> Reviewed-by: Kent Gibson <warthog618@gmail.com>

Thanks I applied it for now as it is, let's get it into master with
the Python bindings and then polish it some more there.

>
> I really would also like to see some feedback from C++ developers that
> will actually be using it, as they have a bigger stake in it than I do.
> But that is up to them.
>

Indeed, any idea where to post that to get some free code reviews from
C++ devs? :)

Bart
Kent Gibson May 2, 2022, 1:54 p.m. UTC | #3
On Mon, May 02, 2022 at 02:34:34PM +0200, Bartosz Golaszewski wrote:
> On Wed, Apr 27, 2022 at 8:02 AM Kent Gibson <warthog618@gmail.com> wrote:
> >
> > On Tue, Apr 26, 2022 at 02:50:23PM +0200, Bartosz Golaszewski wrote:
> > > This contains the actual implementation of the v2 C++ bindings.
> > >
> > > Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
> >
> > [snip]
> >
> > > +
> > > +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;
> > > +}
> > > +
> >
> > My previous comment was "Cache num_lines locally rather than calling
> > num_lines() several times."
> >
> > You cached it in the wrong place - it should be first thing in the
> > function, so you only call it once, not three times.
> >
> 
> I fixed it when applying.
> 
> > And the throw_if_released() is still "redundant as this->num_lines()
> > also does it", and it is the first thing called here - after the
> > throw_if_released().
> >
> 
> I think it's still worth having it here because at least the code
> makes it clear, we need to have a valid request here. It's not like
> it's a hot path where this additional function call would matter IMO.
> 

So add a comment?
Pointless work is pointless work, hot path or not.

> > And I would've made this patch 3/5, not 5/5.
> >
> > But I'm fine with this set going in either way - in fact give its size
> > I'd rather see minor tweaks applied later than go through another round
> > of review.
> >
> > For the series:
> >
> > Reviewed-by: Kent Gibson <warthog618@gmail.com>
> 
> Thanks I applied it for now as it is, let's get it into master with
> the Python bindings and then polish it some more there.
> 
> >
> > I really would also like to see some feedback from C++ developers that
> > will actually be using it, as they have a bigger stake in it than I do.
> > But that is up to them.
> >
> 
> Indeed, any idea where to post that to get some free code reviews from
> C++ devs? :)
> 

That does raise the issue of whether libgpiod should have a forum other
than this mailing list.

I was hoping to at least nudge the others on the CC list ¯\_(ツ)_/¯.
If no one is sufficiently interested to bother reviewing, or even acking,
the C++ bindings, perhaps deprecate them instead ;-)?

Cheers,
Kent.
Bartosz Golaszewski May 2, 2022, 5:41 p.m. UTC | #4
On Mon, May 2, 2022 at 3:54 PM Kent Gibson <warthog618@gmail.com> wrote:
>
> On Mon, May 02, 2022 at 02:34:34PM +0200, Bartosz Golaszewski wrote:
> > On Wed, Apr 27, 2022 at 8:02 AM Kent Gibson <warthog618@gmail.com> wrote:
> > >
> > > On Tue, Apr 26, 2022 at 02:50:23PM +0200, Bartosz Golaszewski wrote:
> > > > This contains the actual implementation of the v2 C++ bindings.
> > > >
> > > > Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
> > >
> > > [snip]
> > >
> > > > +
> > > > +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;
> > > > +}
> > > > +
> > >
> > > My previous comment was "Cache num_lines locally rather than calling
> > > num_lines() several times."
> > >
> > > You cached it in the wrong place - it should be first thing in the
> > > function, so you only call it once, not three times.
> > >
> >
> > I fixed it when applying.
> >
> > > And the throw_if_released() is still "redundant as this->num_lines()
> > > also does it", and it is the first thing called here - after the
> > > throw_if_released().
> > >
> >
> > I think it's still worth having it here because at least the code
> > makes it clear, we need to have a valid request here. It's not like
> > it's a hot path where this additional function call would matter IMO.
> >
>
> So add a comment?
> Pointless work is pointless work, hot path or not.
>
> > > And I would've made this patch 3/5, not 5/5.
> > >
> > > But I'm fine with this set going in either way - in fact give its size
> > > I'd rather see minor tweaks applied later than go through another round
> > > of review.
> > >
> > > For the series:
> > >
> > > Reviewed-by: Kent Gibson <warthog618@gmail.com>
> >
> > Thanks I applied it for now as it is, let's get it into master with
> > the Python bindings and then polish it some more there.
> >
> > >
> > > I really would also like to see some feedback from C++ developers that
> > > will actually be using it, as they have a bigger stake in it than I do.
> > > But that is up to them.
> > >
> >
> > Indeed, any idea where to post that to get some free code reviews from
> > C++ devs? :)
> >
>
> That does raise the issue of whether libgpiod should have a forum other
> than this mailing list.
>
> I was hoping to at least nudge the others on the CC list ¯\_(ツ)_/¯.
> If no one is sufficiently interested to bother reviewing, or even acking,
> the C++ bindings, perhaps deprecate them instead ;-)?
>

Well, I've received enough emails over the years with questions about
C++ bindings to assume they are being used (although Python bindings
seem to be preferred indeed). I think it's just a significant amount
of work to go through 10000 LOC just for fun, hence the lack of
reviews. :)

Anyway, I'm pretty happy with how they turned out and I intend to keep
them. Hopefully nothing is too broken to require an incompatible fix.
I'll try to find some reviewers outside the list in the meantime.

Bart
Kent Gibson May 3, 2022, 8:04 a.m. UTC | #5
On Mon, May 02, 2022 at 07:41:56PM +0200, Bartosz Golaszewski wrote:
> On Mon, May 2, 2022 at 3:54 PM Kent Gibson <warthog618@gmail.com> wrote:
> >
> > On Mon, May 02, 2022 at 02:34:34PM +0200, Bartosz Golaszewski wrote:
> > > On Wed, Apr 27, 2022 at 8:02 AM Kent Gibson <warthog618@gmail.com> wrote:
> > > >
> > > > On Tue, Apr 26, 2022 at 02:50:23PM +0200, Bartosz Golaszewski wrote:
> > > > > This contains the actual implementation of the v2 C++ bindings.
> > > > >
> > > > > Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
> > > >
> > > > [snip]
> > > >
> > > > > +
> > > > > +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;
> > > > > +}
> > > > > +
> > > >
> > > > My previous comment was "Cache num_lines locally rather than calling
> > > > num_lines() several times."
> > > >
> > > > You cached it in the wrong place - it should be first thing in the
> > > > function, so you only call it once, not three times.
> > > >
> > >
> > > I fixed it when applying.
> > >
> > > > And the throw_if_released() is still "redundant as this->num_lines()
> > > > also does it", and it is the first thing called here - after the
> > > > throw_if_released().
> > > >
> > >
> > > I think it's still worth having it here because at least the code
> > > makes it clear, we need to have a valid request here. It's not like
> > > it's a hot path where this additional function call would matter IMO.
> > >
> >
> > So add a comment?
> > Pointless work is pointless work, hot path or not.
> >
> > > > And I would've made this patch 3/5, not 5/5.
> > > >
> > > > But I'm fine with this set going in either way - in fact give its size
> > > > I'd rather see minor tweaks applied later than go through another round
> > > > of review.
> > > >
> > > > For the series:
> > > >
> > > > Reviewed-by: Kent Gibson <warthog618@gmail.com>
> > >
> > > Thanks I applied it for now as it is, let's get it into master with
> > > the Python bindings and then polish it some more there.
> > >
> > > >
> > > > I really would also like to see some feedback from C++ developers that
> > > > will actually be using it, as they have a bigger stake in it than I do.
> > > > But that is up to them.
> > > >
> > >
> > > Indeed, any idea where to post that to get some free code reviews from
> > > C++ devs? :)
> > >
> >
> > That does raise the issue of whether libgpiod should have a forum other
> > than this mailing list.
> >
> > I was hoping to at least nudge the others on the CC list ¯\_(ツ)_/¯.
> > If no one is sufficiently interested to bother reviewing, or even acking,
> > the C++ bindings, perhaps deprecate them instead ;-)?
> >
> 
> Well, I've received enough emails over the years with questions about
> C++ bindings to assume they are being used (although Python bindings
> seem to be preferred indeed). I think it's just a significant amount
> of work to go through 10000 LOC just for fun, hence the lack of
> reviews. :)

That was why I asked to have the original patch split.
An end user/lite review could just cover the headers and examples, and
optionally the tools as additional examples, and say "yeah that makes
sense", or "why the hell did you do it that way?" and not have to concern
themselves with the impl or tests.

> 
> Anyway, I'm pretty happy with how they turned out and I intend to keep
> them. Hopefully nothing is too broken to require an incompatible fix.
> I'll try to find some reviewers outside the list in the meantime.
> 

Btw, by "deprecate" I didn't mean throw them away, just delay merging
them into master until you get some user feedback.
And even then it was in jest.  Mostly.

Cheers,
Kent.
diff mbox series

Patch

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 */