Project import generated by Copybara.

GitOrigin-RevId: 736879d3e4e7f92b1490308628540ec547cda7d0
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..dc96031
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,75 @@
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE:= libgpiod
+LOCAL_SRC_FILES := \
+		lib/core.c \
+		lib/ctxless.c \
+		lib/helpers.c \
+		lib/iter.c \
+		lib/misc.c
+LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
+LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
+LOCAL_MODULE_TAGS := optional
+LOCAL_CFLAGS := -Wall -Wno-missing-field-initializers
+include $(BUILD_SHARED_LIBRARY)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE:= gpiodetect
+LOCAL_SRC_FILES := \
+		tools/$(LOCAL_MODULE).c \
+		tools/tools-common.c
+LOCAL_SHARED_LIBRARIES := libgpiod
+LOCAL_MODULE_TAGS := optional
+LOCAL_CFLAGS := -Wall -Wno-missing-field-initializers
+include $(BUILD_EXECUTABLE)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE:= gpiofind
+LOCAL_SRC_FILES := \
+		tools/$(LOCAL_MODULE).c \
+		tools/tools-common.c
+LOCAL_SHARED_LIBRARIES := libgpiod
+LOCAL_MODULE_TAGS := optional
+LOCAL_CFLAGS := -Wall -Wno-missing-field-initializers
+include $(BUILD_EXECUTABLE)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE:= gpioget
+LOCAL_SRC_FILES := \
+		tools/$(LOCAL_MODULE).c \
+		tools/tools-common.c
+LOCAL_SHARED_LIBRARIES := libgpiod
+LOCAL_MODULE_TAGS := optional
+LOCAL_CFLAGS := -Wall -Wno-missing-field-initializers
+include $(BUILD_EXECUTABLE)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE:= gpioset
+LOCAL_SRC_FILES := \
+		tools/$(LOCAL_MODULE).c \
+		tools/tools-common.c
+LOCAL_SHARED_LIBRARIES := libgpiod
+LOCAL_MODULE_TAGS := optional
+LOCAL_CFLAGS := -Wall -Wno-missing-field-initializers
+include $(BUILD_EXECUTABLE)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE:= gpioinfo
+LOCAL_SRC_FILES := \
+		tools/$(LOCAL_MODULE).c \
+		tools/tools-common.c
+LOCAL_SHARED_LIBRARIES := libgpiod
+LOCAL_MODULE_TAGS := optional
+LOCAL_CFLAGS := -Wall -Wno-missing-field-initializers
+include $(BUILD_EXECUTABLE)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE:= gpiomon
+LOCAL_SRC_FILES := \
+		tools/$(LOCAL_MODULE).c \
+		tools/tools-common.c
+LOCAL_SHARED_LIBRARIES := libgpiod
+LOCAL_MODULE_TAGS := optional
+LOCAL_CFLAGS := -Wall -Wno-missing-field-initializers
+include $(BUILD_EXECUTABLE)
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..5522aa5
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,508 @@
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+	51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations
+below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it
+becomes a de-facto standard.  To achieve this, non-free programs must
+be allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control
+compilation and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at least
+    three years, to give the same user the materials specified in
+    Subsection 6a, above, for a charge no more than the cost of
+    performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply, and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License
+may add an explicit geographical distribution limitation excluding those
+countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms
+of the ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.
+It is safest to attach them to the start of each source file to most
+effectively convey the exclusion of warranty; and each file should
+have at least the "copyright" line and a pointer to where the full
+notice is found.
+
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or
+your school, if any, to sign a "copyright disclaimer" for the library,
+if necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James
+  Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/Doxyfile b/Doxyfile
new file mode 100644
index 0000000..b194095
--- /dev/null
+++ b/Doxyfile
@@ -0,0 +1,109 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+# libgpiod doxygen configuration
+
+# General configuration
+PROJECT_NAME           = libgpiod
+OUTPUT_DIRECTORY       = doc
+OUTPUT_LANGUAGE        = English
+EXTRACT_ALL            = NO
+EXTRACT_PRIVATE        = NO
+EXTRACT_STATIC         = YES
+HIDE_UNDOC_MEMBERS     = NO
+HIDE_UNDOC_CLASSES     = NO
+BRIEF_MEMBER_DESC      = YES
+REPEAT_BRIEF           = YES
+ALWAYS_DETAILED_SEC    = NO
+FULL_PATH_NAMES        = NO
+STRIP_FROM_PATH        =
+INTERNAL_DOCS          = NO
+STRIP_CODE_COMMENTS    = YES
+CASE_SENSE_NAMES       = YES
+SHORT_NAMES            = NO
+HIDE_SCOPE_NAMES       = NO
+VERBATIM_HEADERS       = YES
+SHOW_INCLUDE_FILES     = YES
+JAVADOC_AUTOBRIEF      = YES
+INHERIT_DOCS           = YES
+INLINE_INFO            = YES
+SORT_MEMBER_DOCS       = YES
+DISTRIBUTE_GROUP_DOC   = NO
+TAB_SIZE               = 8
+GENERATE_TODOLIST      = YES
+GENERATE_TESTLIST      = YES
+GENERATE_BUGLIST       = YES
+ALIASES                =
+ENABLED_SECTIONS       =
+MAX_INITIALIZER_LINES  = 30
+OPTIMIZE_OUTPUT_FOR_C  = YES
+SHOW_USED_FILES        = YES
+QUIET                  = YES
+WARNINGS               = YES
+WARN_IF_UNDOCUMENTED   = YES
+WARN_FORMAT            =
+WARN_LOGFILE           =
+INPUT                  = include/gpiod.h
+SOURCE_BROWSER         = YES
+INLINE_SOURCES         = NO
+REFERENCED_BY_RELATION = YES
+REFERENCES_RELATION    = YES
+ALPHABETICAL_INDEX     = NO
+COLS_IN_ALPHA_INDEX    = 5
+IGNORE_PREFIX          =
+SEARCHENGINE           = NO
+ENABLE_PREPROCESSING   = YES
+
+# HTML output
+GENERATE_HTML          = YES
+HTML_OUTPUT            =
+HTML_HEADER            =
+HTML_FOOTER            =
+HTML_STYLESHEET        =
+GENERATE_HTMLHELP      = NO
+GENERATE_CHI           = NO
+BINARY_TOC             = NO
+TOC_EXPAND             = NO
+DISABLE_INDEX          = NO
+ENUM_VALUES_PER_LINE   = 4
+GENERATE_TREEVIEW      = NO
+TREEVIEW_WIDTH         = 250
+
+# LaTeX output
+GENERATE_LATEX         = NO
+LATEX_OUTPUT           =
+COMPACT_LATEX          = NO
+PAPER_TYPE             = a4wide
+EXTRA_PACKAGES         =
+LATEX_HEADER           =
+PDF_HYPERLINKS         = NO
+USE_PDFLATEX           = NO
+LATEX_BATCHMODE        = NO
+
+# RTF output
+GENERATE_RTF           = NO
+RTF_OUTPUT             =
+COMPACT_RTF            = NO
+RTF_HYPERLINKS         = NO
+RTF_STYLESHEET_FILE    =
+RTF_EXTENSIONS_FILE    =
+
+# Man page output
+GENERATE_MAN           = YES
+MAN_OUTPUT             = man
+MAN_EXTENSION          = .3
+MAN_LINKS              = YES
+
+# XML output
+GENERATE_XML           = YES
+
+# External references
+TAGFILES               =
+GENERATE_TAGFILE       =
+ALLEXTERNALS           = NO
+PERL_PATH              =
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..adf0d9c
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1,41 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2019 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+ACLOCAL_AMFLAGS = -I m4
+AUTOMAKE_OPTIONS = foreign
+SUBDIRS = include lib bindings
+
+if WITH_TOOLS
+
+SUBDIRS += tools man
+
+endif
+
+if WITH_TESTS
+
+SUBDIRS += tests
+
+endif
+
+if HAS_DOXYGEN
+
+doc:
+	@(cat Doxyfile; \
+		echo PROJECT_NUMBER = $(VERSION_STR); \
+		echo INPUT += bindings/cxx/gpiod.hpp) | doxygen -
+.PHONY: doc
+
+clean-local:
+	rm -rf doc
+
+EXTRA_DIST = Doxyfile
+
+endif
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA = libgpiod.pc
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..20640ef
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,195 @@
+libgpiod v1.3
+=============
+
+New features:
+- the gpio-tools now have automatically generated (using help2man) man pages
+  that are bundled with the release tarball
+- support a singular 'default_val' argument in Line.request() in python
+  bindings
+- the test executable can now be installed to the bindir along with the
+  gpio-tools and the testing framework will look for the binaries in standard
+  locations if it's not run from the top source directory
+- gpiomon now supports line buffered output
+
+Improvements:
+- tweaks to the C API documentation
+- treewide unification of the naming of local variables
+- extended helptest in gpioset (explanation of the way the character device
+  works aimed at reducing user confusion when a GPIO line reverts to its
+  default value after gpioset exits)
+- the source directories have been rearranged and the src/ directory was
+  dropped, lib/ and tools/ now live in the top source directory
+- minor coding style fixes in python bindings, ctxless functions and tools
+- automatically generated documentation is now removed by 'make clean'
+- all Makefiles now use top_builddir instead of relative paths
+- code shrink in configure.ac
+- add a brief section about API documentation to README
+
+Bug fixes:
+- fix a segfault causing bug in C++ bindings
+- make bitset_cmp::operator() const as this is required by C++17
+- ignore 'remove' events from udev in the testing framework
+- don't segfault on num_lines = 0 in ctxless functions
+
+libgpiod v1.2
+=============
+
+New features:
+- new contextless event monitor that should replace the previous event loop
+  which caused problems on hardware that doesn't allow to watch both rising
+  and falling edge events
+- port gpiomon to the new event monitor
+- deprecate event loop routines
+
+Improvements:
+- many minor improvements and tweaks in the python module
+- new test cases for python bindings
+- add much more detailed documentation for python bindings
+- coding style improvements in gpio-tools
+- remove unicode characters from build scripts
+- improve the help text messages in gpio-tools
+- make gpiod_chip_open() and its variants verify that we're really trying to
+  open a character device associated with a GPIO chip
+
+Bug fixes:
+- fix memory leaks in python bindings
+- fix a memory corruption bug in python bindings
+- fix the default_vals argument in line request implementation in python
+  bindings
+- fix a compilation warning in python bindings
+- fix gpiod_Chip_find_lines() for nonexistent lines (python bindings)
+- add a missing include in C++ bindings examples
+- correctly display the version string in gpio-tools
+
+libgpiod v1.1
+=============
+
+New features:
+- add object-oriented C++ bindings
+- add object-oriented Python3 bindings
+- add several new helpers to the C API
+
+Improvements:
+- start using separate versioning schemes for API and ABI
+- use SPDX license identifiers and remove LGPL boilerplate
+- check for unexpanded macros in configure.ac
+
+Bug fixes:
+- include Doxyfile in the release tarball
+- fix the implicit-fallthrough warnings
+- make tests work together with gpio-mockup post v4.16 linux kernel
+- use reference counting for line file descriptors
+- correctly handle POLLNVAL when polling for events
+- fix the copyright notice in tools
+
+libgpiod v1.0
+=============
+
+NOTE: This is a major release - it breaks the API compatibility with
+      the 0.x.y series.
+
+New features:
+- remove custom error handling in favor of errnos
+- merge the two separate interfaces for event requests and regular line
+  requests
+- redesign the simple API
+- change the prefix of the high-level API from 'simple' to 'ctxless' (for
+  contextless) which better reflects its purpose
+- redesign the iterator API
+- make use of prefixes more consistent
+- rename symbols all over the place
+- various minor tweaks
+- add support for pkg-config
+
+Improvements:
+- add a bunch of helpers for line requests
+- split the library code into multiple source files by functionality
+- re-enable a test case previously broken by a bug in the kernel
+
+Bug fixes:
+- correctly handle signal interrupts when polling in gpiod_simple_event_loop()
+- fix the linking order when building with static libraries
+- pass the correct consumer string to gpiod_simple_get_value_multiple() in
+  gpioget
+- fix a line test case: don't use open-drain or open-source flags for input
+  mode
+- fix the flags passed to ar in order to supress a build warning
+- set the last error code in gpiod_chip_open_by_label() to ENOENT if a chip
+  can't be found
+- fix checking the kernel version in the test suite
+- fix various coding style issues
+- initialize the active low variable in gpiomon
+
+libgpiod v0.3
+=============
+
+New features:
+- gpiomon can now watch multiple lines at the same time and supports custom
+  output formats which can be specified using the --format argument
+- testing framework can now test external programs: test cases for gpio-tools
+  have been added
+
+Improvements:
+- improve error messages
+- improve README examples
+- configure script improvements
+
+Bug fixes:
+- use correct UAPI flags when requesting line events
+- capitalize 'GPIO' in error messages in gpioset, gpioget & gpiomon
+- tweak the error message on invalid arguments in gpiofind
+- don't ignore superfluous arguments and fix the displayed name for falling
+  edge events in gpiomon
+
+libgpiod v0.2
+=============
+
+New features:
+- relicensed under LGPLv2.1
+- implemented a unit testing framework together with a comprehensive
+  set of test cases
+- added a non-closing variant of the gpiochip iterator and foreach
+  macro [by Clemens Gruber]
+- added gpiod_chip_open_by_label()
+
+Improvements:
+- Makefiles & build commands have been reworked [by Thierry Reding]
+- documentation updates
+- code shrinkage here and there
+- coding style fixes
+- removed all designated initializers from the header for better standards
+  compliance
+
+Bug fixes:
+- fix the return value of gpiod_simple_event_loop()
+- don't try to process docs if doxygen is not installed
+- pass the O_CLOEXEC flag to open() when opening the GPIO chip device file
+- include <poll.h> instead of <sys/poll.h> in gpioset
+- fix a formatting issue in gpioinfo for chips with >100 GPIO lines
+- fix a bug when requesting both-edges event notifications
+- fix short options in gpiomon (short opt for --silent was missing)
+- correct the kernel headers requirements in README
+- include <time.h> for struct timespec
+- include <poll.h> instead of <sys/poll.h>
+- detect the version of strerror_r()
+
+libgpiod v0.1
+=============
+
+First version of libgpiod.
+
+It's currently possible to:
+- get and set the values of multiple GPIO lines with a single function call
+- monitor a GPIO line for events
+- enumerate all GPIO chips present in the system
+- enumerate all GPIO lines exposed by a chip
+- extract information about GPIO chips (label, name, number of lines)
+- extract information about GPIO lines (name, flags, state, user)
+
+Tools provided with the library are:
+- gpioget - read values from GPIO lines
+- gpioset - set values of GPIO lines
+- gpiodetect - list GPIO chips
+- gpioinfo - print info about GPIO lines exposed by a chip
+- gpiomon - monitor a GPIO line for events
+- gpiofind - find a GPIO line by name
diff --git a/NOTICE b/NOTICE
new file mode 120000
index 0000000..d24842f
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1 @@
+COPYING
\ No newline at end of file
diff --git a/README b/README
new file mode 100644
index 0000000..6794d4f
--- /dev/null
+++ b/README
@@ -0,0 +1,170 @@
+libgpiod
+========
+
+  libgpiod - C library and tools for interacting with the linux GPIO
+             character device (gpiod stands for GPIO device)
+
+Since linux 4.8 the GPIO sysfs interface is deprecated. User space should use
+the character device instead. This library encapsulates the ioctl calls and
+data structures behind a straightforward API.
+
+RATIONALE
+---------
+
+The new character device interface guarantees all allocated resources are
+freed after closing the device file descriptor and adds several new features
+that are not present in the obsolete sysfs interface (like event polling,
+setting/reading multiple values at once or open-source and open-drain GPIOs).
+
+Unfortunately interacting with the linux device file can no longer be done
+using only standard command-line tools. This is the reason for creating a
+library encapsulating the cumbersome, ioctl-based kernel-userspace interaction
+in a set of convenient functions and opaque data structures.
+
+Additionally this project contains a set of command-line tools that should
+allow an easy conversion of user scripts to using the character device.
+
+BUILDING
+--------
+
+This is a pretty standard autotools project. It does not depend on any
+libraries other than the standard C library with GNU extensions.
+
+The autoconf version needed to compile the project is 2.61.
+
+Recent (as in >= v4.8) kernel headers are also required for the GPIO user
+API definitions.
+
+To build the project (including command-line utilities) run:
+
+    ./autogen.sh --enable-tools=yes --prefix=<install path>
+    make
+    make install
+
+The autogen script will execute ./configure and pass all the command-line
+arguments to it.
+
+If building from release tarballs, the configure script is already provided and
+there's no need to invoke autogen.sh.
+
+TOOLS
+-----
+
+There are currently six command-line tools available:
+
+* gpiodetect - list all gpiochips present on the system, their names, labels
+               and number of GPIO lines
+
+* gpioinfo   - list all lines of specified gpiochips, their names, consumers,
+               direction, active state and additional flags
+
+* gpioget    - read values of specified GPIO lines
+
+* gpioset    - set values of specified GPIO lines, potentially keep the lines
+               exported and wait until timeout, user input or signal
+
+* gpiofind   - find the gpiochip name and line offset given the line name
+
+* gpiomon    - wait for events on GPIO lines, specify which events to watch,
+               how many events to process before exiting or if the events
+               should be reported to the console
+
+Examples:
+
+    # Read the value of a single GPIO line.
+    $ gpioget gpiochip1 23
+    0
+
+    # Read two values at the same time. Set the active state of the lines
+    # to low.
+    $ gpioget --active-low gpiochip1 23 24
+    1 1
+
+    # Set values of two lines, then daemonize and wait for a signal (SIGINT or
+    # SIGTERM) before releasing them.
+    $ gpioset --mode=signal --background gpiochip1 23=1 24=0
+
+    # Set the value of a single line, then exit immediately. This is useful
+    # for floating pins.
+    $ gpioset gpiochip1 23=1
+
+    # Find a GPIO line by name.
+    $ gpiofind "USR-LED-2"
+    gpiochip1 23
+
+    # Toggle a GPIO by name, then wait for the user to press ENTER.
+    $ gpioset --mode=wait `gpiofind "USR-LED-2"`=1
+
+    # Wait for three rising edge events on a single GPIO line, then exit.
+    $ gpiomon --num-events=3 --rising-edge gpiochip2 3
+    event:  RISING EDGE offset: 3 timestamp: [    1151.814356387]
+    event:  RISING EDGE offset: 3 timestamp: [    1151.815449803]
+    event:  RISING EDGE offset: 3 timestamp: [    1152.091556803]
+
+    # Wait for a single falling edge event. Specify a custom output format.
+    $ gpiomon --format="%e %o %s %n" --falling-edge gpiochip1 4
+    0 4 1156 615459801
+
+    # Pause execution until a single event of any type occurs. Don't print
+    # anything. Find the line by name.
+    $ gpiomon --num-events=1 --silent `gpiofind "USR-IN"`
+
+    # Monitor multiple lines, exit after the first event.
+    $ gpiomon --silent --num-events=1 gpiochip0 2 3 5
+
+TESTING
+-------
+
+A comprehensive testing framework is included with the library and can be
+used to test both the library code and the linux kernel user-space interface.
+
+The minimum kernel version required to run the tests is 4.16. The tests work
+together with the gpio-mockup kernel module which must be enabled. NOTE: the
+module must not be built-in.
+
+To build the testing executable run:
+
+    ./configure --enable-tests
+    make check
+
+As opposed to standard autotools projects, libgpiod doesn't execute any tests
+when invoking 'make check'. Instead the user must run them manually with
+superuser privileges:
+
+    sudo ./tests/gpiod-test
+
+BINDINGS
+--------
+
+High-level, object-oriented bindings for C++ and python3 are provided. They
+can be enabled by passing --enable-bindings-cxx and --enable-bindings-python
+arguments respectively to configure.
+
+C++ bindings require C++11 support and autoconf-archive collection if building
+from git.
+
+Python bindings require python3 support and libpython development files. Care
+must be taken when cross-compiling python bindings: users usually must specify
+the PYTHON_CPPFLAGS and PYTHON_LIBS variables in order to point the build
+system to the correct locations. During native builds, the configure script
+can auto-detect the location of the development files.
+
+DOCUMENTATION
+-------------
+
+All API symbols exposed by the core C API and C++ bindings are documented with
+doxygen markup blocks. Doxygen documentation can be generated by executing
+'make doc' given that the doxygen executable is available in the system.
+
+Python bindings contain help strings that can be accessed with the help
+builtin.
+
+Man pages for command-line programs are generated automatically if gpio-tools
+were selected and help2man is available in the system.
+
+CONTRIBUTING
+------------
+
+Contributions are welcome - please send patches and bug reports to
+linux-gpio@vger.kernel.org (add the [libgpiod] prefix to the e-mail subject
+line) and stick to the linux kernel coding style when submitting new code.
diff --git a/autogen.sh b/autogen.sh
new file mode 100755
index 0000000..1da6874
--- /dev/null
+++ b/autogen.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#                    2017 Thierry Reding <treding@nvidia.com>
+#
+
+srcdir=`dirname $0`
+test -z "$srcdir" && srcdir=.
+
+ORIGDIR=`pwd`
+cd "$srcdir"
+
+autoreconf --force --install --verbose || exit 1
+cd $ORIGDIR || exit $?
+
+if test -z "$NOCONFIGURE"; then
+	exec "$srcdir"/configure "$@"
+fi
diff --git a/bindings/Makefile.am b/bindings/Makefile.am
new file mode 100644
index 0000000..24e2c98
--- /dev/null
+++ b/bindings/Makefile.am
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+SUBDIRS = .
+
+if WITH_BINDINGS_CXX
+
+SUBDIRS += cxx
+
+endif
+
+if WITH_BINDINGS_PYTHON
+
+SUBDIRS += python
+
+endif
diff --git a/bindings/cxx/Makefile.am b/bindings/cxx/Makefile.am
new file mode 100644
index 0000000..d901836
--- /dev/null
+++ b/bindings/cxx/Makefile.am
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+lib_LTLIBRARIES = libgpiodcxx.la
+libgpiodcxx_la_SOURCES = chip.cpp iter.cpp line.cpp line_bulk.cpp
+libgpiodcxx_la_CPPFLAGS = -Wall -Wextra -g -std=gnu++11
+libgpiodcxx_la_CPPFLAGS += -fvisibility=hidden -I$(top_srcdir)/include/
+libgpiodcxx_la_LDFLAGS = -version-info $(subst .,:,$(ABI_CXX_VERSION))
+libgpiodcxx_la_LDFLAGS += -lgpiod -L$(top_builddir)/lib
+
+include_HEADERS = gpiod.hpp
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA = libgpiodcxx.pc
+
+SUBDIRS = . examples
diff --git a/bindings/cxx/chip.cpp b/bindings/cxx/chip.cpp
new file mode 100644
index 0000000..9b825c7
--- /dev/null
+++ b/bindings/cxx/chip.cpp
@@ -0,0 +1,201 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#include <functional>
+#include <gpiod.hpp>
+#include <map>
+#include <system_error>
+#include <utility>
+
+namespace gpiod {
+
+namespace {
+
+::gpiod_chip* open_lookup(const ::std::string& device)
+{
+	return ::gpiod_chip_open_lookup(device.c_str());
+}
+
+::gpiod_chip* open_by_path(const ::std::string& device)
+{
+	return ::gpiod_chip_open(device.c_str());
+}
+
+::gpiod_chip* open_by_name(const ::std::string& device)
+{
+	return ::gpiod_chip_open_by_name(device.c_str());
+}
+
+::gpiod_chip* open_by_label(const ::std::string& device)
+{
+	return ::gpiod_chip_open_by_label(device.c_str());
+}
+
+::gpiod_chip* open_by_number(const ::std::string& device)
+{
+	return ::gpiod_chip_open_by_number(::std::stoul(device));
+}
+
+using open_func = ::std::function<::gpiod_chip* (const ::std::string&)>;
+
+const ::std::map<int, open_func> open_funcs = {
+	{ chip::OPEN_LOOKUP,	open_lookup,	},
+	{ chip::OPEN_BY_PATH,	open_by_path,	},
+	{ chip::OPEN_BY_NAME,	open_by_name,	},
+	{ chip::OPEN_BY_LABEL,	open_by_label,	},
+	{ chip::OPEN_BY_NUMBER,	open_by_number,	},
+};
+
+void chip_deleter(::gpiod_chip* chip)
+{
+	::gpiod_chip_close(chip);
+}
+
+} /* namespace */
+
+chip::chip(const ::std::string& device, int how)
+	: _m_chip()
+{
+	this->open(device, how);
+}
+
+chip::chip(::gpiod_chip* chip)
+	: _m_chip(chip, chip_deleter)
+{
+
+}
+
+void chip::open(const ::std::string& device, int how)
+{
+	auto func = open_funcs.at(how);
+
+	::gpiod_chip *chip = func(device);
+	if (!chip)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "cannot open GPIO device " + device);
+
+	this->_m_chip.reset(chip, chip_deleter);
+}
+
+void chip::reset(void) noexcept
+{
+	this->_m_chip.reset();
+}
+
+::std::string chip::name(void) const
+{
+	this->throw_if_noref();
+
+	return ::std::move(::std::string(::gpiod_chip_name(this->_m_chip.get())));
+}
+
+::std::string chip::label(void) const
+{
+	this->throw_if_noref();
+
+	return ::std::move(::std::string(::gpiod_chip_label(this->_m_chip.get())));
+}
+
+unsigned int chip::num_lines(void) const
+{
+	this->throw_if_noref();
+
+	return ::gpiod_chip_num_lines(this->_m_chip.get());
+}
+
+line chip::get_line(unsigned int offset) const
+{
+	this->throw_if_noref();
+
+	if (offset >= this->num_lines())
+		throw ::std::out_of_range("line offset greater than the number of lines");
+
+	::gpiod_line* line_handle = ::gpiod_chip_get_line(this->_m_chip.get(), offset);
+	if (!line_handle)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "error getting GPIO line from chip");
+
+	return ::std::move(line(line_handle, *this));
+}
+
+line chip::find_line(const ::std::string& name) const
+{
+	this->throw_if_noref();
+
+	::gpiod_line* handle = ::gpiod_chip_find_line(this->_m_chip.get(), name.c_str());
+	if (!handle && errno != ENOENT)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "error looking up GPIO line by name");
+
+	return ::std::move(handle ? line(handle, *this) : line());
+}
+
+line_bulk chip::get_lines(const ::std::vector<unsigned int>& offsets) const
+{
+	line_bulk lines;
+
+	for (auto& it: offsets)
+		lines.append(this->get_line(it));
+
+	return ::std::move(lines);
+}
+
+line_bulk chip::get_all_lines(void) const
+{
+	line_bulk lines;
+
+	for (unsigned int i = 0; i < this->num_lines(); i++)
+		lines.append(this->get_line(i));
+
+	return ::std::move(lines);
+}
+
+line_bulk chip::find_lines(const ::std::vector<::std::string>& names) const
+{
+	line_bulk lines;
+	line line;
+
+	for (auto& it: names) {
+		line = this->find_line(it);
+		if (!line) {
+			lines.clear();
+			return ::std::move(lines);
+		}
+
+		lines.append(line);
+	}
+
+	return ::std::move(lines);
+}
+
+bool chip::operator==(const chip& rhs) const noexcept
+{
+	return this->_m_chip.get() == rhs._m_chip.get();
+}
+
+bool chip::operator!=(const chip& rhs) const noexcept
+{
+	return this->_m_chip.get() != rhs._m_chip.get();
+}
+
+chip::operator bool(void) const noexcept
+{
+	return this->_m_chip.get() != nullptr;
+}
+
+bool chip::operator!(void) const noexcept
+{
+	return this->_m_chip.get() == nullptr;
+}
+
+void chip::throw_if_noref(void) const
+{
+	if (!this->_m_chip.get())
+		throw ::std::logic_error("object not associated with an open GPIO chip");
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/examples/Makefile.am b/bindings/cxx/examples/Makefile.am
new file mode 100644
index 0000000..201e1db
--- /dev/null
+++ b/bindings/cxx/examples/Makefile.am
@@ -0,0 +1,33 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+AM_CPPFLAGS = -I$(top_srcdir)/bindings/cxx/ -I$(top_srcdir)/include
+AM_CPPFLAGS += -Wall -Wextra -g -std=gnu++11
+AM_LDFLAGS = -lgpiodcxx -L$(top_builddir)/bindings/cxx/
+
+check_PROGRAMS =	gpiod_cxx_tests \
+			gpiodetectcxx \
+			gpiofindcxx \
+			gpiogetcxx \
+			gpioinfocxx \
+			gpiomoncxx \
+			gpiosetcxx
+
+gpiod_cxx_tests_SOURCES = gpiod_cxx_tests.cpp
+
+gpiodetectcxx_SOURCES = gpiodetectcxx.cpp
+
+gpiofindcxx_SOURCES = gpiofindcxx.cpp
+
+gpiogetcxx_SOURCES = gpiogetcxx.cpp
+
+gpioinfocxx_SOURCES = gpioinfocxx.cpp
+
+gpiomoncxx_SOURCES = gpiomoncxx.cpp
+
+gpiosetcxx_SOURCES = gpiosetcxx.cpp
diff --git a/bindings/cxx/examples/gpiod_cxx_tests.cpp b/bindings/cxx/examples/gpiod_cxx_tests.cpp
new file mode 100644
index 0000000..767fe98
--- /dev/null
+++ b/bindings/cxx/examples/gpiod_cxx_tests.cpp
@@ -0,0 +1,487 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/*
+ * Misc tests/examples of the C++ API.
+ *
+ * These tests assume that at least one dummy gpiochip is present in the
+ * system and that it's detected as gpiochip0.
+ */
+
+#include <gpiod.hpp>
+
+#include <stdexcept>
+#include <cstdlib>
+#include <iostream>
+#include <fstream>
+#include <map>
+#include <cstring>
+#include <cerrno>
+#include <functional>
+
+#include <poll.h>
+
+namespace {
+
+using test_func = ::std::function<void (void)>;
+
+::std::vector<::std::pair<::std::string, test_func>> test_funcs;
+
+struct test_case
+{
+	test_case(const ::std::string& name, test_func& func)
+	{
+		test_funcs.push_back(::std::make_pair(name, func));
+	}
+};
+
+#define TEST_CASE(_func)						\
+	test_func _test_func_##_func(_func);				\
+	test_case _test_case_##_func(#_func, _test_func_##_func)
+
+std::string boolstr(bool b)
+{
+	return b ? "true" : "false";
+}
+
+void fire_line_event(const ::std::string& chip, unsigned int offset, bool rising)
+{
+	::std::string path;
+
+	path = "/sys/kernel/debug/gpio-mockup-event/" + chip + "/" + ::std::to_string(offset);
+
+	::std::ofstream out(path);
+	if (!out)
+		throw ::std::runtime_error("unable to open " + path);
+
+	out << ::std::to_string(rising ? 1 : 0) << ::std::endl;
+}
+
+void print_event(const ::gpiod::line_event& event)
+{
+	::std::cerr << "type: "
+		    << (event.event_type == ::gpiod::line_event::RISING_EDGE ? "rising" : "falling")
+		    << "\n"
+		    << "timestamp: "
+		    << ::std::chrono::duration_cast<::std::chrono::seconds>(event.timestamp).count()
+		    << "."
+		    << event.timestamp.count() % 1000000000
+		    << "\n"
+		    << "source line offset: "
+		    << event.source.offset()
+		    << ::std::endl;	
+}
+
+void chip_open_default_lookup(void)
+{
+	::gpiod::chip by_name("gpiochip0");
+	::gpiod::chip by_path("/dev/gpiochip0");
+	::gpiod::chip by_label("gpio-mockup-A");
+	::gpiod::chip by_number("0");
+
+	::std::cerr << "all good" << ::std::endl;
+}
+TEST_CASE(chip_open_default_lookup);
+
+void chip_open_different_modes(void)
+{
+	::gpiod::chip by_name("gpiochip0", ::gpiod::chip::OPEN_BY_NAME);
+	::gpiod::chip by_path("/dev/gpiochip0", ::gpiod::chip::OPEN_BY_PATH);
+	::gpiod::chip by_label("gpio-mockup-A", ::gpiod::chip::OPEN_BY_LABEL);
+	::gpiod::chip by_number("0", ::gpiod::chip::OPEN_BY_NUMBER);
+
+	::std::cerr << "all good" << ::std::endl;
+}
+TEST_CASE(chip_open_different_modes);	
+
+void chip_open_nonexistent(void)
+{
+	try {
+		::gpiod::chip chip("/nonexistent_gpiochip");
+	} catch (const ::std::system_error& exc) {
+		::std::cerr << "system_error thrown as expected: " << exc.what() << ::std::endl;
+		return;
+	}
+
+	throw ::std::runtime_error("system_error should have been thrown");
+}
+TEST_CASE(chip_open_nonexistent);
+
+void chip_open_method(void)
+{
+	::gpiod::chip chip;
+
+	if (chip)
+		throw ::std::runtime_error("chip should be empty");
+
+	chip.open("gpiochip0");
+
+	::std::cerr << "all good" << ::std::endl;
+}
+TEST_CASE(chip_open_method);
+
+void chip_reset(void)
+{
+	::gpiod::chip chip("gpiochip0");
+
+	if (!chip)
+		throw ::std::runtime_error("chip object is not valid");
+
+	chip.reset();
+
+	if (chip)
+		throw ::std::runtime_error("chip should be invalid");
+
+	::std::cerr << "all good" << ::std::endl;
+}
+TEST_CASE(chip_reset);
+
+void chip_info(void)
+{
+	::gpiod::chip chip("gpiochip0");
+
+	::std::cerr << "chip name: " << chip.name() << ::std::endl;
+	::std::cerr << "chip label: " << chip.label() << ::std::endl;
+	::std::cerr << "number of lines " << chip.num_lines() << ::std::endl;
+}
+TEST_CASE(chip_info);
+
+void chip_operators(void)
+{
+	::gpiod::chip chip1("gpiochip0");
+	auto chip2 = chip1;
+
+	if ((chip1 != chip2) || !(chip1 == chip2))
+		throw ::std::runtime_error("chip objects should be equal");
+
+	::std::cerr << "all good" << ::std::endl;
+}
+TEST_CASE(chip_operators);
+
+void chip_line_ops(void)
+{
+	::gpiod::chip chip("gpiochip0");
+
+	auto line = chip.get_line(3);
+	::std::cerr << "Got line by offset: " << line.offset() << ::std::endl;
+
+	line = chip.find_line("gpio-mockup-A-4");
+	::std::cerr << "Got line by name: " << line.name() << ::std::endl;
+
+	auto lines = chip.get_lines({ 1, 2, 3, 6, 6 });
+	::std::cerr << "Got multiple lines by offset: ";
+	for (auto& it: lines)
+		::std::cerr << it.offset() << " ";
+	::std::cerr << ::std::endl;
+
+	lines = chip.find_lines({ "gpio-mockup-A-1", "gpio-mockup-A-4", "gpio-mockup-A-7" });
+	::std::cerr << "Got multiple lines by name: ";
+	for (auto& it: lines)
+		::std::cerr << it.name() << " ";
+	::std::cerr << ::std::endl;
+}
+TEST_CASE(chip_line_ops);
+
+void chip_find_lines_nonexistent(void)
+{
+	::gpiod::chip chip("gpiochip0");
+
+	auto lines = chip.find_lines({ "gpio-mockup-A-1", "nonexistent", "gpio-mockup-A-4" });
+	if (lines)
+		throw ::std::logic_error("line_bulk object should be invalid");
+
+	::std::cerr << "line_bulk invalid as expected" << ::std::endl;
+}
+TEST_CASE(chip_find_lines_nonexistent);
+
+void line_info(void)
+{
+	::gpiod::chip chip("gpiochip0");
+	::gpiod::line line = chip.get_line(2);
+
+	::std::cerr << "line offset: " << line.offset() << ::std::endl;
+	::std::cerr << "line name: " << line.name() << ::std::endl;
+	::std::cerr << "line direction: "
+		    << (line.direction() == ::gpiod::line::DIRECTION_INPUT ?
+			"input" : "output") << ::std::endl;
+	::std::cerr << "line active state: "
+		    << (line.active_state() == ::gpiod::line::ACTIVE_LOW ?
+			"active low" : "active high") << :: std::endl;
+}
+TEST_CASE(line_info);
+
+void empty_objects(void)
+{
+	::std::cerr << "Are initialized line & chip objects 'false'?" << ::std::endl;
+
+	::gpiod::line line;
+	if (line)
+		throw ::std::logic_error("line built with a default constructor should be 'false'");
+
+	::gpiod::chip chip;
+	if (chip)
+		throw ::std::logic_error("chip built with a default constructor should be 'false'");
+
+	::std::cerr << "YES" << ::std::endl;
+}
+TEST_CASE(empty_objects);
+
+void line_bulk_iterator(void)
+{
+	::std::cerr << "Checking line_bulk iterators" << ::std::endl;
+
+	::gpiod::chip chip("gpiochip0");
+	::gpiod::line_bulk bulk = chip.get_lines({ 0, 1, 2, 3, 4 });
+
+	for (auto& it: bulk)
+		::std::cerr << it.name() << ::std::endl;
+
+	::std::cerr << "DONE" << ::std::endl;
+}
+TEST_CASE(line_bulk_iterator);
+
+void single_line_test(void)
+{
+	const ::std::string line_name("gpio-mockup-A-4");
+
+	::std::cerr << "Looking up a GPIO line by name (" << line_name << ")" << ::std::endl;
+
+	auto line = ::gpiod::find_line(line_name);
+	if (!line)
+		throw ::std::runtime_error(line_name + " line not found");
+
+	::std::cerr << "Requesting a single line" << ::std::endl;
+
+	::gpiod::line_request conf;
+	conf.consumer = "gpiod_cxx_tests";
+	conf.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
+
+	line.request(conf, 1);
+	::std::cerr << "Reading value" << ::std::endl;
+	::std::cerr << line.get_value() << ::std::endl;
+	::std::cerr << "Changing value to 0" << ::std::endl;
+	line.set_value(0);
+	::std::cerr << "Reading value again" << ::std::endl;
+	::std::cerr << line.get_value() << ::std::endl;
+}
+TEST_CASE(single_line_test);
+
+void line_flags(void)
+{
+	::gpiod::chip chip("gpiochip0");
+
+	auto line = chip.get_line(0);
+
+	::std::cerr << "line is used: " << boolstr(line.is_used()) << ::std::endl;
+	::std::cerr << "line is requested: " << boolstr(line.is_requested()) << ::std::endl;
+
+	::std::cerr << "requesting line" << ::std::endl;
+
+	::gpiod::line_request config;
+	config.consumer = "gpiod_cxx_tests";
+	config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
+	config.flags = ::gpiod::line_request::FLAG_OPEN_DRAIN;
+	line.request(config);
+
+	::std::cerr << "line is used: " << boolstr(line.is_used()) << ::std::endl;
+	::std::cerr << "line is open drain: " << boolstr(line.is_open_drain()) << ::std::endl;
+	::std::cerr << "line is open source: " << boolstr(line.is_open_source()) << ::std::endl;
+	::std::cerr << "line is requested: " << boolstr(line.is_requested()) << ::std::endl;
+
+	if (!line.is_open_drain())
+		throw ::std::logic_error("line should be open-drain");
+}
+TEST_CASE(line_flags);
+
+void multiple_lines_test(void)
+{
+	::gpiod::chip chip("gpiochip0");
+
+	::std::cerr << "Getting multiple lines by offsets" << ::std::endl;
+
+	auto lines = chip.get_lines({ 0, 2, 3, 4, 6 });
+
+	::std::cerr << "Requesting them for output" << ::std::endl;
+
+	::gpiod::line_request config;
+	config.consumer = "gpiod_cxx_tests";
+	config.request_type = ::gpiod::line_request::DIRECTION_OUTPUT;
+
+	lines.request(config);
+
+	::std::cerr << "Setting values" << ::std::endl;
+
+	lines.set_values({ 0, 1, 1, 0, 1});
+
+	::std::cerr << "Requesting the lines for input" << ::std::endl;
+
+	config.request_type = ::gpiod::line_request::DIRECTION_INPUT;
+	lines.release();
+	lines.request(config);
+
+	::std::cerr << "Reading the values" << ::std::endl;
+
+	auto vals = lines.get_values();
+
+	for (auto& it: vals)
+		::std::cerr << it << " ";
+	::std::cerr << ::std::endl;
+}
+TEST_CASE(multiple_lines_test);
+
+void chip_get_all_lines(void)
+{
+	::gpiod::chip chip("gpiochip0");
+
+	::std::cerr << "Getting all lines from a chip" << ::std::endl;
+
+	auto lines = chip.get_all_lines();
+
+	for (auto& it: lines)
+		::std::cerr << "Offset: " << it.offset() << ::std::endl;
+}
+TEST_CASE(chip_get_all_lines);
+
+void line_event_single_line(void)
+{
+	::gpiod::chip chip("gpiochip0");
+
+	auto line = chip.get_line(1);
+
+	::gpiod::line_request config;
+	config.consumer = "gpiod_cxx_tests";
+	config.request_type = ::gpiod::line_request::EVENT_BOTH_EDGES;
+
+	::std::cerr << "requesting line for events" << ::std::endl;
+	line.request(config);
+
+	::std::cerr << "generating a line event" << ::std::endl;
+	fire_line_event("gpiochip0", 1, true);
+
+	if (!line.event_wait(::std::chrono::nanoseconds(1000000000)))
+		throw ::std::runtime_error("waiting for event timed out");
+
+	auto event = line.event_read();
+
+	::std::cerr << "event received:" << ::std::endl;
+	print_event(event);
+}
+TEST_CASE(line_event_single_line);
+
+void line_event_multiple_lines(void)
+{
+	::gpiod::chip chip("gpiochip0");
+
+	auto lines = chip.get_lines({ 1, 2, 3, 4, 5 });
+
+	::gpiod::line_request config;
+	config.consumer = "gpiod_cxx_tests";
+	config.request_type = ::gpiod::line_request::EVENT_BOTH_EDGES;
+
+	::std::cerr << "requesting lines for events" << ::std::endl;
+	lines.request(config);
+
+	::std::cerr << "generating two line events" << ::std::endl;
+	fire_line_event("gpiochip0", 1, true);
+	fire_line_event("gpiochip0", 2, true);
+
+	auto event_lines = lines.event_wait(::std::chrono::nanoseconds(1000000000));
+	if (!event_lines || event_lines.size() != 2)
+		throw ::std::runtime_error("expected two line events");
+
+	::std::vector<::gpiod::line_event> events;
+	for (auto& line: event_lines) {
+		auto event = line.event_read();
+		events.push_back(event);
+	}
+
+	::std::cerr << "events received:" << ::std::endl;
+	for (auto& event: events)
+		print_event(event);
+}
+TEST_CASE(line_event_multiple_lines);
+
+void line_event_poll_fd(void)
+{
+	::std::map<int, ::gpiod::line> fd_line_map;
+
+	::gpiod::chip chip("gpiochip0");
+	auto lines = chip.get_lines({ 1, 2, 3, 4, 5, 6 });
+
+	::gpiod::line_request config;
+	config.consumer = "gpiod_cxx_tests";
+	config.request_type = ::gpiod::line_request::EVENT_BOTH_EDGES;
+
+	::std::cerr << "requesting lines for events" << ::std::endl;
+	lines.request(config);
+
+	::std::cerr << "generating three line events" << ::std::endl;
+	fire_line_event("gpiochip0", 2, true);
+	fire_line_event("gpiochip0", 3, true);
+	fire_line_event("gpiochip0", 5, false);
+
+	fd_line_map[lines[1].event_get_fd()] = lines[1];
+	fd_line_map[lines[2].event_get_fd()] = lines[2];
+	fd_line_map[lines[4].event_get_fd()] = lines[4];
+
+	pollfd fds[3];
+	fds[0].fd = lines[1].event_get_fd();
+	fds[1].fd = lines[2].event_get_fd();
+	fds[2].fd = lines[4].event_get_fd();
+
+	fds[0].events = fds[1].events = fds[2].events = POLLIN | POLLPRI;
+
+	int rv = poll(fds, 3, 1000);
+	if (rv < 0)
+		throw ::std::runtime_error("error polling for events: "
+						+ ::std::string(::strerror(errno)));
+	else if (rv == 0)
+		throw ::std::runtime_error("poll() timed out while waiting for events");
+
+	if (rv != 3)
+		throw ::std::runtime_error("expected three line events");
+
+	::std::cerr << "events received:" << ::std::endl;
+	for (unsigned int i = 0; i < 3; i++) {
+		if (fds[i].revents)
+			print_event(fd_line_map[fds[i].fd].event_read());
+	}
+}
+TEST_CASE(line_event_poll_fd);
+
+void chip_iterator(void)
+{
+	::std::cerr << "iterating over all GPIO chips in the system:" << ::std::endl;
+
+	for (auto& it: ::gpiod::make_chip_iter())
+		::std::cerr << it.name() << ::std::endl;
+}
+TEST_CASE(chip_iterator);
+
+void line_iterator(void)
+{
+	::std::cerr << "iterating over all lines exposed by a GPIO chip:" << ::std::endl;
+
+	::gpiod::chip chip("gpiochip0");
+
+	for (auto& it: ::gpiod::line_iter(chip))
+		::std::cerr << it.offset() << ": " << it.name() << ::std::endl;
+}
+TEST_CASE(line_iterator);
+
+} /* namespace */
+
+int main(int, char **)
+{
+	for (auto& it: test_funcs) {
+		::std::cerr << "=================================================" << ::std::endl;
+		::std::cerr << it.first << ":\n" << ::std::endl;
+		it.second();
+	}
+
+	return EXIT_SUCCESS;
+}
diff --git a/bindings/cxx/examples/gpiodetectcxx.cpp b/bindings/cxx/examples/gpiodetectcxx.cpp
new file mode 100644
index 0000000..6da5573
--- /dev/null
+++ b/bindings/cxx/examples/gpiodetectcxx.cpp
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* C++ reimplementation of the gpiodetect tool. */
+
+#include <gpiod.hpp>
+
+#include <cstdlib>
+#include <iostream>
+
+int main(int argc, char **argv)
+{
+	if (argc != 1) {
+		::std::cerr << "usage: " << argv[0] << ::std::endl;
+		return EXIT_FAILURE;
+	}
+
+	for (auto& it: ::gpiod::make_chip_iter()) {
+	        ::std::cout << it.name() << " ["
+			  << it.label() << "] ("
+			  << it.num_lines() << " lines)" << ::std::endl;
+	}
+
+	return EXIT_SUCCESS;
+}
diff --git a/bindings/cxx/examples/gpiofindcxx.cpp b/bindings/cxx/examples/gpiofindcxx.cpp
new file mode 100644
index 0000000..08fb62c
--- /dev/null
+++ b/bindings/cxx/examples/gpiofindcxx.cpp
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* C++ reimplementation of the gpiofind tool. */
+
+#include <gpiod.hpp>
+
+#include <cstdlib>
+#include <iostream>
+
+int main(int argc, char **argv)
+{
+	if (argc != 2) {
+		::std::cerr << "usage: " << argv[0] << " <line name>" << ::std::endl;
+		return EXIT_FAILURE;
+	}
+
+	::gpiod::line line = ::gpiod::find_line(argv[1]);
+	if (!line)
+		return EXIT_FAILURE;
+
+	::std::cout << line.get_chip().name() << " " << line.offset() << ::std::endl;
+
+	return EXIT_SUCCESS;
+}
diff --git a/bindings/cxx/examples/gpiogetcxx.cpp b/bindings/cxx/examples/gpiogetcxx.cpp
new file mode 100644
index 0000000..d565a43
--- /dev/null
+++ b/bindings/cxx/examples/gpiogetcxx.cpp
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Simplified C++ reimplementation of the gpioget tool. */
+
+#include <gpiod.hpp>
+
+#include <cstdlib>
+#include <iostream>
+
+int main(int argc, char **argv)
+{
+	if (argc < 3) {
+		::std::cerr << "usage: " << argv[0] << " <chip> <line_offset0> ..." << ::std::endl;
+		return EXIT_FAILURE;
+	}
+
+	::std::vector<unsigned int> offsets;
+
+	for (int i = 2; i < argc; i++)
+		offsets.push_back(::std::stoul(argv[i]));
+
+	::gpiod::chip chip(argv[1]);
+	auto lines = chip.get_lines(offsets);
+
+	lines.request({
+		argv[0],
+		::gpiod::line_request::DIRECTION_INPUT,
+		0
+	});
+
+	auto vals = lines.get_values();
+
+	for (auto& it: vals)
+		::std::cout << it << ' ';
+	::std::cout << ::std::endl;
+
+	return EXIT_SUCCESS;
+}
diff --git a/bindings/cxx/examples/gpioinfocxx.cpp b/bindings/cxx/examples/gpioinfocxx.cpp
new file mode 100644
index 0000000..02d69b6
--- /dev/null
+++ b/bindings/cxx/examples/gpioinfocxx.cpp
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Simplified C++ reimplementation of the gpioinfo tool. */
+
+#include <gpiod.hpp>
+
+#include <cstdlib>
+#include <iostream>
+
+int main(int argc, char **argv)
+{
+	if (argc != 1) {
+		::std::cerr << "usage: " << argv[0] << ::std::endl;
+		return EXIT_FAILURE;
+	}
+
+	for (auto& cit: ::gpiod::make_chip_iter()) {
+		::std::cout << cit.name() << " - " << cit.num_lines() << " lines:" << ::std::endl;
+
+		for (auto& lit: ::gpiod::line_iter(cit)) {
+			::std::cout << "\tline ";
+			::std::cout.width(3);
+			::std::cout << lit.offset() << ": ";
+
+			::std::cout.width(12);
+			::std::cout << (lit.name().empty() ? "unnamed" : lit.name());
+			::std::cout << " ";
+
+			::std::cout.width(12);
+			::std::cout << (lit.consumer().empty() ? "unused" : lit.consumer());
+			::std::cout << " ";
+
+			::std::cout.width(8);
+			::std::cout << (lit.direction() == ::gpiod::line::DIRECTION_INPUT ? "input" : "output");
+			::std::cout << " ";
+
+			::std::cout.width(10);
+			::std::cout << (lit.active_state() == ::gpiod::line::ACTIVE_LOW
+								? "active-low" : "active-high");
+
+			::std::cout << ::std::endl;
+		}
+	}
+
+	return EXIT_SUCCESS;
+}
diff --git a/bindings/cxx/examples/gpiomoncxx.cpp b/bindings/cxx/examples/gpiomoncxx.cpp
new file mode 100644
index 0000000..c603d5d
--- /dev/null
+++ b/bindings/cxx/examples/gpiomoncxx.cpp
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Simplified C++ reimplementation of the gpiomon tool. */
+
+#include <gpiod.hpp>
+
+#include <cstdlib>
+#include <iostream>
+
+namespace {
+
+void print_event(const ::gpiod::line_event& event)
+{
+	if (event.event_type == ::gpiod::line_event::RISING_EDGE)
+		::std::cout << " RISING EDGE";
+	else if (event.event_type == ::gpiod::line_event::FALLING_EDGE)
+		::std::cout << "FALLING EDGE";
+	else
+		throw ::std::logic_error("invalid event type");
+
+	::std::cout << " ";
+
+	::std::cout << ::std::chrono::duration_cast<::std::chrono::seconds>(event.timestamp).count();
+	::std::cout << ".";
+	::std::cout << event.timestamp.count() % 1000000000;
+
+	::std::cout << " line: " << event.source.offset();
+
+	::std::cout << ::std::endl;
+}
+
+} /* namespace */
+
+int main(int argc, char **argv)
+{
+	if (argc < 3) {
+		::std::cout << "usage: " << argv[0] << " <chip> <offset0> ..." << ::std::endl;
+		return EXIT_FAILURE;
+	}
+
+	::std::vector<unsigned int> offsets;
+	offsets.reserve(argc);
+	for (int i = 2; i < argc; i++)
+		offsets.push_back(::std::stoul(argv[i]));
+
+	::gpiod::chip chip(argv[1]);
+	auto lines = chip.get_lines(offsets);
+
+	lines.request({
+		argv[0],
+		::gpiod::line_request::EVENT_BOTH_EDGES,
+		0,
+	});
+
+	for (;;) {
+		auto events = lines.event_wait(::std::chrono::nanoseconds(1000000000));
+		if (events) {
+			for (auto& it: events)
+				print_event(it.event_read());
+		}
+	}
+
+	return EXIT_SUCCESS;
+}
diff --git a/bindings/cxx/examples/gpiosetcxx.cpp b/bindings/cxx/examples/gpiosetcxx.cpp
new file mode 100644
index 0000000..5c0b8c9
--- /dev/null
+++ b/bindings/cxx/examples/gpiosetcxx.cpp
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Simplified C++ reimplementation of the gpioset tool. */
+
+#include <gpiod.hpp>
+
+#include <cstdlib>
+#include <iostream>
+
+int main(int argc, char **argv)
+{
+	if (argc < 3) {
+		::std::cerr << "usage: " << argv[0] << " <chip> <line_offset0>=<value0> ..." << ::std::endl;
+		return EXIT_FAILURE;
+	}
+
+	::std::vector<unsigned int> offsets;
+	::std::vector<int> values;
+
+	for (int i = 2; i < argc; i++) {
+		::std::string arg(argv[i]);
+
+		size_t pos = arg.find('=');
+
+		::std::string offset(arg.substr(0, pos));
+		::std::string value(arg.substr(pos + 1, ::std::string::npos));
+
+		if (offset.empty() || value.empty())
+			throw ::std::invalid_argument("invalid argument: " + ::std::string(argv[i]));
+
+		offsets.push_back(::std::stoul(offset));
+		values.push_back(::std::stoul(value));
+	}
+
+	::gpiod::chip chip(argv[1]);
+	auto lines = chip.get_lines(offsets);
+
+	lines.request({
+		argv[0],
+		::gpiod::line_request::DIRECTION_OUTPUT,
+		0
+	}, values);
+
+	::std::cin.get();
+
+	return EXIT_SUCCESS;
+}
diff --git a/bindings/cxx/gpiod.hpp b/bindings/cxx/gpiod.hpp
new file mode 100644
index 0000000..b92597f
--- /dev/null
+++ b/bindings/cxx/gpiod.hpp
@@ -0,0 +1,970 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#ifndef __LIBGPIOD_GPIOD_CXX_HPP__
+#define __LIBGPIOD_GPIOD_CXX_HPP__
+
+#include <bitset>
+#include <chrono>
+#include <gpiod.h>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace gpiod {
+
+class line;
+class line_bulk;
+class line_event;
+class line_iter;
+class chip_iter;
+
+/**
+ * @defgroup __gpiod_cxx__ C++ bindings
+ * @{
+ */
+
+/**
+ * @brief Represents a GPIO chip.
+ *
+ * Internally this class holds a smart pointer to an open GPIO chip descriptor.
+ * Multiple objects of this class can reference the same chip. The chip is
+ * closed and all resources freed when the last reference is dropped.
+ */
+class chip
+{
+public:
+
+	/**
+	 * @brief Default constructor. Creates an empty GPIO chip object.
+	 */
+	GPIOD_API chip(void) = default;
+
+	/**
+	 * @brief Constructor. Opens the chip using chip::open.
+	 * @param device String describing the GPIO chip.
+	 * @param how Indicates how the chip should be opened.
+	 */
+	GPIOD_API chip(const ::std::string& device, int how = OPEN_LOOKUP);
+
+	/**
+	 * @brief Copy constructor. References the object held by other.
+	 * @param other Other chip object.
+	 */
+	GPIOD_API chip(const chip& other) = default;
+
+	/**
+	 * @brief Move constructor. References the object held by other.
+	 * @param other Other chip object.
+	 */
+	GPIOD_API chip(chip&& other) = default;
+
+	/**
+	 * @brief Assignment operator. References the object held by other.
+	 * @param other Other chip object.
+	 * @return Reference to this object.
+	 */
+	GPIOD_API chip& operator=(const chip& other) = default;
+
+	/**
+	 * @brief Move assignment operator. References the object held by other.
+	 * @param other Other chip object.
+	 * @return Reference to this object.
+	 */
+	GPIOD_API chip& operator=(chip&& other) = default;
+
+	/**
+	 * @brief Destructor. Unreferences the internal chip object.
+	 */
+	GPIOD_API ~chip(void) = default;
+
+	/**
+	 * @brief Open a GPIO chip.
+	 * @param device String describing the GPIO chip.
+	 * @param how Indicates how the chip should be opened.
+	 *
+	 * If the object already holds a reference to an open chip, it will be
+	 * closed and the reference reset.
+	 */
+	GPIOD_API void open(const ::std::string &device, int how = OPEN_LOOKUP);
+
+	/**
+	 * @brief Reset the internal smart pointer owned by this object.
+	 */
+	GPIOD_API void reset(void) noexcept;
+
+	/**
+	 * @brief Return the name of the chip held by this object.
+	 * @return Name of the GPIO chip.
+	 */
+	GPIOD_API ::std::string name(void) const;
+
+	/**
+	 * @brief Return the label of the chip held by this object.
+	 * @return Label of the GPIO chip.
+	 */
+	GPIOD_API ::std::string label(void) const;
+
+	/**
+	 * @brief Return the number of lines exposed by this chip.
+	 * @return Number of lines.
+	 */
+	GPIOD_API unsigned int num_lines(void) const;
+
+	/**
+	 * @brief Get the line exposed by this chip at given offset.
+	 * @param offset Offset of the line.
+	 * @return Line object.
+	 */
+	GPIOD_API line get_line(unsigned int offset) const;
+
+	/**
+	 * @brief Get the line exposed by this chip by name.
+	 * @param name Line name.
+	 * @return Line object.
+	 */
+	GPIOD_API line find_line(const ::std::string& name) const;
+
+	/**
+	 * @brief Get a set of lines exposed by this chip at given offsets.
+	 * @param offsets Vector of line offsets.
+	 * @return Set of lines held by a line_bulk object.
+	 */
+	GPIOD_API line_bulk get_lines(const ::std::vector<unsigned int>& offsets) const;
+
+	/**
+	 * @brief Get all lines exposed by this chip.
+	 * @return All lines exposed by this chip held by a line_bulk object.
+	 */
+	GPIOD_API line_bulk get_all_lines(void) const;
+
+	/**
+	 * @brief Get a set of lines exposed by this chip by their names.
+	 * @param names Vector of line names.
+	 * @return Set of lines held by a line_bulk object.
+	 */
+	GPIOD_API line_bulk find_lines(const ::std::vector<::std::string>& names) const;
+
+	/**
+	 * @brief Equality operator.
+	 * @param rhs Right-hand side of the equation.
+	 * @return True if rhs references the same chip. False otherwise.
+	 */
+	GPIOD_API bool operator==(const chip& rhs) const noexcept;
+
+	/**
+	 * @brief Inequality operator.
+	 * @param rhs Right-hand side of the equation.
+	 * @return False if rhs references the same chip. True otherwise.
+	 */
+	GPIOD_API bool operator!=(const chip& rhs) const noexcept;
+
+	/**
+	 * @brief Check if this object holds a reference to a GPIO chip.
+	 * @return True if this object references a GPIO chip, false otherwise.
+	 */
+	GPIOD_API operator bool(void) const noexcept;
+
+	/**
+	 * @brief Check if this object doesn't hold a reference to a GPIO chip.
+	 * @return False if this object references a GPIO chip, true otherwise.
+	 */
+	GPIOD_API bool operator!(void) const noexcept;
+
+	/**
+	 * @brief Affect the way in which chip::chip and chip::open will try
+	 *        to open a GPIO chip character device.
+	 */
+	enum : int {
+		OPEN_LOOKUP = 1,
+		/**< Open based on the best guess what the supplied string is. */
+		OPEN_BY_PATH,
+		/**< Assume the string is a path to the GPIO chardev. */
+		OPEN_BY_NAME,
+		/**< Assume the string is the name of the chip */
+		OPEN_BY_LABEL,
+		/**< Assume the string is the label of the GPIO chip. */
+		OPEN_BY_NUMBER,
+		/**< Assume the string is the number of the GPIO chip. */
+	};
+
+private:
+
+	chip(::gpiod_chip* chip);
+
+	void throw_if_noref(void) const;
+
+	::std::shared_ptr<::gpiod_chip> _m_chip;
+
+	friend chip_iter;
+	friend line_iter;
+};
+
+/**
+ * @brief Stores the configuration for line requests.
+ */
+struct line_request
+{
+	/**
+	 * @brief Request types.
+	 */
+	enum : int {
+		DIRECTION_AS_IS = 1,
+		/**< Request for values, don't change the direction. */
+		DIRECTION_INPUT,
+		/**< Request for reading line values. */
+		DIRECTION_OUTPUT,
+		/**< Request for driving the GPIO lines. */
+		EVENT_FALLING_EDGE,
+		/**< Listen for falling edge events. */
+		EVENT_RISING_EDGE,
+		/**< Listen for rising edge events. */
+		EVENT_BOTH_EDGES,
+		/**< Listen for all types of events. */
+	};
+
+	GPIOD_API static const ::std::bitset<32> FLAG_ACTIVE_LOW;
+	/**< Set the active state to 'low' (high is the default). */
+	GPIOD_API static const ::std::bitset<32> FLAG_OPEN_SOURCE;
+	/**< The line is an open-source port. */
+	GPIOD_API static const ::std::bitset<32> FLAG_OPEN_DRAIN;
+	/**< The line is an open-drain port. */
+
+	::std::string consumer;
+	/**< Consumer name to pass to the request. */
+	int request_type;
+	/**< Type of the request. */
+	::std::bitset<32> flags;
+	/**< Additional request flags. */
+};
+
+/**
+ * @brief Represents a single GPIO line.
+ *
+ * Internally this class holds a raw pointer to a GPIO line descriptor and a
+ * reference to the parent chip. All line resources are freed when the last
+ * reference to the parent chip is dropped.
+ */
+class line
+{
+public:
+
+	/**
+	 * @brief Default constructor. Creates an empty line object.
+	 */
+	GPIOD_API line(void);
+
+	/**
+	 * @brief Copy constructor.
+	 * @param other Other line object.
+	 */
+	GPIOD_API line(const line& other) = default;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Other line object.
+	 */
+	GPIOD_API line(line&& other) = default;
+
+	/**
+	 * @brief Assignment operator.
+	 * @param other Other line object.
+	 * @return Reference to this object.
+	 */
+	GPIOD_API line& operator=(const line& other) = default;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Other line object.
+	 * @return Reference to this object.
+	 */
+	GPIOD_API line& operator=(line&& other) = default;
+
+	/**
+	 * @brief Destructor.
+	 */
+	GPIOD_API ~line(void) = default;
+
+	/**
+	 * @brief Get the offset of this line.
+	 * @return Offet of this line.
+	 */
+	GPIOD_API unsigned int offset(void) const;
+
+	/**
+	 * @brief Get the name of this line (if any).
+	 * @return Name of this line or an empty string if it is unnamed.
+	 */
+	GPIOD_API ::std::string name(void) const;
+
+	/**
+	 * @brief Get the consumer of this line (if any).
+	 * @return Name of the consumer of this line or an empty string if it
+	 *         is unused.
+	 */
+	GPIOD_API ::std::string consumer(void) const;
+
+	/**
+	 * @brief Get current direction of this line.
+	 * @return Current direction setting.
+	 */
+	GPIOD_API int direction(void) const noexcept;
+
+	/**
+	 * @brief Get current active state of this line.
+	 * @return Current active state setting.
+	 */
+	GPIOD_API int active_state(void) const noexcept;
+
+	/**
+	 * @brief Check if this line is used by the kernel or other user space
+	 *        process.
+	 * @return True if this line is in use, false otherwise.
+	 */
+	GPIOD_API bool is_used(void) const;
+
+	/**
+	 * @brief Check if this line represents an open-drain GPIO.
+	 * @return True if the line is an open-drain GPIO, false otherwise.
+	 */
+	GPIOD_API bool is_open_drain(void) const;
+
+	/**
+	 * @brief Check if this line represents an open-source GPIO.
+	 * @return True if the line is an open-source GPIO, false otherwise.
+	 */
+	GPIOD_API bool is_open_source(void) const;
+
+	/**
+	 * @brief Request this line.
+	 * @param config Request config (see gpiod::line_request).
+	 * @param default_val Default value - only matters for OUTPUT direction.
+	 */
+	GPIOD_API void request(const line_request& config, int default_val = 0) const;
+
+	/**
+	 * @brief Release the line if it was previously requested.
+	 */
+	GPIOD_API void release(void) const;
+
+	/**
+	 * @brief Check if this user has ownership of this line.
+	 * @return True if the user has ownership of this line, false otherwise.
+	 */
+	GPIOD_API bool is_requested(void) const;
+
+	/**
+	 * @brief Read the line value.
+	 * @return Current value (0 or 1).
+	 */
+	GPIOD_API int get_value(void) const;
+
+	/**
+	 * @brief Set the value of this line.
+	 * @param val New value (0 or 1).
+	 */
+	GPIOD_API void set_value(int val) const;
+
+	/**
+	 * @brief Wait for an event on this line.
+	 * @param timeout Time to wait before returning if no event occurred.
+	 * @return True if an event occurred and can be read, false if the wait
+	 *         timed out.
+	 */
+	GPIOD_API bool event_wait(const ::std::chrono::nanoseconds& timeout) const;
+
+	/**
+	 * @brief Read a line event.
+	 * @return Line event object.
+	 */
+	GPIOD_API line_event event_read(void) const;
+
+	/**
+	 * @brief Get the event file descriptor associated with this line.
+	 * @return File descriptor number.
+	 */
+	GPIOD_API int event_get_fd(void) const;
+
+	/**
+	 * @brief Get the reference to the parent chip.
+	 * @return Reference to the parent chip object.
+	 */
+	GPIOD_API const chip& get_chip(void) const;
+
+	/**
+	 * @brief Reset the state of this object.
+	 *
+	 * This is useful when the user needs to e.g. keep the line_event object
+	 * but wants to drop the reference to the GPIO chip indirectly held by
+	 * the line being the source of the event.
+	 */
+	GPIOD_API void reset(void);
+
+	/**
+	 * @brief Check if two line objects reference the same GPIO line.
+	 * @param rhs Right-hand side of the equation.
+	 * @return True if both objects reference the same line, fale otherwise.
+	 */
+	GPIOD_API bool operator==(const line& rhs) const noexcept;
+
+	/**
+	 * @brief Check if two line objects reference different GPIO lines.
+	 * @param rhs Right-hand side of the equation.
+	 * @return False if both objects reference the same line, true otherwise.
+	 */
+	GPIOD_API bool operator!=(const line& rhs) const noexcept;
+
+	/**
+	 * @brief Check if this object holds a reference to any GPIO line.
+	 * @return True if this object references a GPIO line, false otherwise.
+	 */
+	GPIOD_API operator bool(void) const noexcept;
+
+	/**
+	 * @brief Check if this object doesn't reference any GPIO line.
+	 * @return True if this object doesn't reference any GPIO line, true
+	 *         otherwise.
+	 */
+	GPIOD_API bool operator!(void) const noexcept;
+
+	/**
+	 * @brief Possible direction settings.
+	 */
+	enum : int {
+		DIRECTION_INPUT = 1,
+		/**< Line's direction setting is input. */
+		DIRECTION_OUTPUT,
+		/**< Line's direction setting is output. */
+	};
+
+	/**
+	 * @brief Possible active state settings.
+	 */
+	enum : int {
+		ACTIVE_LOW = 1,
+		/**< Line's active state is low. */
+		ACTIVE_HIGH,
+		/**< Line's active state is high. */
+	};
+
+private:
+
+	line(::gpiod_line* line, const chip& owner);
+
+	void throw_if_null(void) const;
+
+	::gpiod_line* _m_line;
+	chip _m_chip;
+
+	friend chip;
+	friend line_bulk;
+	friend line_iter;
+};
+
+/**
+ * @brief Find a GPIO line by name. Search all GPIO chips present on the system.
+ * @param name Name of the line.
+ * @return Returns a line object - empty if the line was not found.
+ */
+GPIOD_API line find_line(const ::std::string& name);
+
+/**
+ * @brief Describes a single GPIO line event.
+ */
+struct line_event
+{
+
+	/**
+	 * @brief Possible event types.
+	 */
+	enum : int {
+		RISING_EDGE = 1,
+		/**< Rising edge event. */
+		FALLING_EDGE,
+		/**< Falling edge event. */
+	};
+
+	::std::chrono::nanoseconds timestamp;
+	/**< Best estimate of time of event occurrence in nanoseconds. */
+	int event_type;
+	/**< Type of the event that occurred. */
+	line source;
+	/**< Line object referencing the GPIO line on which the event occurred. */
+};
+
+/**
+ * @brief Represents a set of GPIO lines.
+ *
+ * Internally an object of this class stores an array of line objects
+ * owned by a single chip.
+ */
+class line_bulk
+{
+public:
+
+	/**
+	 * @brief Default constructor. Creates an empty line_bulk object.
+	 */
+	GPIOD_API line_bulk(void) = default;
+
+	/**
+	 * @brief Construct a line_bulk from a vector of lines.
+	 * @param lines Vector of gpiod::line objects.
+	 * @note All lines must be owned by the same GPIO chip.
+	 */
+	GPIOD_API line_bulk(const ::std::vector<line>& lines);
+
+	/**
+	 * @brief Copy constructor.
+	 * @param other Other line_bulk object.
+	 */
+	GPIOD_API line_bulk(const line_bulk& other) = default;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Other line_bulk object.
+	 */
+	GPIOD_API line_bulk(line_bulk&& other) = default;
+
+	/**
+	 * @brief Assignment operator.
+	 * @param other Other line_bulk object.
+	 * @return Reference to this object.
+	 */
+	GPIOD_API line_bulk& operator=(const line_bulk& other) = default;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Other line_bulk object.
+	 * @return Reference to this object.
+	 */
+	GPIOD_API line_bulk& operator=(line_bulk&& other) = default;
+
+	/**
+	 * @brief Destructor.
+	 */
+	GPIOD_API ~line_bulk(void) = default;
+
+	/**
+	 * @brief Add a line to this line_bulk object.
+	 * @param new_line Line to add.
+	 * @note The new line must be owned by the same chip as all the other
+	 *       lines already held by this line_bulk object.
+	 */
+	GPIOD_API void append(const line& new_line);
+
+	/**
+	 * @brief Get the line at given offset.
+	 * @param offset Offset of the line to get.
+	 * @return Reference to the line object.
+	 */
+	GPIOD_API line& get(unsigned int offset);
+
+	/**
+	 * @brief Get the line at given offset without bounds checking.
+	 * @param offset Offset of the line to get.
+	 * @return Reference to the line object.
+	 * @note No bounds checking is performed.
+	 */
+	GPIOD_API line& operator[](unsigned int offset);
+
+	/**
+	 * @brief Get the number of lines currently held by this object.
+	 * @return Number of elements in this line_bulk.
+	 */
+	GPIOD_API unsigned int size(void) const noexcept;
+
+	/**
+	 * @brief Check if this line_bulk doesn't hold any lines.
+	 * @return True if this object is empty, false otherwise.
+	 */
+	GPIOD_API bool empty(void) const noexcept;
+
+	/**
+	 * @brief Remove all lines from this object.
+	 */
+	GPIOD_API void clear(void);
+
+	/**
+	 * @brief Request all lines held by this object.
+	 * @param config Request config (see gpiod::line_request).
+	 * @param default_vals Vector of default values. Only relevant for
+	 *                     output direction requests.
+	 */
+	GPIOD_API void request(const line_request& config,
+			       const std::vector<int> default_vals = std::vector<int>()) const;
+
+	/**
+	 * @brief Release all lines held by this object.
+	 */
+	GPIOD_API void release(void) const;
+
+	/**
+	 * @brief Read values from all lines held by this object.
+	 * @return Vector containing line values the order of which corresponds
+	 *         with the order of lines in the internal array.
+	 */
+	GPIOD_API ::std::vector<int> get_values(void) const;
+
+	/**
+	 * @brief Set values of all lines held by this object.
+	 * @param values Vector of values to set. Must be the same size as the
+	 *        number of lines held by this line_bulk.
+	 */
+	GPIOD_API void set_values(const ::std::vector<int>& values) const;
+
+	/**
+	 * @brief Poll the set of lines for line events.
+	 * @param timeout Number of nanoseconds to wait before returning an
+	 *        empty line_bulk.
+	 * @return Returns a line_bulk object containing lines on which events
+	 *         occurred.
+	 */
+	GPIOD_API line_bulk event_wait(const ::std::chrono::nanoseconds& timeout) const;
+
+	/**
+	 * @brief Check if this object holds any lines.
+	 * @return True if this line_bulk holds at least one line, false otherwise.
+	 */
+	GPIOD_API operator bool(void) const noexcept;
+
+	/**
+	 * @brief Check if this object doesn't hold any lines.
+	 * @return True if this line_bulk is empty, false otherwise.
+	 */
+	GPIOD_API bool operator!(void) const noexcept;
+
+	/**
+	 * @brief Max number of lines that this object can hold.
+	 */
+	GPIOD_API static const unsigned int MAX_LINES;
+
+	/**
+	 * @brief Iterator for iterating over lines held by line_bulk.
+	 */
+	class iterator
+	{
+	public:
+
+		/**
+		 * @brief Default constructor. Builds an empty iterator object.
+		 */
+		GPIOD_API iterator(void) = default;
+
+		/**
+		 * @brief Copy constructor.
+		 * @param other Other line_bulk iterator.
+		 */
+		GPIOD_API iterator(const iterator& other) = default;
+
+		/**
+		 * @brief Move constructor.
+		 * @param other Other line_bulk iterator.
+		 */
+		GPIOD_API iterator(iterator&& other) = default;
+
+		/**
+		 * @brief Assignment operator.
+		 * @param other Other line_bulk iterator.
+		 * @return Reference to this iterator.
+		 */
+		GPIOD_API iterator& operator=(const iterator& other) = default;
+
+		/**
+		 * @brief Move assignment operator.
+		 * @param other Other line_bulk iterator.
+		 * @return Reference to this iterator.
+		 */
+		GPIOD_API iterator& operator=(iterator&& other) = default;
+
+		/**
+		 * @brief Destructor.
+		 */
+		GPIOD_API ~iterator(void) = default;
+
+		/**
+		 * @brief Advance the iterator by one element.
+		 * @return Reference to this iterator.
+		 */
+		GPIOD_API iterator& operator++(void);
+
+		/**
+		 * @brief Dereference current element.
+		 * @return Current GPIO line by reference.
+		 */
+		GPIOD_API const line& operator*(void) const;
+
+		/**
+		 * @brief Member access operator.
+		 * @return Current GPIO line by pointer.
+		 */
+		GPIOD_API const line* operator->(void) const;
+
+		/**
+		 * @brief Check if this operator points to the same element.
+		 * @param rhs Right-hand side of the equation.
+		 * @return True if this iterator points to the same GPIO line,
+		 *         false otherwise.
+		 */
+		GPIOD_API bool operator==(const iterator& rhs) const noexcept;
+
+		/**
+		 * @brief Check if this operator doesn't point to the same element.
+		 * @param rhs Right-hand side of the equation.
+		 * @return True if this iterator doesn't point to the same GPIO
+		 *         line, false otherwise.
+		 */
+		GPIOD_API bool operator!=(const iterator& rhs) const noexcept;
+
+	private:
+
+		iterator(const ::std::vector<line>::iterator& it);
+
+		::std::vector<line>::iterator _m_iter;
+
+		friend line_bulk;
+	};
+
+	/**
+	 * @brief Returns an iterator to the first line.
+	 * @return A line_bulk iterator.
+	 */
+	GPIOD_API iterator begin(void) noexcept;
+
+	/**
+	 * @brief Returns an iterator to the element following the last line.
+	 * @return A line_bulk iterator.
+	 */
+	GPIOD_API iterator end(void) noexcept;
+
+private:
+
+	void throw_if_empty(void) const;
+	void to_line_bulk(::gpiod_line_bulk* bulk) const;
+
+	::std::vector<line> _m_bulk;
+};
+
+/**
+ * @brief Create a new chip_iter.
+ * @return New chip iterator object pointing to the first GPIO chip on the system.
+ * @note This function is needed as we already use the default constructor of
+ *       gpiod::chip_iter as the return value of gpiod::end.
+ */
+GPIOD_API chip_iter make_chip_iter(void);
+
+/**
+ * @brief Support for range-based loops for chip iterators.
+ * @param iter A chip iterator.
+ * @return Iterator unchanged.
+ */
+GPIOD_API chip_iter begin(chip_iter iter) noexcept;
+
+/**
+ * @brief Support for range-based loops for chip iterators.
+ * @param iter A chip iterator.
+ * @return New end iterator.
+ */
+GPIOD_API chip_iter end(const chip_iter& iter) noexcept;
+
+/**
+ * @brief Allows to iterate over all GPIO chips present on the system.
+ */
+class chip_iter
+{
+public:
+
+	/**
+	 * @brief Default constructor. Creates the end iterator.
+	 */
+	GPIOD_API chip_iter(void) = default;
+
+	/**
+	 * @brief Copy constructor.
+	 * @param other Other chip_iter.
+	 */
+	GPIOD_API chip_iter(const chip_iter& other) = default;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Other chip_iter.
+	 */
+	GPIOD_API chip_iter(chip_iter&& other) = default;
+
+	/**
+	 * @brief Assignment operator.
+	 * @param other Other chip_iter.
+	 * @return Reference to this iterator.
+	 */
+	GPIOD_API chip_iter& operator=(const chip_iter& other) = default;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Other chip_iter.
+	 * @return Reference to this iterator.
+	 */
+	GPIOD_API chip_iter& operator=(chip_iter&& other) = default;
+
+	/**
+	 * @brief Destructor.
+	 */
+	GPIOD_API ~chip_iter(void) = default;
+
+	/**
+	 * @brief Advance the iterator by one element.
+	 * @return Reference to this iterator.
+	 */
+	GPIOD_API chip_iter& operator++(void);
+
+	/**
+	 * @brief Dereference current element.
+	 * @return Current GPIO chip by reference.
+	 */
+	GPIOD_API const chip& operator*(void) const;
+
+	/**
+	 * @brief Member access operator.
+	 * @return Current GPIO chip by pointer.
+	 */
+	GPIOD_API const chip* operator->(void) const;
+
+	/**
+	 * @brief Check if this operator points to the same element.
+	 * @param rhs Right-hand side of the equation.
+	 * @return True if this iterator points to the same chip_iter,
+	 *         false otherwise.
+	 */
+	GPIOD_API bool operator==(const chip_iter& rhs) const noexcept;
+
+	/**
+	 * @brief Check if this operator doesn't point to the same element.
+	 * @param rhs Right-hand side of the equation.
+	 * @return True if this iterator doesn't point to the same chip_iter,
+	 *         false otherwise.
+	 */
+	GPIOD_API bool operator!=(const chip_iter& rhs) const noexcept;
+
+private:
+
+	chip_iter(::gpiod_chip_iter* iter);
+
+	::std::shared_ptr<::gpiod_chip_iter> _m_iter;
+	chip _m_current;
+
+	friend chip_iter make_chip_iter(void);
+};
+
+/**
+ * @brief Support for range-based loops for line iterators.
+ * @param iter A line iterator.
+ * @return Iterator unchanged.
+ */
+GPIOD_API line_iter begin(line_iter iter) noexcept;
+
+/**
+ * @brief Support for range-based loops for line iterators.
+ * @param iter A line iterator.
+ * @return New end iterator.
+ */
+GPIOD_API line_iter end(const line_iter& iter) noexcept;
+
+/**
+ * @brief Allows to iterate over all lines owned by a GPIO chip.
+ */
+class line_iter
+{
+public:
+
+	/**
+	 * @brief Default constructor. Creates the end iterator.
+	 */
+	GPIOD_API line_iter(void) = default;
+
+	/**
+	 * @brief Constructor. Creates the begin iterator.
+	 * @param owner Chip owning the GPIO lines over which we want to iterate.
+	 */
+	GPIOD_API line_iter(const chip& owner);
+
+	/**
+	 * @brief Copy constructor.
+	 * @param other Other line iterator.
+	 */
+	GPIOD_API line_iter(const line_iter& other) = default;
+
+	/**
+	 * @brief Move constructor.
+	 * @param other Other line iterator.
+	 */
+	GPIOD_API line_iter(line_iter&& other) = default;
+
+	/**
+	 * @brief Assignment operator.
+	 * @param other Other line iterator.
+	 * @return Reference to this line_iter.
+	 */
+	GPIOD_API line_iter& operator=(const line_iter& other) = default;
+
+	/**
+	 * @brief Move assignment operator.
+	 * @param other Other line iterator.
+	 * @return Reference to this line_iter.
+	 */
+	GPIOD_API line_iter& operator=(line_iter&& other) = default;
+
+	/**
+	 * @brief Destructor.
+	 */
+	GPIOD_API ~line_iter(void) = default;
+
+	/**
+	 * @brief Advance the iterator by one element.
+	 * @return Reference to this iterator.
+	 */
+	GPIOD_API line_iter& operator++(void);
+
+	/**
+	 * @brief Dereference current element.
+	 * @return Current GPIO line by reference.
+	 */
+	GPIOD_API const line& operator*(void) const;
+
+	/**
+	 * @brief Member access operator.
+	 * @return Current GPIO line by pointer.
+	 */
+	GPIOD_API const line* operator->(void) const;
+
+	/**
+	 * @brief Check if this operator points to the same element.
+	 * @param rhs Right-hand side of the equation.
+	 * @return True if this iterator points to the same line_iter,
+	 *         false otherwise.
+	 */
+	GPIOD_API bool operator==(const line_iter& rhs) const noexcept;
+
+	/**
+	 * @brief Check if this operator doesn't point to the same element.
+	 * @param rhs Right-hand side of the equation.
+	 * @return True if this iterator doesn't point to the same line_iter,
+	 *         false otherwise.
+	 */
+	GPIOD_API bool operator!=(const line_iter& rhs) const noexcept;
+
+private:
+
+	::std::shared_ptr<::gpiod_line_iter> _m_iter;
+	line _m_current;
+};
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_GPIOD_CXX_HPP__ */
diff --git a/bindings/cxx/iter.cpp b/bindings/cxx/iter.cpp
new file mode 100644
index 0000000..8c0b9ba
--- /dev/null
+++ b/bindings/cxx/iter.cpp
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#include <gpiod.hpp>
+#include <system_error>
+
+namespace gpiod {
+
+namespace {
+
+void chip_iter_deleter(::gpiod_chip_iter* iter)
+{
+	::gpiod_chip_iter_free_noclose(iter);
+}
+
+void line_iter_deleter(::gpiod_line_iter* iter)
+{
+	::gpiod_line_iter_free(iter);
+}
+
+::gpiod_line_iter* make_line_iter(::gpiod_chip* chip)
+{
+	::gpiod_line_iter* iter;
+
+	iter = ::gpiod_line_iter_new(chip);
+	if (!iter)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "error creating GPIO line iterator");
+
+	return iter;
+}
+
+} /* namespace */
+
+chip_iter make_chip_iter(void)
+{
+	::gpiod_chip_iter* iter = ::gpiod_chip_iter_new();
+	if (!iter)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "error creating GPIO chip iterator");
+
+	return ::std::move(chip_iter(iter));
+}
+
+bool chip_iter::operator==(const chip_iter& rhs) const noexcept
+{
+	return this->_m_current == rhs._m_current;
+}
+
+bool chip_iter::operator!=(const chip_iter& rhs) const noexcept
+{
+	return this->_m_current != rhs._m_current;
+}
+
+chip_iter::chip_iter(::gpiod_chip_iter *iter)
+	: _m_iter(iter, chip_iter_deleter)
+{
+	::gpiod_chip* first = ::gpiod_chip_iter_next_noclose(this->_m_iter.get());
+
+	if (first != nullptr)
+		this->_m_current = ::std::move(chip(first));
+}
+
+chip_iter& chip_iter::operator++(void)
+{
+	::gpiod_chip* next = ::gpiod_chip_iter_next_noclose(this->_m_iter.get());
+
+	this->_m_current = next ? chip(next) : chip();
+
+	return *this;
+}
+
+const chip& chip_iter::operator*(void) const
+{
+	return this->_m_current;
+}
+
+const chip* chip_iter::operator->(void) const
+{
+	return ::std::addressof(this->_m_current);
+}
+
+chip_iter begin(chip_iter iter) noexcept
+{
+	return iter;
+}
+
+chip_iter end(const chip_iter&) noexcept
+{
+	return ::std::move(chip_iter());
+}
+
+line_iter begin(line_iter iter) noexcept
+{
+	return iter;
+}
+
+line_iter end(const line_iter&) noexcept
+{
+	return ::std::move(line_iter());
+}
+
+line_iter::line_iter(const chip& owner)
+	: _m_iter(make_line_iter(owner._m_chip.get()), line_iter_deleter),
+	  _m_current(line(::gpiod_line_iter_next(this->_m_iter.get()), owner))
+{
+
+}
+
+line_iter& line_iter::operator++(void)
+{
+	::gpiod_line* next = ::gpiod_line_iter_next(this->_m_iter.get());
+
+	this->_m_current = next ? line(next, this->_m_current._m_chip) : line();
+
+	return *this;
+}
+
+const line& line_iter::operator*(void) const
+{
+	return this->_m_current;
+}
+
+const line* line_iter::operator->(void) const
+{
+	return ::std::addressof(this->_m_current);
+}
+
+bool line_iter::operator==(const line_iter& rhs) const noexcept
+{
+	return this->_m_current._m_line == rhs._m_current._m_line;
+}
+
+bool line_iter::operator!=(const line_iter& rhs) const noexcept
+{
+	return this->_m_current._m_line != rhs._m_current._m_line;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/libgpiodcxx.pc.in b/bindings/cxx/libgpiodcxx.pc.in
new file mode 100644
index 0000000..9d227c9
--- /dev/null
+++ b/bindings/cxx/libgpiodcxx.pc.in
@@ -0,0 +1,11 @@
+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.cpp b/bindings/cxx/line.cpp
new file mode 100644
index 0000000..f8d0e62
--- /dev/null
+++ b/bindings/cxx/line.cpp
@@ -0,0 +1,242 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#include <gpiod.hpp>
+#include <system_error>
+
+namespace gpiod {
+
+line::line(void)
+	: _m_line(nullptr),
+	  _m_chip()
+{
+
+}
+
+line::line(::gpiod_line* line, const chip& owner)
+	: _m_line(line),
+	  _m_chip(owner)
+{
+
+}
+
+unsigned int line::offset(void) const
+{
+	this->throw_if_null();
+
+	return ::gpiod_line_offset(this->_m_line);
+}
+
+::std::string line::name(void) const
+{
+	this->throw_if_null();
+
+	const char* name = ::gpiod_line_name(this->_m_line);
+
+	return ::std::move(name ? ::std::string(name) : ::std::string());
+}
+
+::std::string line::consumer(void) const
+{
+	this->throw_if_null();
+
+	const char* consumer = ::gpiod_line_consumer(this->_m_line);
+
+	return ::std::move(consumer ? ::std::string(consumer) : ::std::string());
+}
+
+int line::direction(void) const noexcept
+{
+	this->throw_if_null();
+
+	int dir = ::gpiod_line_direction(this->_m_line);
+
+	return dir == GPIOD_LINE_DIRECTION_INPUT ? DIRECTION_INPUT : DIRECTION_OUTPUT;
+}
+
+int line::active_state(void) const noexcept
+{
+	this->throw_if_null();
+
+	int active = ::gpiod_line_active_state(this->_m_line);
+
+	return active == GPIOD_LINE_ACTIVE_STATE_HIGH ? ACTIVE_HIGH : ACTIVE_LOW;
+}
+
+bool line::is_used(void) const
+{
+	this->throw_if_null();
+
+	return ::gpiod_line_is_used(this->_m_line);
+}
+
+bool line::is_open_drain(void) const
+{
+	this->throw_if_null();
+
+	return ::gpiod_line_is_open_drain(this->_m_line);
+}
+
+bool line::is_open_source(void) const
+{
+	this->throw_if_null();
+
+	return ::gpiod_line_is_open_source(this->_m_line);
+}
+
+void line::request(const line_request& config, int default_val) const
+{
+	this->throw_if_null();
+
+	line_bulk bulk({ *this });
+
+	bulk.request(config, { default_val });
+}
+
+void line::release(void) const
+{
+	this->throw_if_null();
+
+	line_bulk bulk({ *this });
+
+	bulk.release();
+}
+
+bool line::is_requested(void) const
+{
+	this->throw_if_null();
+
+	return ::gpiod_line_is_requested(this->_m_line);
+}
+
+/*
+ * REVISIT: Check the performance of get/set_value & event_wait compared to
+ * the C API. Creating a line_bulk object involves a memory allocation every
+ * time this method if called. If the performance is significantly lower,
+ * switch to calling the C functions for setting/getting line values and
+ * polling for events on single lines directly.
+ */
+
+int line::get_value(void) const
+{
+	this->throw_if_null();
+
+	line_bulk bulk({ *this });
+
+	return bulk.get_values()[0];
+}
+
+void line::set_value(int val) const
+{
+	this->throw_if_null();
+
+	line_bulk bulk({ *this });
+
+	bulk.set_values({ val });
+}
+
+bool line::event_wait(const ::std::chrono::nanoseconds& timeout) const
+{
+	this->throw_if_null();
+
+	line_bulk bulk({ *this });
+
+	line_bulk event_bulk = bulk.event_wait(timeout);
+
+	return ::std::move(event_bulk);
+}
+
+line_event line::event_read(void) const
+{
+	this->throw_if_null();
+
+	::gpiod_line_event event_buf;
+	line_event event;
+	int rv;
+
+	rv = ::gpiod_line_event_read(this->_m_line, ::std::addressof(event_buf));
+	if (rv < 0)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "error reading line event");
+
+	if (event_buf.event_type == GPIOD_LINE_EVENT_RISING_EDGE)
+		event.event_type = line_event::RISING_EDGE;
+	else if (event_buf.event_type == GPIOD_LINE_EVENT_FALLING_EDGE)
+		event.event_type = line_event::FALLING_EDGE;
+
+	event.timestamp = ::std::chrono::nanoseconds(
+				event_buf.ts.tv_nsec + (event_buf.ts.tv_sec * 1000000000));
+
+	event.source = *this;
+
+	return ::std::move(event);
+}
+
+int line::event_get_fd(void) const
+{
+	this->throw_if_null();
+
+	int ret = ::gpiod_line_event_get_fd(this->_m_line);
+
+	if (ret < 0)
+		::std::system_error(errno, ::std::system_category(),
+				    "unable to get the line event file descriptor");
+
+	return ret;
+}
+
+const chip& line::get_chip(void) const
+{
+	return this->_m_chip;
+}
+
+void line::reset(void)
+{
+	this->_m_line = nullptr;
+	this->_m_chip.reset();
+}
+
+bool line::operator==(const line& rhs) const noexcept
+{
+	return this->_m_line == rhs._m_line;
+}
+
+bool line::operator!=(const line& rhs) const noexcept
+{
+	return this->_m_line != rhs._m_line;
+}
+
+line::operator bool(void) const noexcept
+{
+	return this->_m_line != nullptr;
+}
+
+bool line::operator!(void) const noexcept
+{
+	return this->_m_line == nullptr;
+}
+
+void line::throw_if_null(void) const
+{
+	if (!this->_m_line)
+		throw ::std::logic_error("object not holding a GPIO line handle");
+}
+
+line find_line(const ::std::string& name)
+{
+	line ret;
+
+	for (auto& it: make_chip_iter()) {
+		ret = it.find_line(name);
+		if (ret)
+			break;
+	}
+
+	return ::std::move(ret);
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/line_bulk.cpp b/bindings/cxx/line_bulk.cpp
new file mode 100644
index 0000000..3006b65
--- /dev/null
+++ b/bindings/cxx/line_bulk.cpp
@@ -0,0 +1,269 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#include <gpiod.hpp>
+#include <map>
+#include <system_error>
+
+namespace gpiod {
+
+const ::std::bitset<32> line_request::FLAG_ACTIVE_LOW("001");
+const ::std::bitset<32> line_request::FLAG_OPEN_SOURCE("010");
+const ::std::bitset<32> line_request::FLAG_OPEN_DRAIN("100");
+
+namespace {
+
+const ::std::map<int, int> reqtype_mapping = {
+	{ line_request::DIRECTION_AS_IS,	GPIOD_LINE_REQUEST_DIRECTION_AS_IS, },
+	{ line_request::DIRECTION_INPUT,	GPIOD_LINE_REQUEST_DIRECTION_INPUT, },
+	{ line_request::DIRECTION_OUTPUT,	GPIOD_LINE_REQUEST_DIRECTION_OUTPUT, },
+	{ line_request::EVENT_FALLING_EDGE,	GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE, },
+	{ line_request::EVENT_RISING_EDGE,	GPIOD_LINE_REQUEST_EVENT_RISING_EDGE, },
+	{ line_request::EVENT_BOTH_EDGES,	GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES, },
+};
+
+struct bitset_cmp
+{
+	bool operator()(const ::std::bitset<32>& lhs, const ::std::bitset<32>& rhs) const
+	{
+		return lhs.to_ulong() < rhs.to_ulong();
+	}
+};
+
+const ::std::map<::std::bitset<32>, int, bitset_cmp> reqflag_mapping = {
+	{ line_request::FLAG_ACTIVE_LOW,	GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW, },
+	{ line_request::FLAG_OPEN_DRAIN,	GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN, },
+	{ line_request::FLAG_OPEN_SOURCE,	GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE, },
+};
+
+} /* namespace */
+
+const unsigned int line_bulk::MAX_LINES = GPIOD_LINE_BULK_MAX_LINES;
+
+line_bulk::line_bulk(const ::std::vector<line>& lines)
+	: _m_bulk()
+{
+	this->_m_bulk.reserve(lines.size());
+
+	for (auto& it: lines)
+		this->append(it);
+}
+
+void line_bulk::append(const line& new_line)
+{
+	if (!new_line)
+		throw ::std::logic_error("line_bulk cannot hold empty line objects");
+
+	if (this->_m_bulk.size() >= MAX_LINES)
+		throw ::std::logic_error("maximum number of lines reached");
+
+	if (this->_m_bulk.size() >= 1 && this->_m_bulk.begin()->get_chip() != new_line.get_chip())
+		throw std::logic_error("line_bulk cannot hold GPIO lines from different chips");
+
+	this->_m_bulk.push_back(new_line);
+}
+
+line& line_bulk::get(unsigned int offset)
+{
+	return this->_m_bulk.at(offset);
+}
+
+line& line_bulk::operator[](unsigned int offset)
+{
+	return this->_m_bulk[offset];
+}
+
+unsigned int line_bulk::size(void) const noexcept
+{
+	return this->_m_bulk.size();
+}
+
+bool line_bulk::empty(void) const noexcept
+{
+	return this->_m_bulk.empty();
+}
+
+void line_bulk::clear(void)
+{
+	this->_m_bulk.clear();
+}
+
+void line_bulk::request(const line_request& config, const std::vector<int> default_vals) const
+{
+	this->throw_if_empty();
+
+	if (!default_vals.empty() && this->size() != default_vals.size())
+		throw ::std::invalid_argument("the number of default values must correspond with the number of lines");
+
+	::gpiod_line_request_config conf;
+	::gpiod_line_bulk bulk;
+	int rv;
+
+	this->to_line_bulk(::std::addressof(bulk));
+
+	conf.consumer = config.consumer.c_str();
+	conf.request_type = reqtype_mapping.at(config.request_type);
+	conf.flags = 0;
+
+	for (auto& it: reqflag_mapping) {
+		if ((it.first & config.flags).to_ulong())
+			conf.flags |= it.second;
+	}
+
+	rv = ::gpiod_line_request_bulk(::std::addressof(bulk),
+				       ::std::addressof(conf),
+				       default_vals.empty() ? NULL : default_vals.data());
+	if (rv)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "error requesting GPIO lines");
+}
+
+void line_bulk::release(void) const
+{
+	this->throw_if_empty();
+
+	::gpiod_line_bulk bulk;
+
+	this->to_line_bulk(::std::addressof(bulk));
+
+	::gpiod_line_release_bulk(::std::addressof(bulk));
+}
+
+::std::vector<int> line_bulk::get_values(void) const
+{
+	this->throw_if_empty();
+
+	::std::vector<int> values;
+	::gpiod_line_bulk bulk;
+	int rv;
+
+	this->to_line_bulk(::std::addressof(bulk));
+	values.resize(this->_m_bulk.size());
+
+	rv = ::gpiod_line_get_value_bulk(::std::addressof(bulk), values.data());
+	if (rv)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "error reading GPIO line values");
+
+	return ::std::move(values);
+}
+
+void line_bulk::set_values(const ::std::vector<int>& values) const
+{
+	this->throw_if_empty();
+
+	if (values.size() != this->_m_bulk.size())
+		throw ::std::invalid_argument("the size of values array must correspond with the number of lines");
+
+	::gpiod_line_bulk bulk;
+	int rv;
+
+	this->to_line_bulk(::std::addressof(bulk));
+
+	rv = ::gpiod_line_set_value_bulk(::std::addressof(bulk), values.data());
+	if (rv)
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "error setting GPIO line values");
+}
+
+line_bulk line_bulk::event_wait(const ::std::chrono::nanoseconds& timeout) const
+{
+	this->throw_if_empty();
+
+	::gpiod_line_bulk bulk, event_bulk;
+	::timespec ts;
+	line_bulk ret;
+	int rv;
+
+	this->to_line_bulk(::std::addressof(bulk));
+
+	::gpiod_line_bulk_init(::std::addressof(event_bulk));
+
+	ts.tv_sec = timeout.count() / 1000000000ULL;
+	ts.tv_nsec = timeout.count() % 1000000000ULL;
+
+	rv = ::gpiod_line_event_wait_bulk(::std::addressof(bulk),
+					  ::std::addressof(ts),
+					  ::std::addressof(event_bulk));
+	if (rv < 0) {
+		throw ::std::system_error(errno, ::std::system_category(),
+					  "error polling for events");
+	} else if (rv > 0) {
+		for (unsigned int i = 0; i < event_bulk.num_lines; i++)
+			ret.append(line(event_bulk.lines[i], this->_m_bulk[i].get_chip()));
+	}
+
+	return ::std::move(ret);
+}
+
+line_bulk::operator bool(void) const noexcept
+{
+	return !this->_m_bulk.empty();
+}
+
+bool line_bulk::operator!(void) const noexcept
+{
+	return this->_m_bulk.empty();
+}
+
+line_bulk::iterator::iterator(const ::std::vector<line>::iterator& it)
+	: _m_iter(it)
+{
+
+}
+
+line_bulk::iterator& line_bulk::iterator::operator++(void)
+{
+	this->_m_iter++;
+
+	return *this;
+}
+
+const line& line_bulk::iterator::operator*(void) const
+{
+	return *this->_m_iter;
+}
+
+const line* line_bulk::iterator::operator->(void) const
+{
+	return this->_m_iter.operator->();
+}
+
+bool line_bulk::iterator::operator==(const iterator& rhs) const noexcept
+{
+	return this->_m_iter == rhs._m_iter;
+}
+
+bool line_bulk::iterator::operator!=(const iterator& rhs) const noexcept
+{
+	return this->_m_iter != rhs._m_iter;
+}
+
+line_bulk::iterator line_bulk::begin(void) noexcept
+{
+	return ::std::move(line_bulk::iterator(this->_m_bulk.begin()));
+}
+
+line_bulk::iterator line_bulk::end(void) noexcept
+{
+	return ::std::move(line_bulk::iterator(this->_m_bulk.end()));
+}
+
+void line_bulk::throw_if_empty(void) const
+{
+	if (this->_m_bulk.empty())
+		throw std::logic_error("line_bulk not holding any GPIO lines");
+}
+
+void line_bulk::to_line_bulk(::gpiod_line_bulk *bulk) const
+{
+	::gpiod_line_bulk_init(bulk);
+	for (auto& it: this->_m_bulk)
+		::gpiod_line_bulk_add(bulk, it._m_line);
+}
+
+} /* namespace gpiod */
diff --git a/bindings/python/Makefile.am b/bindings/python/Makefile.am
new file mode 100644
index 0000000..2c00c6e
--- /dev/null
+++ b/bindings/python/Makefile.am
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+SUBDIRS = . examples
+
+pyexec_LTLIBRARIES = gpiod.la
+
+gpiod_la_SOURCES = gpiodmodule.c
+
+gpiod_la_CFLAGS = -I$(top_srcdir)/include/
+gpiod_la_CFLAGS += -Wall -Wextra -g $(PYTHON_CPPFLAGS)
+gpiod_la_LDFLAGS = -module -avoid-version
+gpiod_la_LIBADD = $(top_builddir)/lib/libgpiod.la $(PYTHON_LIBS)
diff --git a/bindings/python/examples/Makefile.am b/bindings/python/examples/Makefile.am
new file mode 100644
index 0000000..2745718
--- /dev/null
+++ b/bindings/python/examples/Makefile.am
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+EXTRA_DIST =	gpiod_tests.py \
+		gpiodetect.py \
+		gpiofind.py \
+		gpioget.py \
+		gpioinfo.py \
+		gpiomon.py \
+		gpioset.py
diff --git a/bindings/python/examples/gpiod_tests.py b/bindings/python/examples/gpiod_tests.py
new file mode 100755
index 0000000..910613a
--- /dev/null
+++ b/bindings/python/examples/gpiod_tests.py
@@ -0,0 +1,437 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+'''Misc tests of libgpiod python bindings.
+
+These tests assume that at least one dummy gpiochip is present in the
+system and that it's detected as gpiochip0.
+'''
+
+import gpiod
+import select
+
+test_cases = []
+
+def add_test(name, func):
+    global test_cases
+
+    test_cases.append((name, func))
+
+def fire_line_event(chip, offset, rising):
+    path = '/sys/kernel/debug/gpio-mockup-event/{}/{}'.format(chip, offset)
+    with open(path, 'w') as fd:
+        fd.write('{}'.format(1 if rising else 0))
+
+def print_event(event):
+    print('type: {}'.format('rising' if event.type == gpiod.LineEvent.RISING_EDGE else 'falling'))
+    print('timestamp: {}.{}'.format(event.sec, event.nsec))
+    print('source line offset: {}'.format(event.source.offset()))
+
+def chip_open_default_lookup():
+    by_name = gpiod.Chip('gpiochip0')
+    by_path = gpiod.Chip('/dev/gpiochip0')
+    by_label = gpiod.Chip('gpio-mockup-A')
+    by_number = gpiod.Chip('0')
+    print('All good')
+    by_name.close()
+    by_path.close()
+    by_label.close()
+    by_number.close()
+
+add_test('Open a GPIO chip using different lookup modes', chip_open_default_lookup)
+
+def chip_open_different_modes():
+    chip = gpiod.Chip('/dev/gpiochip0', gpiod.Chip.OPEN_BY_PATH)
+    chip.close()
+    chip = gpiod.Chip('gpiochip0', gpiod.Chip.OPEN_BY_NAME)
+    chip.close()
+    chip = gpiod.Chip('gpio-mockup-A', gpiod.Chip.OPEN_BY_LABEL)
+    chip.close()
+    chip = gpiod.Chip('0', gpiod.Chip.OPEN_BY_NUMBER)
+    chip.close()
+    print('All good')
+
+add_test('Open a GPIO chip using different modes', chip_open_different_modes)
+
+def chip_open_nonexistent():
+    try:
+        chip = gpiod.Chip('/nonexistent_gpiochip')
+    except OSError as ex:
+        print('Exception raised as expected: {}'.format(ex))
+        return
+
+    assert False, 'OSError expected'
+
+add_test('Try to open a nonexistent GPIO chip', chip_open_nonexistent)
+
+def chip_open_no_args():
+    try:
+        chip = gpiod.Chip()
+    except TypeError:
+        print('Error as expected')
+        return
+
+    assert False, 'TypeError expected'
+
+add_test('Open a GPIO chip without arguments', chip_open_no_args)
+
+def chip_use_after_close():
+    chip = gpiod.Chip('gpiochip0')
+    line = chip.get_line(2)
+    chip.close()
+
+    try:
+        chip.name()
+    except ValueError as ex:
+        print('Error as expected: {}'.format(ex))
+
+    try:
+        line = chip.get_line(3)
+    except ValueError as ex:
+        print('Error as expected: {}'.format(ex))
+        return
+
+    assert False, 'ValueError expected'
+
+add_test('Use a GPIO chip after closing it', chip_use_after_close)
+
+def chip_with_statement():
+    with gpiod.Chip('gpiochip0') as chip:
+        print('Chip name in controlled execution: {}'.format(chip.name()))
+        line = chip.get_line(3)
+        print('Got line from chip in controlled execution: {}'.format(line.name()))
+
+add_test('Use a GPIO chip in controlled execution', chip_with_statement)
+
+def chip_info():
+    chip = gpiod.Chip('gpiochip0')
+    print('name: {}'.format(chip.name()))
+    print('label: {}'.format(chip.label()))
+    print('lines: {}'.format(chip.num_lines()))
+    chip.close()
+
+add_test('Print chip info', chip_info)
+
+def print_chip():
+    chip = gpiod.Chip('/dev/gpiochip0')
+    print(chip)
+    chip.close()
+
+add_test('Print chip object', print_chip)
+
+def create_chip_without_arguments():
+    try:
+        chip = gpiod.Chip()
+    except TypeError as ex:
+        print('Exception raised as expected: {}'.format(ex))
+        return
+
+    assert False, 'TypeError expected'
+
+add_test('Create chip object without arguments', create_chip_without_arguments)
+
+def create_line_object():
+    try:
+        line = gpiod.Line()
+    except NotImplementedError:
+        print('Error as expected')
+        return
+
+    assert False, 'NotImplementedError expected'
+
+add_test('Create a line object - should fail', create_line_object)
+
+def print_line():
+    chip = gpiod.Chip('gpio-mockup-A')
+    line = chip.get_line(3)
+    print(line)
+    chip.close()
+
+add_test('Print line object', print_line)
+
+def find_line():
+    line = gpiod.find_line('gpio-mockup-A-4')
+    print('found line - offset: {}'.format(line.offset()))
+    line.owner().close()
+
+add_test('Find line globally', find_line)
+
+def create_empty_line_bulk():
+    try:
+        lines = gpiod.LineBulk()
+    except TypeError:
+        print('Error as expected')
+        return
+
+    assert False, 'TypeError expected'
+
+add_test('Create a line bulk object - should fail', create_empty_line_bulk)
+
+def get_lines():
+    chip = gpiod.Chip('gpio-mockup-A')
+
+    print('getting four lines from chip')
+    lines = chip.get_lines([2, 4, 5, 7])
+
+    print('Retrieved lines:')
+    for line in lines:
+        print(line)
+
+    chip.close()
+
+add_test('Get lines from chip', get_lines)
+
+def get_all_lines():
+    chip = gpiod.Chip('gpio-mockup-A')
+
+    print('Retrieving all lines from chip')
+    lines = chip.get_all_lines()
+
+    print('Retrieved lines:')
+    for line in lines:
+        print(line)
+
+    chip.close()
+
+add_test('Get all lines from chip', get_all_lines)
+
+def find_lines():
+    chip = gpiod.Chip('gpiochip0')
+
+    print('looking up lines by names')
+    lines = chip.find_lines(['gpio-mockup-A-3', 'gpio-mockup-A-4', 'gpio-mockup-A-7'])
+
+    print('Retrieved lines:')
+    for line in lines:
+        print(line)
+
+    chip.close()
+
+add_test('Find multiple lines by name', find_lines)
+
+def find_lines_one_bad():
+    chip = gpiod.Chip('gpiochip0')
+
+    print('looking up lines by names')
+    try:
+        lines = chip.find_lines(['gpio-mockup-A-3', 'nonexistent', 'gpio-mockup-A-7'])
+    except TypeError as ex:
+        print('Error as expected')
+        return
+
+    assert False, 'TypeError expected'
+
+add_test('Find multiple lines but one line name is non-existent', find_lines_one_bad)
+
+def create_line_bulk_from_lines():
+    chip = gpiod.Chip('gpio-mockup-A')
+    line1 = chip.get_line(2)
+    line2 = chip.get_line(4)
+    line3 = chip.get_line(6)
+    lines = gpiod.LineBulk([line1, line2, line3])
+    print('Created LineBulk:')
+    print(lines)
+    chip.close()
+
+add_test('Create a LineBulk from a list of lines', create_line_bulk_from_lines)
+
+def line_bulk_to_list():
+    chip = gpiod.Chip('gpio-mockup-A')
+    lines = chip.get_lines((1, 2, 3))
+    print(lines.to_list())
+    chip.close()
+
+add_test('Convert a LineBulk to a list', line_bulk_to_list)
+
+def line_flags():
+    chip = gpiod.Chip('gpiochip0')
+    line = chip.get_line(3)
+
+    print('line is used: {}'.format(line.is_used()))
+    print('line is requested: {}'.format(line.is_requested()))
+
+    print('requesting line')
+    line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_DIR_OUT,
+                 flags=(gpiod.LINE_REQ_FLAG_OPEN_DRAIN | gpiod.LINE_REQ_FLAG_ACTIVE_LOW))
+
+    print('line is used: {}'.format(line.is_used()))
+    print('line is open drain: {}'.format(line.is_open_drain()))
+    print('line is open source: {}'.format(line.is_open_source()))
+    print('line is requested: {}'.format(line.is_requested()))
+    print('line is active-low: {}'.format(
+            "True" if line.active_state() == gpiod.Line.ACTIVE_LOW else "False"))
+
+    chip.close()
+
+add_test('Check various line flags', line_flags)
+
+def get_value_single_line():
+    chip = gpiod.Chip('gpio-mockup-A')
+    line = chip.get_line(2)
+    line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_DIR_IN)
+    print('line value: {}'.format(line.get_value()))
+    chip.close()
+
+add_test('Get value - single line', get_value_single_line)
+
+def set_value_single_line():
+    chip = gpiod.Chip('gpiochip0')
+    line = chip.get_line(3)
+    line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_DIR_IN)
+
+    print('line value before: {}'.format(line.get_value()))
+    line.release()
+    line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_DIR_OUT)
+    line.set_value(1)
+    line.release()
+    line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_DIR_IN)
+    print('line value after: {}'.format(line.get_value()))
+
+    chip.close()
+
+add_test('Set value - single line', set_value_single_line)
+
+def request_line_with_default_values():
+    chip = gpiod.Chip('gpiochip0')
+    line = chip.get_line(3)
+
+    print('requesting a single line with a default value')
+    line.request(consumer='gpiod_test.py', type=gpiod.LINE_REQ_DIR_OUT, default_vals=[ 1 ])
+
+    print('line value after request: {}'.format(line.get_value()))
+
+    chip.close()
+
+add_test('Request line with default value', request_line_with_default_values)
+
+def request_multiple_lines_with_default_values():
+    chip = gpiod.Chip('gpiochip0')
+    lines = chip.get_lines(( 1, 2, 3, 4, 5 ))
+
+    print('requesting lines with default values')
+    lines.request(consumer='gpiod_test.py', type=gpiod.LINE_REQ_DIR_OUT, default_vals=( 1, 0, 1, 0, 1 ))
+
+    print('line values after request: {}'.format(lines.get_values()))
+
+    chip.close()
+
+add_test('Request multiple lines with default values', request_multiple_lines_with_default_values)
+
+def request_line_incorrect_number_of_def_vals():
+    with gpiod.Chip('gpiochip0') as chip:
+        lines = chip.get_lines(( 1, 2, 3, 4, 5 ))
+
+        print('requesting lines with incorrect number of default values')
+        try:
+            lines.request(consumer='gpiod_test.py',
+                          type=gpiod.LINE_REQ_DIR_OUT,
+                          default_vals=( 1, 0, 1, 0 ))
+        except TypeError:
+            print('TypeError raised as expected')
+            return
+
+        assert False, 'TypeError expected'
+
+add_test('Request with incorrect number of default values', request_line_incorrect_number_of_def_vals)
+
+def line_event_single_line():
+    chip = gpiod.Chip('gpiochip0')
+    line = chip.get_line(1)
+
+    print('requesting line for events')
+    line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_EV_BOTH_EDGES)
+
+    print('generating a line event')
+    fire_line_event('gpiochip0', 1, True)
+    assert line.event_wait(sec=1), 'Expected a line event to occur'
+
+    print('event received')
+    event = line.event_read()
+    print_event(event)
+
+    chip.close()
+
+add_test('Monitor a single line for events', line_event_single_line)
+
+def line_event_multiple_lines():
+    chip = gpiod.Chip('gpiochip0')
+    lines = chip.get_lines((1, 2, 3, 4, 5))
+
+    print('requesting lines for events')
+    lines.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_EV_BOTH_EDGES)
+
+    print('generating two line events')
+    fire_line_event('gpiochip0', 1, True)
+    fire_line_event('gpiochip0', 2, True)
+
+    events = lines.event_wait(sec=1)
+    assert events is not None and len(events) == 2, 'Expected to receive two line events'
+
+    print('events received:')
+    for line in events:
+        event = line.event_read()
+        print_event(event)
+
+    chip.close()
+
+add_test('Monitor multiple lines for events', line_event_multiple_lines)
+
+def line_event_poll_fd():
+    chip = gpiod.Chip('gpiochip0')
+    lines = chip.get_lines((1, 2, 3, 4, 5, 6))
+    print('requesting lines for events')
+    lines.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_EV_BOTH_EDGES)
+
+    print('generating three line events')
+    fire_line_event('gpiochip0', 2, True)
+    fire_line_event('gpiochip0', 3, False)
+    fire_line_event('gpiochip0', 5, True)
+
+    print('retrieving the file descriptors')
+    inputs = []
+    fd_mapping = {}
+    for line in lines:
+        inputs.append(line.event_get_fd())
+        fd_mapping[line.event_get_fd()] = line
+
+    readable, writable, exceptional = select.select(inputs, [], inputs, 1.0)
+    assert len(readable) == 3, 'Expected to receive three line events'
+
+    print('events received:')
+    for fd in readable:
+        line = fd_mapping[fd]
+        event = line.event_read()
+        print_event(event)
+
+    chip.close()
+
+add_test('Monitor multiple lines using their file descriptors', line_event_poll_fd)
+
+def line_event_repr():
+    with gpiod.Chip('gpiochip0') as chip:
+        line = chip.get_line(1)
+
+        print('requesting line for events')
+        line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_EV_BOTH_EDGES)
+
+        print('generating a line event')
+        fire_line_event('gpiochip0', 1, True)
+        assert line.event_wait(sec=1), 'Expected a line event to occur'
+
+        print('event received: {}'.format(line.event_read()))
+
+add_test('Line event string repr', line_event_repr)
+
+print('API version is {}'.format(gpiod.version_string()))
+
+for name, func in test_cases:
+    print('==============================================')
+    print('{}:'.format(name))
+    print()
+    func()
diff --git a/bindings/python/examples/gpiodetect.py b/bindings/python/examples/gpiodetect.py
new file mode 100755
index 0000000..f539f04
--- /dev/null
+++ b/bindings/python/examples/gpiodetect.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+'''Reimplementation of the gpiodetect tool in Python.'''
+
+import gpiod
+
+for chip in gpiod.ChipIter():
+    print('{} [{}] ({} lines)'.format(chip.name(),
+                                      chip.label(),
+                                      chip.num_lines()))
+    chip.close()
diff --git a/bindings/python/examples/gpiofind.py b/bindings/python/examples/gpiofind.py
new file mode 100755
index 0000000..30b8e2b
--- /dev/null
+++ b/bindings/python/examples/gpiofind.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+'''Reimplementation of the gpiofind tool in Python.'''
+
+import gpiod
+import sys
+
+line = gpiod.find_line(sys.argv[1])
+if line is None:
+    sys.exit(1)
+
+print('{} {}'.format(line.owner().name(), line.offset()))
+line.owner().close()
diff --git a/bindings/python/examples/gpioget.py b/bindings/python/examples/gpioget.py
new file mode 100755
index 0000000..bc7645b
--- /dev/null
+++ b/bindings/python/examples/gpioget.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+'''Simplified reimplementation of the gpioget tool in Python.'''
+
+import gpiod
+import sys
+
+if len(sys.argv) < 3:
+    raise TypeError('usage: gpioget.py <gpiochip> <offset1> <offset2> ...')
+
+with gpiod.Chip(sys.argv[1]) as chip:
+    offsets = []
+    for off in sys.argv[2:]:
+        offsets.append(int(off))
+
+    lines = chip.get_lines(offsets)
+    lines.request(consumer=sys.argv[0], type=gpiod.LINE_REQ_DIR_IN)
+    vals = lines.get_values()
+
+    for val in vals:
+        print(val, end=' ')
+    print()
diff --git a/bindings/python/examples/gpioinfo.py b/bindings/python/examples/gpioinfo.py
new file mode 100755
index 0000000..f9d91bf
--- /dev/null
+++ b/bindings/python/examples/gpioinfo.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+'''Simplified reimplementation of the gpioinfo tool in Python.'''
+
+import gpiod
+
+for chip in gpiod.ChipIter():
+    print('{} - {} lines:'.format(chip.name(), chip.num_lines()))
+
+    for line in gpiod.LineIter(chip):
+        offset = line.offset()
+        name = line.name()
+        consumer = line.consumer()
+        direction = line.direction()
+        active_state = line.active_state()
+
+        print('\tline {:>3}: {:>18} {:>12} {:>8} {:>10}'.format(
+                offset,
+                'unnamed' if name is None else name,
+                'unused' if consumer is None else consumer,
+                'input' if direction == gpiod.Line.DIRECTION_INPUT else 'output',
+                'active-low' if active_state == gpiod.Line.ACTIVE_LOW else 'active-high'))
+
+    chip.close()
diff --git a/bindings/python/examples/gpiomon.py b/bindings/python/examples/gpiomon.py
new file mode 100755
index 0000000..9cb71da
--- /dev/null
+++ b/bindings/python/examples/gpiomon.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+'''Simplified reimplementation of the gpiomon tool in Python.'''
+
+import gpiod
+import sys
+
+def print_event(event):
+    if event.type == gpiod.LineEvent.RISING_EDGE:
+        print(' RISING EDGE', end='')
+    elif event.type == gpiod.LineEvent.FALLING_EDGE:
+        print('FALLING EDGE', end='')
+    else:
+        raise TypeError('Invalid event type')
+
+    print(' {}.{} line: {}'.format(event.sec, event.nsec, event.source.offset()))
+
+if len(sys.argv) < 3:
+    raise TypeError('usage: gpiomon.py <gpiochip> <offset1> <offset2> ...')
+
+with gpiod.Chip(sys.argv[1]) as chip:
+    offsets = []
+    for off in sys.argv[2:]:
+        offsets.append(int(off))
+
+    lines = chip.get_lines(offsets)
+    lines.request(consumer=sys.argv[0], type=gpiod.LINE_REQ_EV_BOTH_EDGES)
+
+    try:
+        while True:
+            ev_lines = lines.event_wait(sec=1)
+            if ev_lines:
+                for line in ev_lines:
+                    event = line.event_read()
+                    print_event(event)
+    except KeyboardInterrupt:
+        sys.exit(130)
diff --git a/bindings/python/examples/gpioset.py b/bindings/python/examples/gpioset.py
new file mode 100755
index 0000000..70fbcc9
--- /dev/null
+++ b/bindings/python/examples/gpioset.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+'''Simplified reimplementation of the gpioset tool in Python.'''
+
+import gpiod
+import sys
+
+if len(sys.argv) < 3:
+    raise TypeError('usage: gpioset.py <gpiochip> <offset1>=<value1> ...')
+
+with gpiod.Chip(sys.argv[1]) as chip:
+    offsets = []
+    values = []
+    for arg in sys.argv[2:]:
+        arg = arg.split('=')
+        offsets.append(int(arg[0]))
+        values.append(int(arg[1]))
+
+    lines = chip.get_lines(offsets)
+    lines.request(consumer=sys.argv[0], type=gpiod.LINE_REQ_DIR_OUT)
+    vals = lines.set_values(values)
diff --git a/bindings/python/gpiodmodule.c b/bindings/python/gpiodmodule.c
new file mode 100644
index 0000000..ec28553
--- /dev/null
+++ b/bindings/python/gpiodmodule.c
@@ -0,0 +1,2391 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#include <Python.h>
+#include <gpiod.h>
+
+typedef struct {
+	PyObject_HEAD
+	struct gpiod_chip *chip;
+} gpiod_ChipObject;
+
+typedef struct {
+	PyObject_HEAD
+	struct gpiod_line *line;
+	gpiod_ChipObject *owner;
+} gpiod_LineObject;
+
+typedef struct {
+	PyObject_HEAD
+	struct gpiod_line_event event;
+	gpiod_LineObject *source;
+} gpiod_LineEventObject;
+
+typedef struct {
+	PyObject_HEAD
+	PyObject **lines;
+	Py_ssize_t num_lines;
+	Py_ssize_t iter_idx;
+} gpiod_LineBulkObject;
+
+typedef struct {
+	PyObject_HEAD
+	struct gpiod_chip_iter *iter;
+} gpiod_ChipIterObject;
+
+typedef struct {
+	PyObject_HEAD
+	struct gpiod_line_iter *iter;
+	gpiod_ChipObject *owner;
+} gpiod_LineIterObject;
+
+static gpiod_LineBulkObject *gpiod_LineToLineBulk(gpiod_LineObject *line);
+static gpiod_LineObject *gpiod_MakeLineObject(gpiod_ChipObject *owner,
+					      struct gpiod_line *line);
+
+enum {
+	gpiod_LINE_REQ_DIR_AS_IS = 1,
+	gpiod_LINE_REQ_DIR_IN,
+	gpiod_LINE_REQ_DIR_OUT,
+	gpiod_LINE_REQ_EV_FALLING_EDGE,
+	gpiod_LINE_REQ_EV_RISING_EDGE,
+	gpiod_LINE_REQ_EV_BOTH_EDGES,
+};
+
+enum {
+	gpiod_LINE_REQ_FLAG_OPEN_DRAIN		= GPIOD_BIT(0),
+	gpiod_LINE_REQ_FLAG_OPEN_SOURCE		= GPIOD_BIT(1),
+	gpiod_LINE_REQ_FLAG_ACTIVE_LOW		= GPIOD_BIT(2),
+};
+
+enum {
+	gpiod_DIRECTION_INPUT = 1,
+	gpiod_DIRECTION_OUTPUT,
+};
+
+enum {
+	gpiod_ACTIVE_HIGH = 1,
+	gpiod_ACTIVE_LOW,
+};
+
+enum {
+	gpiod_RISING_EDGE = 1,
+	gpiod_FALLING_EDGE,
+};
+
+static bool gpiod_ChipIsClosed(gpiod_ChipObject *chip)
+{
+	if (!chip->chip) {
+		PyErr_SetString(PyExc_ValueError,
+				"I/O operation on closed file");
+		return true;
+	}
+
+	return false;
+}
+
+static PyObject *gpiod_CallMethodPyArgs(PyObject *obj, const char *method,
+					PyObject *args, PyObject *kwds)
+{
+	PyObject *callable, *ret;
+
+	callable = PyObject_GetAttrString((PyObject *)obj, method);
+	if (!callable)
+		return NULL;
+
+	ret = PyObject_Call(callable, args, kwds);
+	Py_DECREF(callable);
+
+	return ret;
+}
+
+static int gpiod_LineEvent_init(void)
+{
+	PyErr_SetString(PyExc_NotImplementedError,
+			"Only gpiod.Line can create new LineEvent objects.");
+	return -1;
+}
+
+static void gpiod_LineEvent_dealloc(gpiod_LineEventObject *self)
+{
+	if (self->source)
+		Py_DECREF(self->source);
+
+	PyObject_Del(self);
+}
+
+PyDoc_STRVAR(gpiod_LineEvent_get_type_doc,
+"Event type of this line event (integer).");
+
+PyObject *gpiod_LineEvent_get_type(gpiod_LineEventObject *self)
+{
+	int rv;
+
+	if (self->event.event_type == GPIOD_LINE_EVENT_RISING_EDGE)
+		rv = gpiod_RISING_EDGE;
+	else
+		rv = gpiod_FALLING_EDGE;
+
+	return Py_BuildValue("I", rv);
+}
+
+PyDoc_STRVAR(gpiod_LineEvent_get_sec_doc,
+"Seconds value of the line event timestamp (integer).");
+
+PyObject *gpiod_LineEvent_get_sec(gpiod_LineEventObject *self)
+{
+	return Py_BuildValue("I", self->event.ts.tv_sec);
+}
+
+PyDoc_STRVAR(gpiod_LineEvent_get_nsec_doc,
+"Nanoseconds value of the line event timestamp (integer).");
+
+PyObject *gpiod_LineEvent_get_nsec(gpiod_LineEventObject *self)
+{
+	return Py_BuildValue("I", self->event.ts.tv_nsec);
+}
+
+PyDoc_STRVAR(gpiod_LineEvent_get_source_doc,
+"Line object representing the GPIO line on which this event\n"
+"occurred (gpiod.Line object).");
+
+gpiod_LineObject *gpiod_LineEvent_get_source(gpiod_LineEventObject *self)
+{
+	Py_INCREF(self->source);
+	return self->source;
+}
+
+static PyGetSetDef gpiod_LineEvent_getset[] = {
+	{
+		.name = "type",
+		.get = (getter)gpiod_LineEvent_get_type,
+		.doc = gpiod_LineEvent_get_type_doc,
+	},
+	{
+		.name = "sec",
+		.get = (getter)gpiod_LineEvent_get_sec,
+		.doc = gpiod_LineEvent_get_sec_doc,
+	},
+	{
+		.name = "nsec",
+		.get = (getter)gpiod_LineEvent_get_nsec,
+		.doc = gpiod_LineEvent_get_nsec_doc,
+	},
+	{
+		.name = "source",
+		.get = (getter)gpiod_LineEvent_get_source,
+		.doc = gpiod_LineEvent_get_source_doc,
+	},
+	{ }
+};
+
+static PyObject *gpiod_LineEvent_repr(gpiod_LineEventObject *self)
+{
+	PyObject *line_repr, *ret;
+	const char *edge;
+
+	if (self->event.event_type == GPIOD_LINE_EVENT_RISING_EDGE)
+		edge = "RISING EDGE";
+	else
+		edge = "FALLING EDGE";
+
+	line_repr = PyObject_CallMethod((PyObject *)self->source,
+					"__repr__", "");
+
+	ret = PyUnicode_FromFormat("'%s (%ld.%ld) source(%S)'",
+				   edge, self->event.ts.tv_sec,
+				   self->event.ts.tv_nsec, line_repr);
+	Py_DECREF(line_repr);
+
+	return ret;
+}
+
+PyDoc_STRVAR(gpiod_LineEventType_doc,
+"Represents a single GPIO line event. This object is immutable and can only\n"
+"be created by an instance of gpiod.Line.");
+
+static PyTypeObject gpiod_LineEventType = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.LineEvent",
+	.tp_basicsize = sizeof(gpiod_LineEventObject),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = gpiod_LineEventType_doc,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)gpiod_LineEvent_init,
+	.tp_dealloc = (destructor)gpiod_LineEvent_dealloc,
+	.tp_getset = gpiod_LineEvent_getset,
+	.tp_repr = (reprfunc)gpiod_LineEvent_repr,
+};
+
+static int gpiod_Line_init(void)
+{
+	PyErr_SetString(PyExc_NotImplementedError,
+			"Only gpiod.Chip can create new Line objects.");
+	return -1;
+}
+
+static void gpiod_Line_dealloc(gpiod_LineObject *self)
+{
+	if (self->owner)
+		Py_DECREF(self->owner);
+
+	PyObject_Del(self);
+}
+
+PyDoc_STRVAR(gpiod_Line_owner_doc,
+"owner() -> Chip object owning the line\n"
+"\n"
+"Get the GPIO chip owning this line.");
+
+static PyObject *gpiod_Line_owner(gpiod_LineObject *self)
+{
+	Py_INCREF(self->owner);
+	return (PyObject *)self->owner;
+}
+
+PyDoc_STRVAR(gpiod_Line_offset_doc,
+"offset() -> integer\n"
+"\n"
+"Get the offset of the GPIO line.");
+
+static PyObject *gpiod_Line_offset(gpiod_LineObject *self)
+{
+	if (gpiod_ChipIsClosed(self->owner))
+		return NULL;
+
+	return Py_BuildValue("I", gpiod_line_offset(self->line));
+}
+
+PyDoc_STRVAR(gpiod_Line_name_doc,
+"name() -> string\n"
+"\n"
+"Get the name of the GPIO line.");
+
+static PyObject *gpiod_Line_name(gpiod_LineObject *self)
+{
+	const char *name;
+
+	if (gpiod_ChipIsClosed(self->owner))
+		return NULL;
+
+	name = gpiod_line_name(self->line);
+	if (name)
+		return PyUnicode_FromFormat("%s", name);
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(gpiod_Line_consumer_doc,
+"consumer() -> string\n"
+"\n"
+"Get the consumer string of the GPIO line.");
+
+static PyObject *gpiod_Line_consumer(gpiod_LineObject *self)
+{
+	const char *consumer;
+
+	if (gpiod_ChipIsClosed(self->owner))
+		return NULL;
+
+	consumer = gpiod_line_consumer(self->line);
+	if (consumer)
+		return PyUnicode_FromFormat("%s", consumer);
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(gpiod_Line_direction_doc,
+"direction() -> integer\n"
+"\n"
+"Get the direction setting of this GPIO line.");
+
+static PyObject *gpiod_Line_direction(gpiod_LineObject *self)
+{
+	PyObject *ret;
+	int dir;
+
+	if (gpiod_ChipIsClosed(self->owner))
+		return NULL;
+
+	dir = gpiod_line_direction(self->line);
+
+	if (dir == GPIOD_LINE_DIRECTION_INPUT)
+		ret = Py_BuildValue("I", gpiod_DIRECTION_INPUT);
+	else
+		ret = Py_BuildValue("I", gpiod_DIRECTION_OUTPUT);
+
+	return ret;
+}
+
+PyDoc_STRVAR(gpiod_Line_active_state_doc,
+"active_state() -> integer\n"
+"\n"
+"Get the active state setting of this GPIO line.");
+
+static PyObject *gpiod_Line_active_state(gpiod_LineObject *self)
+{
+	PyObject *ret;
+	int active;
+
+	if (gpiod_ChipIsClosed(self->owner))
+		return NULL;
+
+	active = gpiod_line_active_state(self->line);
+
+	if (active == GPIOD_LINE_ACTIVE_STATE_HIGH)
+		ret = Py_BuildValue("I", gpiod_ACTIVE_HIGH);
+	else
+		ret = Py_BuildValue("I", gpiod_ACTIVE_LOW);
+
+	return ret;
+}
+
+PyDoc_STRVAR(gpiod_Line_is_used_doc,
+"is_used() -> boolean\n"
+"\n"
+"Check if this line is used by the kernel or other user space process.");
+
+static PyObject *gpiod_Line_is_used(gpiod_LineObject *self)
+{
+	if (gpiod_ChipIsClosed(self->owner))
+		return NULL;
+
+	if (gpiod_line_is_used(self->line))
+		Py_RETURN_TRUE;
+
+	Py_RETURN_FALSE;
+}
+
+PyDoc_STRVAR(gpiod_Line_is_open_drain_doc,
+"is_open_drain() -> boolean\n"
+"\n"
+"Check if this line represents an open-drain GPIO.");
+
+static PyObject *gpiod_Line_is_open_drain(gpiod_LineObject *self)
+{
+	if (gpiod_ChipIsClosed(self->owner))
+		return NULL;
+
+	if (gpiod_line_is_open_drain(self->line))
+		Py_RETURN_TRUE;
+
+	Py_RETURN_FALSE;
+}
+
+PyDoc_STRVAR(gpiod_Line_is_open_source_doc,
+"is_open_source() -> boolean\n"
+"\n"
+"Check if this line represents an open-source GPIO.");
+
+static PyObject *gpiod_Line_is_open_source(gpiod_LineObject *self)
+{
+	if (gpiod_ChipIsClosed(self->owner))
+		return NULL;
+
+	if (gpiod_line_is_open_source(self->line))
+		Py_RETURN_TRUE;
+
+	Py_RETURN_FALSE;
+}
+
+PyDoc_STRVAR(gpiod_Line_request_doc,
+"request(consumer[, type[, flags[, default_vals]]]) -> None\n"
+"\n"
+"Request this GPIO line.\n"
+"\n"
+"  consumer\n"
+"    Name of the consumer.\n"
+"  type\n"
+"    Type of the request.\n"
+"  flags\n"
+"    Other configuration flags.\n"
+"  default_val\n"
+"    Default value of this line."
+"\n"
+"Note: default_vals argument (sequence of default values passed down to\n"
+"LineBulk.request()) is still supported for backward compatibility but is\n"
+"now deprecated when requesting single lines.");
+
+static PyObject *gpiod_Line_request(gpiod_LineObject *self,
+				    PyObject *args, PyObject *kwds)
+{
+	PyObject *ret, *def_val, *def_vals;
+	gpiod_LineBulkObject *bulk_obj;
+	int rv;
+
+	def_val = PyDict_GetItemString(kwds, "default_val");
+	def_vals = PyDict_GetItemString(kwds, "default_vals");
+
+	if (def_val && def_vals) {
+		PyErr_SetString(PyExc_TypeError,
+				"Cannot pass both default_val and default_vals arguments at the same time");
+		return NULL;
+	}
+
+	if (def_val) {
+		/*
+		 * If default_val was passed as a single value, we wrap it
+		 * in a tuple and add it to the kwds dictionary to be passed
+		 * down to LineBulk.request(). We also remove the 'default_val'
+		 * entry from kwds.
+		 *
+		 * I'm not sure if it's allowed to modify the kwds dictionary
+		 * but it doesn't seem to cause any problems. If it does then
+		 * we can simply copy the dictionary before calling
+		 * LineBulk.request().
+		 */
+		rv = PyDict_DelItemString(kwds, "default_val");
+		if (rv)
+			return NULL;
+
+		def_vals = Py_BuildValue("(O)", def_val);
+		if (!def_vals)
+			return NULL;
+
+		rv = PyDict_SetItemString(kwds, "default_vals", def_vals);
+		if (rv) {
+			Py_DECREF(def_vals);
+			return NULL;
+		}
+	}
+
+	bulk_obj = gpiod_LineToLineBulk(self);
+	if (!bulk_obj)
+		return NULL;
+
+	ret = gpiod_CallMethodPyArgs((PyObject *)bulk_obj,
+				     "request", args, kwds);
+	Py_DECREF(bulk_obj);
+
+	return ret;
+}
+
+PyDoc_STRVAR(gpiod_Line_is_requested_doc,
+"is_requested() -> boolean\n"
+"\n"
+"Check if this user has ownership of this line.");
+
+static PyObject *gpiod_Line_is_requested(gpiod_LineObject *self)
+{
+	if (gpiod_ChipIsClosed(self->owner))
+		return NULL;
+
+	if (gpiod_line_is_requested(self->line))
+		Py_RETURN_TRUE;
+
+	Py_RETURN_FALSE;
+}
+
+PyDoc_STRVAR(gpiod_Line_get_value_doc,
+"get_value() -> integer\n"
+"\n"
+"Read the current value of this GPIO line.");
+
+static PyObject *gpiod_Line_get_value(gpiod_LineObject *self)
+{
+	gpiod_LineBulkObject *bulk_obj;
+	PyObject *vals, *ret;
+
+	bulk_obj = gpiod_LineToLineBulk(self);
+	if (!bulk_obj)
+		return NULL;
+
+	vals = PyObject_CallMethod((PyObject *)bulk_obj, "get_values", "");
+	Py_DECREF(bulk_obj);
+	if (!vals)
+		return NULL;
+
+	ret = PyList_GetItem(vals, 0);
+	Py_INCREF(ret);
+	Py_DECREF(vals);
+
+	return ret;
+}
+
+PyDoc_STRVAR(gpiod_Line_set_value_doc,
+"set_value(value) -> None\n"
+"\n"
+"Set the value of this GPIO line.\n"
+"\n"
+"  value\n"
+"    New value (integer)");
+
+static PyObject *gpiod_Line_set_value(gpiod_LineObject *self, PyObject *args)
+{
+	gpiod_LineBulkObject *bulk_obj;
+	PyObject *val, *vals, *ret;
+	int rv;
+
+	rv = PyArg_ParseTuple(args, "O", &val);
+	if (!rv)
+		return NULL;
+
+	bulk_obj = gpiod_LineToLineBulk(self);
+	if (!bulk_obj)
+		return NULL;
+
+	vals = Py_BuildValue("((O))", val);
+	if (!vals) {
+		Py_DECREF(bulk_obj);
+		return NULL;
+	}
+
+	ret = PyObject_CallMethod((PyObject *)bulk_obj,
+				  "set_values", "O", vals);
+	Py_DECREF(bulk_obj);
+	Py_DECREF(vals);
+
+	return ret;
+}
+
+PyDoc_STRVAR(gpiod_Line_release_doc,
+"release() -> None\n"
+"\n"
+"Release this GPIO line.");
+
+static PyObject *gpiod_Line_release(gpiod_LineObject *self)
+{
+	gpiod_LineBulkObject *bulk_obj;
+	PyObject *ret;
+
+	bulk_obj = gpiod_LineToLineBulk(self);
+	if (!bulk_obj)
+		return NULL;
+
+	ret = PyObject_CallMethod((PyObject *)bulk_obj, "release", "");
+	Py_DECREF(bulk_obj);
+
+	return ret;
+}
+
+PyDoc_STRVAR(gpiod_Line_event_wait_doc,
+"event_wait([sec[ ,nsec]]) -> boolean\n"
+"\n"
+"Wait for a line event to occur on this GPIO line.\n"
+"\n"
+"  sec\n"
+"    Number of seconds to wait before timeout.\n"
+"  nsec\n"
+"    Number of nanoseconds to wait before timeout.\n"
+"\n"
+"Returns True if an event occurred on this line before timeout. False\n"
+"otherwise.");
+
+static PyObject *gpiod_Line_event_wait(gpiod_LineObject *self,
+				       PyObject *args, PyObject *kwds)
+{
+	gpiod_LineBulkObject *bulk_obj;
+	PyObject *events;
+
+	bulk_obj = gpiod_LineToLineBulk(self);
+	if (!bulk_obj)
+		return NULL;
+
+	events = gpiod_CallMethodPyArgs((PyObject *)bulk_obj,
+					"event_wait", args, kwds);
+	Py_DECREF(bulk_obj);
+	if (!events)
+		return NULL;
+
+	if (events == Py_None) {
+		Py_DECREF(Py_None);
+		Py_RETURN_FALSE;
+	}
+
+	Py_DECREF(events);
+	Py_RETURN_TRUE;
+}
+
+PyDoc_STRVAR(gpiod_Line_event_read_doc,
+"event_read() -> gpiod.LineEvent object\n"
+"\n"
+"Read a single line event from this GPIO line object.");
+
+static gpiod_LineEventObject *gpiod_Line_event_read(gpiod_LineObject *self)
+{
+	gpiod_LineEventObject *ret;
+	int rv;
+
+	if (gpiod_ChipIsClosed(self->owner))
+		return NULL;
+
+	ret = PyObject_New(gpiod_LineEventObject, &gpiod_LineEventType);
+	if (!ret)
+		return NULL;
+
+	ret->source = NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	rv = gpiod_line_event_read(self->line, &ret->event);
+	Py_END_ALLOW_THREADS;
+	if (rv) {
+		PyErr_SetFromErrno(PyExc_OSError);
+		Py_DECREF(ret);
+		return NULL;
+	}
+
+	Py_INCREF(self);
+	ret->source = self;
+
+	return ret;
+}
+
+PyDoc_STRVAR(gpiod_Line_event_get_fd_doc,
+"event_get_fd() -> integer\n"
+"\n"
+"Get the event file descriptor number associated with this line.");
+
+static PyObject *gpiod_Line_event_get_fd(gpiod_LineObject *self)
+{
+	int fd;
+
+	if (gpiod_ChipIsClosed(self->owner))
+		return NULL;
+
+	fd = gpiod_line_event_get_fd(self->line);
+	if (fd < 0) {
+		PyErr_SetFromErrno(PyExc_OSError);
+		return NULL;
+	}
+
+	return PyLong_FromLong(fd);
+}
+
+static PyObject *gpiod_Line_repr(gpiod_LineObject *self)
+{
+	PyObject *chip_name, *ret;
+	const char *line_name;
+
+	if (gpiod_ChipIsClosed(self->owner))
+		return NULL;
+
+	chip_name = PyObject_CallMethod((PyObject *)self->owner, "name", "");
+	if (!chip_name)
+		return NULL;
+
+	line_name = gpiod_line_name(self->line);
+
+	ret = PyUnicode_FromFormat("'%S:%u /%s/'", chip_name,
+				   gpiod_line_offset(self->line),
+				   line_name ?: "unnamed");
+	Py_DECREF(chip_name);
+	return ret;
+}
+
+static PyMethodDef gpiod_Line_methods[] = {
+	{
+		.ml_name = "owner",
+		.ml_meth = (PyCFunction)gpiod_Line_owner,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_owner_doc,
+	},
+	{
+		.ml_name = "offset",
+		.ml_meth = (PyCFunction)gpiod_Line_offset,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_offset_doc,
+	},
+	{
+		.ml_name = "name",
+		.ml_meth = (PyCFunction)gpiod_Line_name,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_name_doc,
+	},
+	{
+		.ml_name = "consumer",
+		.ml_meth = (PyCFunction)gpiod_Line_consumer,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_consumer_doc,
+	},
+	{
+		.ml_name = "direction",
+		.ml_meth = (PyCFunction)gpiod_Line_direction,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_direction_doc,
+	},
+	{
+		.ml_name = "active_state",
+		.ml_meth = (PyCFunction)gpiod_Line_active_state,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_active_state_doc,
+	},
+	{
+		.ml_name = "is_used",
+		.ml_meth = (PyCFunction)gpiod_Line_is_used,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_is_used_doc,
+	},
+	{
+		.ml_name = "is_open_drain",
+		.ml_meth = (PyCFunction)gpiod_Line_is_open_drain,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_is_open_drain_doc,
+	},
+	{
+		.ml_name = "is_open_source",
+		.ml_meth = (PyCFunction)gpiod_Line_is_open_source,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_is_open_source_doc,
+	},
+	{
+		.ml_name = "request",
+		.ml_meth = (PyCFunction)gpiod_Line_request,
+		.ml_flags = METH_VARARGS | METH_KEYWORDS,
+		.ml_doc = gpiod_Line_request_doc,
+	},
+	{
+		.ml_name = "is_requested",
+		.ml_meth = (PyCFunction)gpiod_Line_is_requested,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_is_requested_doc,
+	},
+	{
+		.ml_name = "get_value",
+		.ml_meth = (PyCFunction)gpiod_Line_get_value,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_get_value_doc,
+	},
+	{
+		.ml_name = "set_value",
+		.ml_meth = (PyCFunction)gpiod_Line_set_value,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = gpiod_Line_set_value_doc,
+	},
+	{
+		.ml_name = "release",
+		.ml_meth = (PyCFunction)gpiod_Line_release,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_release_doc,
+	},
+	{
+		.ml_name = "event_wait",
+		.ml_meth = (PyCFunction)gpiod_Line_event_wait,
+		.ml_flags = METH_VARARGS | METH_KEYWORDS,
+		.ml_doc = gpiod_Line_event_wait_doc,
+	},
+	{
+		.ml_name = "event_read",
+		.ml_meth = (PyCFunction)gpiod_Line_event_read,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_event_read_doc,
+	},
+	{
+		.ml_name = "event_get_fd",
+		.ml_meth = (PyCFunction)gpiod_Line_event_get_fd,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Line_event_get_fd_doc,
+	},
+	{ }
+};
+
+PyDoc_STRVAR(gpiod_LineType_doc,
+"Represents a GPIO line.\n"
+"\n"
+"The lifetime of this object is managed by the chip that owns it. Once\n"
+"the corresponding gpiod.Chip is closed, a gpiod.Line object must not be\n"
+"used.\n"
+"\n"
+"Line objects can only be created by the owning chip.");
+
+static PyTypeObject gpiod_LineType = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.Line",
+	.tp_basicsize = sizeof(gpiod_LineObject),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = gpiod_LineType_doc,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)gpiod_Line_init,
+	.tp_dealloc = (destructor)gpiod_Line_dealloc,
+	.tp_repr = (reprfunc)gpiod_Line_repr,
+	.tp_methods = gpiod_Line_methods,
+};
+
+static bool gpiod_LineBulkOwnerIsClosed(gpiod_LineBulkObject *self)
+{
+	gpiod_LineObject *line = (gpiod_LineObject *)self->lines[0];
+
+	return gpiod_ChipIsClosed(line->owner);
+}
+
+static int gpiod_LineBulk_init(gpiod_LineBulkObject *self, PyObject *args)
+{
+	PyObject *lines, *iter, *next;
+	Py_ssize_t i;
+	int rv;
+
+	rv = PyArg_ParseTuple(args, "O", &lines);
+	if (!rv)
+		return -1;
+
+	self->num_lines = PyObject_Size(lines);
+	if (self->num_lines < 1) {
+		PyErr_SetString(PyExc_TypeError,
+				"Argument must be a non-empty sequence");
+		return -1;
+	}
+	if (self->num_lines > GPIOD_LINE_BULK_MAX_LINES) {
+		PyErr_SetString(PyExc_TypeError,
+				"Too many objects in the sequence");
+		return -1;
+	}
+
+	self->lines = PyMem_Calloc(self->num_lines, sizeof(PyObject *));
+	if (!self->lines) {
+		PyErr_SetString(PyExc_MemoryError, "Out of memory");
+		return -1;
+	}
+
+	iter = PyObject_GetIter(lines);
+	if (!iter) {
+		PyMem_Free(self->lines);
+		return -1;
+	}
+
+	for (i = 0;;) {
+		next = PyIter_Next(iter);
+		if (!next) {
+			Py_DECREF(iter);
+			break;
+		}
+
+		if (next->ob_type != &gpiod_LineType) {
+			PyErr_SetString(PyExc_TypeError,
+					"Argument must be a sequence of GPIO lines");
+			Py_DECREF(next);
+			Py_DECREF(iter);
+			goto errout;
+		}
+
+		self->lines[i++] = next;
+	}
+
+	self->iter_idx = -1;
+
+	return 0;
+
+errout:
+
+	if (i > 0) {
+		for (--i; i >= 0; i--)
+			Py_DECREF(self->lines[i]);
+	}
+	PyMem_Free(self->lines);
+	self->lines = NULL;
+
+	return -1;
+}
+
+static void gpiod_LineBulk_dealloc(gpiod_LineBulkObject *self)
+{
+	Py_ssize_t i;
+
+	if (!self->lines)
+		return;
+
+	for (i = 0; i < self->num_lines; i++)
+		Py_DECREF(self->lines[i]);
+
+	PyMem_Free(self->lines);
+	PyObject_Del(self);
+}
+
+static PyObject *gpiod_LineBulk_iternext(gpiod_LineBulkObject *self)
+{
+	if (self->iter_idx < 0) {
+		self->iter_idx = 0; /* First element */
+	} else if (self->iter_idx >= self->num_lines) {
+		self->iter_idx = -1;
+		return NULL; /* Last element */
+	}
+
+	Py_INCREF(self->lines[self->iter_idx]);
+	return self->lines[self->iter_idx++];
+}
+
+PyDoc_STRVAR(gpiod_LineBulk_to_list_doc,
+"to_list() -> list of gpiod.Line objects\n"
+"\n"
+"Convert this LineBulk to a list");
+
+static PyObject *gpiod_LineBulk_to_list(gpiod_LineBulkObject *self)
+{
+	PyObject *list;
+	Py_ssize_t i;
+	int rv;
+
+	list = PyList_New(self->num_lines);
+	if (!list)
+		return NULL;
+
+	for (i = 0; i < self->num_lines; i++) {
+		Py_INCREF(self->lines[i]);
+		rv = PyList_SetItem(list, i, self->lines[i]);
+		if (rv < 0) {
+			Py_DECREF(list);
+			return NULL;
+		}
+	}
+
+	return list;
+}
+
+static void gpiod_LineBulkObjToCLineBulk(gpiod_LineBulkObject *bulk_obj,
+					 struct gpiod_line_bulk *bulk)
+{
+	gpiod_LineObject *line_obj;
+	Py_ssize_t i;
+
+	gpiod_line_bulk_init(bulk);
+
+	for (i = 0; i < bulk_obj->num_lines; i++) {
+		line_obj = (gpiod_LineObject *)bulk_obj->lines[i];
+		gpiod_line_bulk_add(bulk, line_obj->line);
+	}
+}
+
+static void gpiod_MakeRequestConfig(struct gpiod_line_request_config *conf,
+				    const char *consumer,
+				    int request_type, int flags)
+{
+	memset(conf, 0, sizeof(*conf));
+
+	conf->consumer = consumer;
+
+	switch (request_type) {
+	case gpiod_LINE_REQ_DIR_IN:
+		conf->request_type = GPIOD_LINE_REQUEST_DIRECTION_INPUT;
+		break;
+	case gpiod_LINE_REQ_DIR_OUT:
+		conf->request_type = GPIOD_LINE_REQUEST_DIRECTION_OUTPUT;
+		break;
+	case gpiod_LINE_REQ_EV_FALLING_EDGE:
+		conf->request_type = GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE;
+		break;
+	case gpiod_LINE_REQ_EV_RISING_EDGE:
+		conf->request_type = GPIOD_LINE_REQUEST_EVENT_RISING_EDGE;
+		break;
+	case gpiod_LINE_REQ_EV_BOTH_EDGES:
+		conf->request_type = GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES;
+		break;
+	case gpiod_LINE_REQ_DIR_AS_IS:
+	default:
+		conf->request_type = GPIOD_LINE_REQUEST_DIRECTION_AS_IS;
+		break;
+	}
+
+	if (flags & gpiod_LINE_REQ_FLAG_OPEN_DRAIN)
+		conf->flags |= GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN;
+	if (flags & gpiod_LINE_REQ_FLAG_OPEN_SOURCE)
+		conf->flags |= GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE;
+	if (flags & gpiod_LINE_REQ_FLAG_ACTIVE_LOW)
+		conf->flags |= GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW;
+}
+
+PyDoc_STRVAR(gpiod_LineBulk_request_doc,
+"request(consumer[, type[, flags[, default_vals]]]) -> None\n"
+"\n"
+"Request all lines held by this LineBulk object.\n"
+"\n"
+"  consumer\n"
+"    Name of the consumer.\n"
+"  type\n"
+"    Type of the request.\n"
+"  flags\n"
+"    Other configuration flags.\n"
+"  default_vals\n"
+"    List of default values.\n");
+
+static PyObject *gpiod_LineBulk_request(gpiod_LineBulkObject *self,
+					PyObject *args, PyObject *kwds)
+{
+	static char *kwlist[] = { "consumer",
+				  "type",
+				  "flags",
+				  "default_vals",
+				  NULL };
+
+	int rv, type = gpiod_LINE_REQ_DIR_AS_IS, flags = 0,
+	    default_vals[GPIOD_LINE_BULK_MAX_LINES], val;
+	PyObject *def_vals_obj = NULL, *iter, *next;
+	struct gpiod_line_request_config conf;
+	struct gpiod_line_bulk bulk;
+	Py_ssize_t num_def_vals;
+	char *consumer = NULL;
+	Py_ssize_t i;
+
+	if (gpiod_LineBulkOwnerIsClosed(self))
+		return NULL;
+
+	rv = PyArg_ParseTupleAndKeywords(args, kwds, "s|iiO", kwlist,
+					 &consumer, &type,
+					 &flags, &def_vals_obj);
+	if (!rv)
+		return NULL;
+
+	gpiod_LineBulkObjToCLineBulk(self, &bulk);
+	gpiod_MakeRequestConfig(&conf, consumer, type, flags);
+
+	if (def_vals_obj) {
+		memset(default_vals, 0, sizeof(default_vals));
+
+		num_def_vals = PyObject_Size(def_vals_obj);
+		if (num_def_vals != self->num_lines) {
+			PyErr_SetString(PyExc_TypeError,
+					"Number of default values is not the same as the number of lines");
+			return NULL;
+		}
+
+		iter = PyObject_GetIter(def_vals_obj);
+		if (!iter)
+			return NULL;
+
+		for (i = 0;; i++) {
+			next = PyIter_Next(iter);
+			if (!next) {
+				Py_DECREF(iter);
+				break;
+			}
+
+			val = PyLong_AsUnsignedLong(next);
+			Py_DECREF(next);
+			if (PyErr_Occurred()) {
+				Py_DECREF(iter);
+				return NULL;
+			}
+
+			default_vals[i] = !!val;
+		}
+	}
+
+	Py_BEGIN_ALLOW_THREADS;
+	rv = gpiod_line_request_bulk(&bulk, &conf, default_vals);
+	Py_END_ALLOW_THREADS;
+	if (rv) {
+		PyErr_SetFromErrno(PyExc_OSError);
+		return NULL;
+	}
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(gpiod_LineBulk_get_values_doc,
+"get_values() -> list of integers\n"
+"\n"
+"Read the values of all the lines held by this LineBulk object. The index\n"
+"of each value in the returned list corresponds with the index of the line\n"
+"in this gpiod.LineBulk object.");
+
+static PyObject *gpiod_LineBulk_get_values(gpiod_LineBulkObject *self)
+{
+	int rv, vals[GPIOD_LINE_BULK_MAX_LINES];
+	struct gpiod_line_bulk bulk;
+	PyObject *val_list, *val;
+	Py_ssize_t i;
+
+	if (gpiod_LineBulkOwnerIsClosed(self))
+		return NULL;
+
+	gpiod_LineBulkObjToCLineBulk(self, &bulk);
+
+	memset(vals, 0, sizeof(vals));
+	Py_BEGIN_ALLOW_THREADS;
+	rv = gpiod_line_get_value_bulk(&bulk, vals);
+	Py_END_ALLOW_THREADS;
+	if (rv) {
+		PyErr_SetFromErrno(PyExc_OSError);
+		return NULL;
+	}
+
+	val_list = PyList_New(self->num_lines);
+	if (!val_list)
+		return NULL;
+
+	for (i = 0; i < self->num_lines; i++) {
+		val = Py_BuildValue("i", vals[i]);
+		if (!val) {
+			Py_DECREF(val_list);
+			return NULL;
+		}
+
+		rv = PyList_SetItem(val_list, i, val);
+		if (rv < 0) {
+			Py_DECREF(val);
+			Py_DECREF(val_list);
+			return NULL;
+		}
+	}
+
+	return val_list;
+}
+
+PyDoc_STRVAR(gpiod_LineBulk_set_values_doc,
+"set_values(values) -> None\n"
+"\n"
+"Set the values of all the lines held by this LineBulk object.\n"
+"\n"
+"  values\n"
+"    List of values (integers) to set.\n"
+"\n"
+"The number of values in the list passed as argument must be the same as\n"
+"the number of lines held by this gpiod.LineBulk object. The index of each\n"
+"value corresponds with the index of each line in the object.\n");
+
+static PyObject *gpiod_LineBulk_set_values(gpiod_LineBulkObject *self,
+					   PyObject *args)
+{
+	int rv, vals[GPIOD_LINE_BULK_MAX_LINES], val;
+	PyObject *val_list, *iter, *next;
+	struct gpiod_line_bulk bulk;
+	Py_ssize_t num_vals, i;
+
+	if (gpiod_LineBulkOwnerIsClosed(self))
+		return NULL;
+
+	gpiod_LineBulkObjToCLineBulk(self, &bulk);
+	memset(vals, 0, sizeof(vals));
+
+	rv = PyArg_ParseTuple(args, "O", &val_list);
+	if (!rv)
+		return NULL;
+
+	num_vals = PyObject_Size(val_list);
+	if (self->num_lines != num_vals) {
+		PyErr_SetString(PyExc_TypeError,
+				"Number of values must correspond with the number of lines");
+		return NULL;
+	}
+
+	iter = PyObject_GetIter(val_list);
+	if (!iter)
+		return NULL;
+
+	for (i = 0;; i++) {
+		next = PyIter_Next(iter);
+		if (!next) {
+			Py_DECREF(iter);
+			break;
+		}
+
+		val = PyLong_AsLong(next);
+		Py_DECREF(next);
+		if (PyErr_Occurred()) {
+			Py_DECREF(iter);
+			return NULL;
+		}
+
+		vals[i] = (int)val;
+	}
+
+	Py_BEGIN_ALLOW_THREADS;
+	rv = gpiod_line_set_value_bulk(&bulk, vals);
+	Py_END_ALLOW_THREADS;
+	if (rv) {
+		PyErr_SetFromErrno(PyExc_OSError);
+		return NULL;
+	}
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(gpiod_LineBulk_release_doc,
+"release() -> None\n"
+"\n"
+"Release all lines held by this LineBulk object.");
+
+static PyObject *gpiod_LineBulk_release(gpiod_LineBulkObject *self)
+{
+	struct gpiod_line_bulk bulk;
+
+	if (gpiod_LineBulkOwnerIsClosed(self))
+		return NULL;
+
+	gpiod_LineBulkObjToCLineBulk(self, &bulk);
+	gpiod_line_release_bulk(&bulk);
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(gpiod_LineBulk_event_wait_doc,
+"event_wait([sec[ ,nsec]]) -> gpiod.LineBulk object or None\n"
+"\n"
+"Poll the lines held by this LineBulk Object for line events.\n"
+"\n"
+"  sec\n"
+"    Number of seconds to wait before timeout.\n"
+"  nsec\n"
+"    Number of nanoseconds to wait before timeout.\n"
+"\n"
+"Returns a gpiod.LineBulk object containing references to lines on which\n"
+"events occurred or None if we reached the timeout without any event\n"
+"occurring.");
+
+static PyObject *gpiod_LineBulk_event_wait(gpiod_LineBulkObject *self,
+					   PyObject *args, PyObject *kwds)
+{
+	static char *kwlist[] = { "sec", "nsec", NULL };
+
+	struct gpiod_line_bulk bulk, ev_bulk;
+	struct gpiod_line *line, **line_ptr;
+	gpiod_LineObject *line_obj;
+	gpiod_ChipObject *owner;
+	long sec = 0, nsec = 0;
+	struct timespec ts;
+	PyObject *ret;
+	Py_ssize_t i;
+	int rv;
+
+	if (gpiod_LineBulkOwnerIsClosed(self))
+		return NULL;
+
+	rv = PyArg_ParseTupleAndKeywords(args, kwds,
+					 "|ll", kwlist, &sec, &nsec);
+	if (!rv)
+		return NULL;
+
+	ts.tv_sec = sec;
+	ts.tv_nsec = nsec;
+
+	gpiod_LineBulkObjToCLineBulk(self, &bulk);
+
+	Py_BEGIN_ALLOW_THREADS;
+	rv = gpiod_line_event_wait_bulk(&bulk, &ts, &ev_bulk);
+	Py_END_ALLOW_THREADS;
+	if (rv < 0) {
+		PyErr_SetFromErrno(PyExc_OSError);
+		return NULL;
+	} else if (rv == 0) {
+		Py_RETURN_NONE;
+	}
+
+	ret = PyList_New(gpiod_line_bulk_num_lines(&ev_bulk));
+	if (!ret)
+		return NULL;
+
+	owner = ((gpiod_LineObject *)(self->lines[0]))->owner;
+
+	i = 0;
+	gpiod_line_bulk_foreach_line(&ev_bulk, line, line_ptr) {
+		line_obj = gpiod_MakeLineObject(owner, line);
+		if (!line_obj) {
+			Py_DECREF(ret);
+			return NULL;
+		}
+
+		rv = PyList_SetItem(ret, i++, (PyObject *)line_obj);
+		if (rv < 0) {
+			Py_DECREF(ret);
+			return NULL;
+		}
+	}
+
+	return ret;
+}
+
+static PyObject *gpiod_LineBulk_repr(gpiod_LineBulkObject *self)
+{
+	PyObject *list, *list_repr, *chip_name, *ret;
+	gpiod_LineObject *line;
+
+	if (gpiod_LineBulkOwnerIsClosed(self))
+		return NULL;
+
+	list = gpiod_LineBulk_to_list(self);
+	if (!list)
+		return NULL;
+
+	list_repr = PyObject_Repr(list);
+	Py_DECREF(list);
+	if (!list_repr)
+		return NULL;
+
+	line = (gpiod_LineObject *)self->lines[0];
+	chip_name = PyObject_CallMethod((PyObject *)line->owner, "name", "");
+	if (!chip_name) {
+		Py_DECREF(list_repr);
+		return NULL;
+	}
+
+	ret = PyUnicode_FromFormat("%U%U", chip_name, list_repr);
+	Py_DECREF(chip_name);
+	Py_DECREF(list_repr);
+	return ret;
+}
+
+static PyMethodDef gpiod_LineBulk_methods[] = {
+	{
+		.ml_name = "to_list",
+		.ml_meth = (PyCFunction)gpiod_LineBulk_to_list,
+		.ml_doc = gpiod_LineBulk_to_list_doc,
+		.ml_flags = METH_NOARGS,
+	},
+	{
+		.ml_name = "request",
+		.ml_meth = (PyCFunction)gpiod_LineBulk_request,
+		.ml_doc = gpiod_LineBulk_request_doc,
+		.ml_flags = METH_VARARGS | METH_KEYWORDS,
+	},
+	{
+		.ml_name = "get_values",
+		.ml_meth = (PyCFunction)gpiod_LineBulk_get_values,
+		.ml_doc = gpiod_LineBulk_get_values_doc,
+		.ml_flags = METH_NOARGS,
+	},
+	{
+		.ml_name = "set_values",
+		.ml_meth = (PyCFunction)gpiod_LineBulk_set_values,
+		.ml_doc = gpiod_LineBulk_set_values_doc,
+		.ml_flags = METH_VARARGS,
+	},
+	{
+		.ml_name = "release",
+		.ml_meth = (PyCFunction)gpiod_LineBulk_release,
+		.ml_doc = gpiod_LineBulk_release_doc,
+		.ml_flags = METH_NOARGS,
+	},
+	{
+		.ml_name = "event_wait",
+		.ml_meth = (PyCFunction)gpiod_LineBulk_event_wait,
+		.ml_doc = gpiod_LineBulk_event_wait_doc,
+		.ml_flags = METH_VARARGS | METH_KEYWORDS,
+	},
+	{ }
+};
+
+PyDoc_STRVAR(gpiod_LineBulkType_doc,
+"Represents a set of GPIO lines.\n"
+"\n"
+"Objects of this type are immutable. The constructor takes as argument\n"
+"a sequence of gpiod.Line objects. It doesn't accept objects of any other\n"
+"type.");
+
+static PyTypeObject gpiod_LineBulkType = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.LineBulk",
+	.tp_basicsize = sizeof(gpiod_LineBulkType),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = gpiod_LineBulkType_doc,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)gpiod_LineBulk_init,
+	.tp_dealloc = (destructor)gpiod_LineBulk_dealloc,
+	.tp_iter = PyObject_SelfIter,
+	.tp_iternext = (iternextfunc)gpiod_LineBulk_iternext,
+	.tp_repr = (reprfunc)gpiod_LineBulk_repr,
+	.tp_methods = gpiod_LineBulk_methods,
+};
+
+static gpiod_LineBulkObject *gpiod_LineToLineBulk(gpiod_LineObject *line)
+{
+	gpiod_LineBulkObject *ret;
+	PyObject *args;
+
+	args = Py_BuildValue("((O))", line);
+	if (!args)
+		return NULL;
+
+	ret = (gpiod_LineBulkObject *)PyObject_CallObject(
+					(PyObject *)&gpiod_LineBulkType,
+					args);
+	Py_DECREF(args);
+
+	return ret;
+}
+
+enum {
+	gpiod_OPEN_LOOKUP = 1,
+	gpiod_OPEN_BY_NAME,
+	gpiod_OPEN_BY_PATH,
+	gpiod_OPEN_BY_LABEL,
+	gpiod_OPEN_BY_NUMBER,
+};
+
+static int gpiod_Chip_init(gpiod_ChipObject *self, PyObject *args)
+{
+	int rv, how = gpiod_OPEN_LOOKUP;
+	PyThreadState *thread;
+	char *descr;
+
+	rv = PyArg_ParseTuple(args, "s|i", &descr, &how);
+	if (!rv)
+		return -1;
+
+	thread = PyEval_SaveThread();
+	switch (how) {
+	case gpiod_OPEN_LOOKUP:
+		self->chip = gpiod_chip_open_lookup(descr);
+		break;
+	case gpiod_OPEN_BY_NAME:
+		self->chip = gpiod_chip_open_by_name(descr);
+		break;
+	case gpiod_OPEN_BY_PATH:
+		self->chip = gpiod_chip_open(descr);
+		break;
+	case gpiod_OPEN_BY_LABEL:
+		self->chip = gpiod_chip_open_by_label(descr);
+		break;
+	case gpiod_OPEN_BY_NUMBER:
+		self->chip = gpiod_chip_open_by_number(atoi(descr));
+		break;
+	default:
+		PyEval_RestoreThread(thread);
+		PyErr_BadArgument();
+		return -1;
+	}
+	PyEval_RestoreThread(thread);
+	if (!self->chip) {
+		PyErr_SetFromErrno(PyExc_OSError);
+		return -1;
+	}
+
+	return 0;
+}
+
+static void gpiod_Chip_dealloc(gpiod_ChipObject *self)
+{
+	if (self->chip)
+		gpiod_chip_close(self->chip);
+
+	PyObject_Del(self);
+}
+
+static PyObject *gpiod_Chip_repr(gpiod_ChipObject *self)
+{
+	if (gpiod_ChipIsClosed(self))
+		return NULL;
+
+	return PyUnicode_FromFormat("'%s /%s/ %u lines'",
+				    gpiod_chip_name(self->chip),
+				    gpiod_chip_label(self->chip),
+				    gpiod_chip_num_lines(self->chip));
+}
+
+PyDoc_STRVAR(gpiod_Chip_close_doc,
+"close() -> None\n"
+"\n"
+"Close the associated gpiochip descriptor. The chip object must no longer\n"
+"be used after this method is called.\n");
+
+static PyObject *gpiod_Chip_close(gpiod_ChipObject *self)
+{
+	if (gpiod_ChipIsClosed(self))
+		return NULL;
+
+	gpiod_chip_close(self->chip);
+	self->chip = NULL;
+
+	Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(gpiod_Chip_enter_doc,
+"Controlled execution enter callback.");
+
+static PyObject *gpiod_Chip_enter(gpiod_ChipObject *chip)
+{
+	Py_INCREF(chip);
+	return (PyObject *)chip;
+}
+
+PyDoc_STRVAR(gpiod_Chip_exit_doc,
+"Controlled execution exit callback.");
+
+static PyObject *gpiod_Chip_exit(gpiod_ChipObject *chip)
+{
+	return PyObject_CallMethod((PyObject *)chip, "close", "");
+}
+
+PyDoc_STRVAR(gpiod_Chip_name_doc,
+"name() -> string\n"
+"\n"
+"Get the name of the GPIO chip");
+
+static PyObject *gpiod_Chip_name(gpiod_ChipObject *self)
+{
+	if (gpiod_ChipIsClosed(self))
+		return NULL;
+
+	return PyUnicode_FromFormat("%s", gpiod_chip_name(self->chip));
+}
+
+PyDoc_STRVAR(gpiod_Chip_label_doc,
+"label() -> string\n"
+"\n"
+"Get the label of the GPIO chip");
+
+static PyObject *gpiod_Chip_label(gpiod_ChipObject *self)
+{
+	if (gpiod_ChipIsClosed(self))
+		return NULL;
+
+	return PyUnicode_FromFormat("%s", gpiod_chip_label(self->chip));
+}
+
+PyDoc_STRVAR(gpiod_Chip_num_lines_doc,
+"num_lines() -> integer\n"
+"\n"
+"Get the number of lines exposed by this GPIO chip.");
+
+static PyObject *gpiod_Chip_num_lines(gpiod_ChipObject *self)
+{
+	if (gpiod_ChipIsClosed(self))
+		return NULL;
+
+	return Py_BuildValue("I", gpiod_chip_num_lines(self->chip));
+}
+
+static gpiod_LineObject *
+gpiod_MakeLineObject(gpiod_ChipObject *owner, struct gpiod_line *line)
+{
+	gpiod_LineObject *obj;
+
+	obj = PyObject_New(gpiod_LineObject, &gpiod_LineType);
+	if (!obj)
+		return NULL;
+
+	obj->line = line;
+	Py_INCREF(owner);
+	obj->owner = owner;
+
+	return obj;
+}
+
+PyDoc_STRVAR(gpiod_Chip_get_line_doc,
+"get_line(offset) -> gpiod.Line object\n"
+"\n"
+"Get the GPIO line at given offset.\n"
+"\n"
+"  offset\n"
+"    Line offset (integer)");
+
+static gpiod_LineObject *
+gpiod_Chip_get_line(gpiod_ChipObject *self, PyObject *args)
+{
+	struct gpiod_line *line;
+	unsigned int offset;
+	int rv;
+
+	if (gpiod_ChipIsClosed(self))
+		return NULL;
+
+	rv = PyArg_ParseTuple(args, "I", &offset);
+	if (!rv)
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	line = gpiod_chip_get_line(self->chip, offset);
+	Py_END_ALLOW_THREADS;
+	if (!line) {
+		PyErr_SetFromErrno(PyExc_OSError);
+		return NULL;
+	}
+
+	return gpiod_MakeLineObject(self, line);
+}
+
+PyDoc_STRVAR(gpiod_Chip_find_line_doc,
+"find_line(name) -> gpiod.Line object or None\n"
+"\n"
+"Get the GPIO line by name.\n"
+"\n"
+"  name\n"
+"    Line name (string)\n"
+"\n"
+"Returns a gpiod.Line object or None if line with given name is not\n"
+"associated with this chip.");
+
+static gpiod_LineObject *
+gpiod_Chip_find_line(gpiod_ChipObject *self, PyObject *args)
+{
+	struct gpiod_line *line;
+	const char *name;
+	int rv;
+
+	if (gpiod_ChipIsClosed(self))
+		return NULL;
+
+	rv = PyArg_ParseTuple(args, "s", &name);
+	if (!rv)
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	line = gpiod_chip_find_line(self->chip, name);
+	Py_END_ALLOW_THREADS;
+	if (!line) {
+		if (errno == ENOENT) {
+			Py_INCREF(Py_None);
+			return (gpiod_LineObject *)Py_None;
+		}
+
+		PyErr_SetFromErrno(PyExc_OSError);
+		return NULL;
+	}
+
+	return gpiod_MakeLineObject(self, line);
+}
+
+static gpiod_LineBulkObject *gpiod_ListToLineBulk(PyObject *lines)
+{
+	gpiod_LineBulkObject *bulk;
+	PyObject *arg;
+
+	arg = PyTuple_Pack(1, lines);
+	if (!arg)
+		return NULL;
+
+	bulk = (gpiod_LineBulkObject *)PyObject_CallObject(
+					(PyObject *)&gpiod_LineBulkType,
+					arg);
+	Py_DECREF(arg);
+
+	return bulk;
+}
+
+PyDoc_STRVAR(gpiod_Chip_get_lines_doc,
+"get_lines(offsets) -> gpiod.LineBulk object\n"
+"\n"
+"Get a set of GPIO lines by their offsets.\n"
+"\n"
+"  offsets\n"
+"    List of lines offsets.");
+
+static gpiod_LineBulkObject *
+gpiod_Chip_get_lines(gpiod_ChipObject *self, PyObject *args)
+{
+	PyObject *offsets, *iter, *next, *lines, *arg;
+	gpiod_LineBulkObject *bulk;
+	Py_ssize_t num_offsets, i;
+	gpiod_LineObject *line;
+	int rv;
+
+	rv = PyArg_ParseTuple(args, "O", &offsets);
+	if (!rv)
+		return NULL;
+
+	num_offsets = PyObject_Size(offsets);
+	if (num_offsets < 1) {
+		PyErr_SetString(PyExc_TypeError,
+				"Argument must be a non-empty sequence of offsets");
+		return NULL;
+	}
+
+	lines = PyList_New(num_offsets);
+	if (!lines)
+		return NULL;
+
+	iter = PyObject_GetIter(offsets);
+	if (!iter) {
+		Py_DECREF(lines);
+		return NULL;
+	}
+
+	for (i = 0;;) {
+		next = PyIter_Next(iter);
+		if (!next) {
+			Py_DECREF(iter);
+			break;
+		}
+
+		arg = PyTuple_Pack(1, next);
+		Py_DECREF(next);
+		if (!arg) {
+			Py_DECREF(iter);
+			Py_DECREF(lines);
+			return NULL;
+		}
+
+		line = gpiod_Chip_get_line(self, arg);
+		Py_DECREF(arg);
+		if (!line) {
+			Py_DECREF(iter);
+			Py_DECREF(lines);
+			return NULL;
+		}
+
+		rv = PyList_SetItem(lines, i++, (PyObject *)line);
+		if (rv < 0) {
+			Py_DECREF(line);
+			Py_DECREF(iter);
+			Py_DECREF(lines);
+			return NULL;
+		}
+	}
+
+	bulk = gpiod_ListToLineBulk(lines);
+	Py_DECREF(lines);
+	if (!bulk)
+		return NULL;
+
+	return bulk;
+}
+
+PyDoc_STRVAR(gpiod_Chip_get_all_lines_doc,
+"get_all_lines() -> gpiod.LineBulk object\n"
+"\n"
+"Get all lines exposed by this Chip.");
+
+static gpiod_LineBulkObject *
+gpiod_Chip_get_all_lines(gpiod_ChipObject *self)
+{
+	gpiod_LineBulkObject *bulk_obj;
+	struct gpiod_line_bulk bulk;
+	gpiod_LineObject *line_obj;
+	struct gpiod_line *line;
+	unsigned int offset;
+	PyObject *list;
+	int rv;
+
+	if (gpiod_ChipIsClosed(self))
+		return NULL;
+
+	rv = gpiod_chip_get_all_lines(self->chip, &bulk);
+	if (rv) {
+		PyErr_SetFromErrno(PyExc_OSError);
+		return NULL;
+	}
+
+	list = PyList_New(gpiod_line_bulk_num_lines(&bulk));
+	if (!list)
+		return NULL;
+
+	gpiod_line_bulk_foreach_line_off(&bulk, line, offset) {
+		line_obj = gpiod_MakeLineObject(self, line);
+		if (!line_obj) {
+			Py_DECREF(list);
+			return NULL;
+		}
+
+		rv = PyList_SetItem(list, offset, (PyObject *)line_obj);
+		if (rv < 0) {
+			Py_DECREF(line_obj);
+			Py_DECREF(list);
+			return NULL;
+		}
+	}
+
+	bulk_obj = gpiod_ListToLineBulk(list);
+	Py_DECREF(list);
+	if (!bulk_obj)
+		return NULL;
+
+	return bulk_obj;
+}
+
+PyDoc_STRVAR(gpiod_Chip_find_lines_doc,
+"find_lines(names) -> gpiod.LineBulk object\n"
+"\n"
+"Look up a set of lines by their names.\n"
+"\n"
+"  names\n"
+"    Sequence of line names.\n"
+"\n"
+"Unlike find_line(), this method raises an exception if at least one line\n"
+"from the list doesn't exist.");
+
+static gpiod_LineBulkObject *
+gpiod_Chip_find_lines(gpiod_ChipObject *self, PyObject *args)
+{
+	PyObject *names, *lines, *iter, *next, *arg;
+	gpiod_LineBulkObject *bulk;
+	Py_ssize_t num_names, i;
+	gpiod_LineObject *line;
+	int rv;
+
+	rv = PyArg_ParseTuple(args, "O", &names);
+	if (!rv)
+		return NULL;
+
+	num_names = PyObject_Size(names);
+	if (num_names < 1) {
+		PyErr_SetString(PyExc_TypeError,
+				"Argument must be a non-empty sequence of names");
+		return NULL;
+	}
+
+	lines = PyList_New(num_names);
+	if (!lines)
+		return NULL;
+
+	iter = PyObject_GetIter(names);
+	if (!iter) {
+		Py_DECREF(lines);
+		return NULL;
+	}
+
+	for (i = 0;;) {
+		next = PyIter_Next(iter);
+		if (!next) {
+			Py_DECREF(iter);
+			break;
+		}
+
+		arg = PyTuple_Pack(1, next);
+		if (!arg) {
+			Py_DECREF(iter);
+			Py_DECREF(lines);
+			return NULL;
+		}
+
+		line = gpiod_Chip_find_line(self, arg);
+		Py_DECREF(arg);
+		if (!line || (PyObject *)line == Py_None) {
+			Py_DECREF(iter);
+			Py_DECREF(lines);
+			if ((PyObject *)line == Py_None)
+				PyErr_SetString(PyExc_TypeError,
+						"Unable to find all lines from the list");
+			return NULL;
+		}
+
+		rv = PyList_SetItem(lines, i++, (PyObject *)line);
+		if (rv < 0) {
+			Py_DECREF(line);
+			Py_DECREF(iter);
+			Py_DECREF(lines);
+			return NULL;
+		}
+	}
+
+	bulk = gpiod_ListToLineBulk(lines);
+	Py_DECREF(lines);
+	return bulk;
+}
+
+static PyMethodDef gpiod_Chip_methods[] = {
+	{
+		.ml_name = "close",
+		.ml_meth = (PyCFunction)gpiod_Chip_close,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Chip_close_doc,
+	},
+	{
+		.ml_name = "__enter__",
+		.ml_meth = (PyCFunction)gpiod_Chip_enter,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Chip_enter_doc,
+	},
+	{
+		.ml_name = "__exit__",
+		.ml_meth = (PyCFunction)gpiod_Chip_exit,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = gpiod_Chip_exit_doc,
+	},
+	{
+		.ml_name = "name",
+		.ml_meth = (PyCFunction)gpiod_Chip_name,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Chip_name_doc,
+	},
+	{
+		.ml_name = "label",
+		.ml_meth = (PyCFunction)gpiod_Chip_label,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Chip_label_doc,
+	},
+	{
+		.ml_name = "num_lines",
+		.ml_meth = (PyCFunction)gpiod_Chip_num_lines,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Chip_num_lines_doc,
+	},
+	{
+		.ml_name = "get_line",
+		.ml_meth = (PyCFunction)gpiod_Chip_get_line,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = gpiod_Chip_get_line_doc,
+	},
+	{
+		.ml_name = "find_line",
+		.ml_meth = (PyCFunction)gpiod_Chip_find_line,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = gpiod_Chip_find_line_doc,
+	},
+	{
+		.ml_name = "get_lines",
+		.ml_meth = (PyCFunction)gpiod_Chip_get_lines,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = gpiod_Chip_get_lines_doc,
+	},
+	{
+		.ml_name = "get_all_lines",
+		.ml_meth = (PyCFunction)gpiod_Chip_get_all_lines,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Chip_get_all_lines_doc,
+	},
+	{
+		.ml_name = "find_lines",
+		.ml_meth = (PyCFunction)gpiod_Chip_find_lines,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = gpiod_Chip_find_lines_doc,
+	},
+	{ }
+};
+
+PyDoc_STRVAR(gpiod_ChipType_doc,
+"Represents a GPIO chip.\n"
+"\n"
+"Chip object manages all resources associated with the GPIO chip\n"
+"it represents.\n"
+"\n"
+"The gpiochip device file is opened during the object's construction.\n"
+"The Chip object's constructor takes a description string as argument the\n"
+"meaning of which depends on the second, optional parameter which defines\n"
+"the way the description string should be interpreted. The available\n"
+"options are: OPEN_BY_LABEL, OPEN_BY_NAME, OPEN_BY_NUMBER, OPEN_BY_PATH,\n"
+"and OPEN_LOOKUP. The last option means that libgpiod should open the chip\n"
+"based on the best guess what the path is. This is also the default if the\n"
+"second argument is missing.\n"
+"\n"
+"Callers must close the chip by calling the close() method when it's no\n"
+"longer used.\n"
+"\n"
+"Example:\n"
+"\n"
+"    chip = gpiod.Chip('gpiochip0', gpiod.Chip.OPEN_BY_NAME)\n"
+"    do_something(chip)\n"
+"    chip.close()\n"
+"\n"
+"The gpiod.Chip class also supports controlled execution ('with' statement).\n"
+"\n"
+"Example:\n"
+"\n"
+"    with gpiod.Chip('0', gpiod.Chip.OPEN_BY_NUMBER) as chip:\n"
+"        do_something(chip)");
+
+static PyTypeObject gpiod_ChipType = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.Chip",
+	.tp_basicsize = sizeof(gpiod_ChipObject),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = gpiod_ChipType_doc,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)gpiod_Chip_init,
+	.tp_dealloc = (destructor)gpiod_Chip_dealloc,
+	.tp_repr = (reprfunc)gpiod_Chip_repr,
+	.tp_methods = gpiod_Chip_methods,
+};
+
+static int gpiod_ChipIter_init(gpiod_ChipIterObject *self)
+{
+	self->iter = gpiod_chip_iter_new();
+	if (!self->iter) {
+		PyErr_SetFromErrno(PyExc_OSError);
+		return -1;
+	}
+
+	return 0;
+}
+
+static void gpiod_ChipIter_dealloc(gpiod_ChipIterObject *self)
+{
+	if (self->iter)
+		gpiod_chip_iter_free_noclose(self->iter);
+
+	PyObject_Del(self);
+}
+
+static gpiod_ChipObject *gpiod_ChipIter_next(gpiod_ChipIterObject *self)
+{
+	gpiod_ChipObject *chip_obj;
+	struct gpiod_chip *chip;
+
+	Py_BEGIN_ALLOW_THREADS;
+	chip = gpiod_chip_iter_next_noclose(self->iter);
+	Py_END_ALLOW_THREADS;
+	if (!chip)
+		return NULL; /* Last element. */
+
+	chip_obj = PyObject_New(gpiod_ChipObject, &gpiod_ChipType);
+	if (!chip_obj) {
+		gpiod_chip_close(chip);
+		return NULL;
+	}
+
+	chip_obj->chip = chip;
+
+	return chip_obj;
+}
+
+PyDoc_STRVAR(gpiod_ChipIterType_doc,
+"Allows to iterate over all GPIO chips in the system.\n"
+"\n"
+"The ChipIter's constructor takes no arguments.\n"
+"\n"
+"Each iteration yields the next open GPIO chip handle. The caller is\n"
+"responsible for closing each chip\n"
+"\n"
+"Example:\n"
+"\n"
+"    for chip in gpiod.ChipIter():\n"
+"        do_something_with_chip(chip)\n"
+"        chip.close()");
+
+static PyTypeObject gpiod_ChipIterType = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.ChipIter",
+	.tp_basicsize = sizeof(gpiod_ChipIterObject),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = gpiod_ChipIterType_doc,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)gpiod_ChipIter_init,
+	.tp_dealloc = (destructor)gpiod_ChipIter_dealloc,
+	.tp_iter = PyObject_SelfIter,
+	.tp_iternext = (iternextfunc)gpiod_ChipIter_next,
+};
+
+static int gpiod_LineIter_init(gpiod_LineIterObject *self, PyObject *args)
+{
+	gpiod_ChipObject *chip_obj;
+	int rv;
+
+	rv = PyArg_ParseTuple(args, "O!", &gpiod_ChipType,
+			      (PyObject *)&chip_obj);
+	if (!rv)
+		return -1;
+
+	if (gpiod_ChipIsClosed(chip_obj))
+		return -1;
+
+	Py_BEGIN_ALLOW_THREADS;
+	self->iter = gpiod_line_iter_new(chip_obj->chip);
+	Py_END_ALLOW_THREADS;
+	if (!self->iter) {
+		PyErr_SetFromErrno(PyExc_OSError);
+		return -1;
+	}
+
+	self->owner = chip_obj;
+	Py_INCREF(chip_obj);
+
+	return 0;
+}
+
+static void gpiod_LineIter_dealloc(gpiod_LineIterObject *self)
+{
+	if (self->iter)
+		gpiod_line_iter_free(self->iter);
+
+	PyObject_Del(self);
+}
+
+static gpiod_LineObject *gpiod_LineIter_next(gpiod_LineIterObject *self)
+{
+	struct gpiod_line *line;
+
+	line = gpiod_line_iter_next(self->iter);
+	if (!line)
+		return NULL; /* Last element. */
+
+	return gpiod_MakeLineObject(self->owner, line);
+}
+
+PyDoc_STRVAR(gpiod_LineIterType_doc,
+"Allows to iterate over all lines exposed by a GPIO chip.\n"
+"\n"
+"New line iterator is created by passing a reference to an open gpiod.Chip\n"
+"object to the constructor of gpiod.LineIter.\n"
+"\n"
+"Caller doesn't need to handle the resource management for lines as their\n"
+"lifetime is managed by the owning chip.\n"
+"\n"
+"Example:\n"
+"\n"
+"    chip = gpiod.Chip('gpiochip0')\n"
+"    for line in gpiod.LineIter(chip):\n"
+"        do_stuff_with_line(line)");
+
+static PyTypeObject gpiod_LineIterType = {
+	PyVarObject_HEAD_INIT(NULL, 0)
+	.tp_name = "gpiod.LineIter",
+	.tp_basicsize = sizeof(gpiod_LineIterObject),
+	.tp_flags = Py_TPFLAGS_DEFAULT,
+	.tp_doc = gpiod_LineIterType_doc,
+	.tp_new = PyType_GenericNew,
+	.tp_init = (initproc)gpiod_LineIter_init,
+	.tp_dealloc = (destructor)gpiod_LineIter_dealloc,
+	.tp_iter = PyObject_SelfIter,
+	.tp_iternext = (iternextfunc)gpiod_LineIter_next,
+};
+
+PyDoc_STRVAR(gpiod_Module_find_line_doc,
+"find_line(name) -> gpiod.Line object or None\n"
+"\n"
+"Lookup a GPIO line by name. Search all gpiochips. Returns a gpiod.Line\n"
+"or None if a line with given name doesn't exist in the system.\n"
+"\n"
+"NOTE: the gpiod.Chip object owning the returned line must be closed\n"
+"by the caller.\n"
+"\n"
+"  name\n"
+"    Name of the line to find (string).");
+
+static gpiod_LineObject *gpiod_Module_find_line(PyObject *self GPIOD_UNUSED,
+						PyObject *args)
+{
+	gpiod_LineObject *line_obj;
+	gpiod_ChipObject *chip_obj;
+	struct gpiod_chip *chip;
+	struct gpiod_line *line;
+	const char *name;
+	int rv;
+
+	rv = PyArg_ParseTuple(args, "s", &name);
+	if (!rv)
+		return NULL;
+
+	Py_BEGIN_ALLOW_THREADS;
+	line = gpiod_line_find(name);
+	Py_END_ALLOW_THREADS;
+	if (!line) {
+		if (errno == ENOENT) {
+			Py_INCREF(Py_None);
+			return (gpiod_LineObject *)Py_None;
+		}
+
+		PyErr_SetFromErrno(PyExc_OSError);
+		return NULL;
+	}
+
+	chip = gpiod_line_get_chip(line);
+
+	chip_obj = PyObject_New(gpiod_ChipObject, &gpiod_ChipType);
+	if (!chip_obj) {
+		gpiod_chip_close(chip);
+		return NULL;
+	}
+
+	chip_obj->chip = chip;
+
+	line_obj = gpiod_MakeLineObject(chip_obj, line);
+	if (!line_obj)
+		return NULL;
+
+	/*
+	 * PyObject_New() set the reference count for the chip object at 1 and
+	 * the call to gpiod_MakeLineObject() increased it to 2. However when
+	 * we return the object to the line object to the python interpreter,
+	 * there'll be only a single reference holder to the chip - the line
+	 * object itself. Decrease the chip reference here manually.
+	 */
+	Py_DECREF(line_obj->owner);
+
+	return line_obj;
+}
+
+PyDoc_STRVAR(gpiod_Module_version_string_doc,
+"version_string() -> string\n"
+"\n"
+"Get the API version of the library as a human-readable string.");
+
+static PyObject *gpiod_Module_version_string(void)
+{
+	return PyUnicode_FromFormat("%s", gpiod_version_string());
+}
+
+static PyMethodDef gpiod_module_methods[] = {
+	{
+		.ml_name = "find_line",
+		.ml_meth = (PyCFunction)gpiod_Module_find_line,
+		.ml_flags = METH_VARARGS,
+		.ml_doc = gpiod_Module_find_line_doc,
+	},
+	{
+		.ml_name = "version_string",
+		.ml_meth = (PyCFunction)gpiod_Module_version_string,
+		.ml_flags = METH_NOARGS,
+		.ml_doc = gpiod_Module_version_string_doc,
+	},
+	{ }
+};
+
+typedef struct {
+	const char *name;
+	PyTypeObject *typeobj;
+} gpiod_PyType;
+
+static gpiod_PyType gpiod_PyType_list[] = {
+	{ .name = "Chip",	.typeobj = &gpiod_ChipType,		},
+	{ .name = "Line",	.typeobj = &gpiod_LineType,		},
+	{ .name = "LineEvent",	.typeobj = &gpiod_LineEventType,	},
+	{ .name = "LineBulk",	.typeobj = &gpiod_LineBulkType,		},
+	{ .name = "LineIter",	.typeobj = &gpiod_LineIterType,		},
+	{ .name = "ChipIter",	.typeobj = &gpiod_ChipIterType		},
+	{ }
+};
+
+typedef struct {
+	PyTypeObject *typeobj;
+	const char *name;
+	long int val;
+} gpiod_ConstDescr;
+
+static gpiod_ConstDescr gpiod_ConstList[] = {
+	{
+		.typeobj = &gpiod_ChipType,
+		.name = "OPEN_LOOKUP",
+		.val = gpiod_OPEN_LOOKUP,
+	},
+	{
+		.typeobj = &gpiod_ChipType,
+		.name = "OPEN_BY_PATH",
+		.val = gpiod_OPEN_BY_PATH,
+	},
+	{
+		.typeobj = &gpiod_ChipType,
+		.name = "OPEN_BY_NAME",
+		.val = gpiod_OPEN_BY_NAME,
+	},
+	{
+		.typeobj = &gpiod_ChipType,
+		.name = "OPEN_BY_LABEL",
+		.val = gpiod_OPEN_BY_LABEL,
+	},
+	{
+		.typeobj = &gpiod_ChipType,
+		.name = "OPEN_BY_NUMBER",
+		.val = gpiod_OPEN_BY_NUMBER,
+	},
+	{
+		.typeobj = &gpiod_LineType,
+		.name = "DIRECTION_INPUT",
+		.val = gpiod_DIRECTION_INPUT,
+	},
+	{
+		.typeobj = &gpiod_LineType,
+		.name = "DIRECTION_OUTPUT",
+		.val = gpiod_DIRECTION_OUTPUT,
+	},
+	{
+		.typeobj = &gpiod_LineType,
+		.name = "ACTIVE_HIGH",
+		.val = gpiod_ACTIVE_HIGH,
+	},
+	{
+		.typeobj = &gpiod_LineType,
+		.name = "ACTIVE_LOW",
+		.val = gpiod_ACTIVE_LOW,
+	},
+	{
+		.typeobj = &gpiod_LineEventType,
+		.name = "RISING_EDGE",
+		.val = gpiod_RISING_EDGE,
+	},
+	{
+		.typeobj = &gpiod_LineEventType,
+		.name = "FALLING_EDGE",
+		.val = gpiod_FALLING_EDGE,
+	},
+	{ }
+};
+
+PyDoc_STRVAR(gpiod_Module_doc,
+"Python bindings for libgpiod.\n\
+\n\
+This module wraps the native C API of libgpiod in a set of python classes.");
+
+static PyModuleDef gpiod_Module = {
+	PyModuleDef_HEAD_INIT,
+	.m_name = "gpiod",
+	.m_doc = gpiod_Module_doc,
+	.m_size = -1,
+	.m_methods = gpiod_module_methods,
+};
+
+typedef struct {
+	const char *name;
+	long int value;
+} gpiod_ModuleConst;
+
+static gpiod_ModuleConst gpiod_ModuleConsts[] = {
+	{
+		.name = "LINE_REQ_DIR_AS_IS",
+		.value = gpiod_LINE_REQ_DIR_AS_IS,
+	},
+	{
+		.name = "LINE_REQ_DIR_IN",
+		.value = gpiod_LINE_REQ_DIR_IN,
+	},
+	{
+		.name = "LINE_REQ_DIR_OUT",
+		.value = gpiod_LINE_REQ_DIR_OUT,
+	},
+	{
+		.name = "LINE_REQ_EV_FALLING_EDGE",
+		.value = gpiod_LINE_REQ_EV_FALLING_EDGE,
+	},
+	{
+		.name = "LINE_REQ_EV_RISING_EDGE",
+		.value = gpiod_LINE_REQ_EV_RISING_EDGE,
+	},
+	{
+		.name = "LINE_REQ_EV_BOTH_EDGES",
+		.value = gpiod_LINE_REQ_EV_BOTH_EDGES,
+	},
+	{
+		.name = "LINE_REQ_FLAG_OPEN_DRAIN",
+		.value = gpiod_LINE_REQ_FLAG_OPEN_DRAIN,
+	},
+	{
+		.name = "LINE_REQ_FLAG_OPEN_SOURCE",
+		.value = gpiod_LINE_REQ_FLAG_OPEN_SOURCE,
+	},
+	{
+		.name = "LINE_REQ_FLAG_ACTIVE_LOW",
+		.value = gpiod_LINE_REQ_FLAG_ACTIVE_LOW,
+	},
+	{ }
+};
+
+PyMODINIT_FUNC PyInit_gpiod(void)
+{
+	gpiod_ConstDescr *const_descr;
+	gpiod_ModuleConst *mod_const;
+	PyObject *module, *val;
+	gpiod_PyType *type;
+	unsigned int i;
+	int rv;
+
+	module = PyModule_Create(&gpiod_Module);
+	if (!module)
+		return NULL;
+
+	for (i = 0; gpiod_PyType_list[i].typeobj; i++) {
+		type = &gpiod_PyType_list[i];
+
+		rv = PyType_Ready(type->typeobj);
+		if (rv)
+			return NULL;
+
+		Py_INCREF(type->typeobj);
+		rv = PyModule_AddObject(module, type->name,
+					(PyObject *)type->typeobj);
+		if (rv < 0)
+			return NULL;
+	}
+
+	for (i = 0; gpiod_ConstList[i].name; i++) {
+		const_descr = &gpiod_ConstList[i];
+
+		val = PyLong_FromLong(const_descr->val);
+		if (!val)
+			return NULL;
+
+		rv = PyDict_SetItemString(const_descr->typeobj->tp_dict,
+					  const_descr->name, val);
+		Py_DECREF(val);
+		if (rv)
+			return NULL;
+	}
+
+	for (i = 0; gpiod_ModuleConsts[i].name; i++) {
+		mod_const = &gpiod_ModuleConsts[i];
+
+		rv = PyModule_AddIntConstant(module,
+					     mod_const->name, mod_const->value);
+		if (rv < 0)
+			return NULL;
+	}
+
+	return module;
+}
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 0000000..7a2870c
--- /dev/null
+++ b/configure.ac
@@ -0,0 +1,198 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2019 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+AC_PREREQ(2.61)
+
+AC_INIT([libgpiod], 1.3)
+AC_SUBST(EXTRA_VERSION, [])
+
+AC_DEFINE_UNQUOTED([GPIOD_VERSION_STR],
+			["$PACKAGE_VERSION$EXTRA_VERSION"],
+			[Full library version string.])
+AC_SUBST(VERSION_STR, [$PACKAGE_VERSION$EXTRA_VERSION])
+
+# From the libtool manual:
+#
+# (...)
+# 3. If the library source code has changed at all since the last update, then
+#    increment revision ('c:r:a' becomes 'c:r+1:a').
+# 4. If any interfaces have been added, removed, or changed since the last
+#    update, increment current, and set revision to 0.
+# 5. If any interfaces have been added since the last public release, then
+#    increment age.
+# 6. If any interfaces have been removed or changed since the last public
+#    release, then set age to 0.
+#
+# Define the libtool version as (C.R.A):
+# NOTE: this version only applies to the core C library.
+AC_SUBST(ABI_VERSION, [3.1.1])
+# Have a separate ABI version for C++ bindings:
+AC_SUBST(ABI_CXX_VERSION, [1.1.0])
+
+AC_CONFIG_AUX_DIR([autostuff])
+AC_CONFIG_MACRO_DIRS([m4])
+AM_INIT_AUTOMAKE([foreign subdir-objects])
+
+m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])])
+m4_pattern_forbid([^AX_],
+	[Unexpanded AX_ macro found. Please install GNU autoconf-archive.])
+
+AC_ARG_VAR([PYTHON_CPPFLAGS],
+	[Compiler flags to find Python headers [default: auto-detect]])
+AC_ARG_VAR([PYTHON_LIBS],
+	[Libraries to link into Python extensions [default: auto-detect]])
+
+AC_CONFIG_SRCDIR([lib])
+AC_CONFIG_HEADER([config.h])
+
+AC_DEFINE([_GNU_SOURCE], [], [We want GNU extensions])
+
+# Silence warning: ar: 'u' modifier ignored since 'D' is the default
+AC_SUBST(AR_FLAGS, [cr])
+
+AM_PROG_AR
+AC_PROG_CC
+AC_PROG_CXX
+AC_PROG_LIBTOOL
+AC_PROG_INSTALL
+
+AC_DEFUN([ERR_NOT_FOUND],
+	[AC_MSG_ERROR([$1 not found (needed to build $2)], [1])])
+
+AC_DEFUN([FUNC_NOT_FOUND_LIB],
+	[ERR_NOT_FOUND([$1()], [the library])])
+
+AC_DEFUN([HEADER_NOT_FOUND_LIB],
+	[ERR_NOT_FOUND([$1 header], [the library])])
+
+# This is always checked (library needs this)
+AC_HEADER_STDC
+AC_FUNC_MALLOC
+AC_CHECK_FUNC([ioctl], [], [FUNC_NOT_FOUND_LIB([ioctl])])
+AC_CHECK_FUNC([asprintf], [], [FUNC_NOT_FOUND_LIB([asprintf])])
+AC_CHECK_FUNC([scandir], [], [FUNC_NOT_FOUND_LIB([scandir])])
+AC_CHECK_FUNC([alphasort], [], [FUNC_NOT_FOUND_LIB([alphasort])])
+AC_CHECK_FUNC([ppoll], [], [FUNC_NOT_FOUND_LIB([ppoll])])
+AC_CHECK_HEADERS([getopt.h], [], [HEADER_NOT_FOUND_LIB([getopt.h])])
+AC_CHECK_HEADERS([dirent.h], [], [HEADER_NOT_FOUND_LIB([dirent.h])])
+AC_CHECK_HEADERS([sys/poll.h], [], [HEADER_NOT_FOUND_LIB([sys/poll.h])])
+AC_CHECK_HEADERS([sys/sysmacros.h], [], [HEADER_NOT_FOUND_LIB([sys/sysmacros.h])])
+AC_CHECK_HEADERS([linux/gpio.h], [], [HEADER_NOT_FOUND_LIB([linux/gpio.h])])
+
+AC_ARG_ENABLE([tools],
+	[AC_HELP_STRING([--enable-tools],
+		[enable libgpiod command-line tools [default=no]])],
+	[if test "x$enableval" = xyes; then with_tools=true; fi],
+	[with_tools=false])
+AM_CONDITIONAL([WITH_TOOLS], [test "x$with_tools" = xtrue])
+
+AC_DEFUN([FUNC_NOT_FOUND_TOOLS],
+	[ERR_NOT_FOUND([$1()], [tools])])
+
+AC_DEFUN([HEADER_NOT_FOUND_TOOLS],
+	[ERR_NOT_FOUND([$1 header], [tools])])
+
+if test "x$with_tools" = xtrue
+then
+	# These are only needed to build tools
+	AC_CHECK_FUNC([basename], [], [FUNC_NOT_FOUND_TOOLS([basename])])
+	AC_CHECK_FUNC([daemon], [], [FUNC_NOT_FOUND_TOOLS([daemon])])
+	AC_CHECK_FUNC([signalfd], [], [FUNC_NOT_FOUND_TOOLS([signalfd])])
+	AC_CHECK_FUNC([setlinebuf], [], [FUNC_NOT_FOUND_TOOLS([setlinebuf])])
+	AC_CHECK_HEADERS([sys/signalfd.h], [], [HEADER_NOT_FOUND_TOOLS([sys/signalfd.h])])
+fi
+
+AC_ARG_ENABLE([install-tests],
+	[AC_HELP_STRING([--enable-install-tests],
+		[enable install tests [default=no]])],
+	[if test "x$enableval" = xyes; then with_install_tests=true; with_tests=true; fi],
+	[with_install_tests=false])
+AM_CONDITIONAL([WITH_INSTALL_TESTS], [test "x$with_install_tests" = xtrue])
+
+AC_ARG_ENABLE([tests],
+	[AC_HELP_STRING([--enable-tests],
+		[enable libgpiod tests [default=no]])],
+	[if test "x$enableval" = xyes; then with_tests=true; fi],
+	[with_tests=false])
+AM_CONDITIONAL([WITH_TESTS], [test "x$with_tests" = xtrue])
+
+AC_DEFUN([FUNC_NOT_FOUND_TESTS],
+	[ERR_NOT_FOUND([$1()], [tests])])
+
+if test "x$with_tests" = xtrue
+then
+	AC_CHECK_FUNC([qsort], [], [FUNC_NOT_FOUND_TESTS([qsort])])
+	AC_CHECK_FUNC([regexec], [], [FUNC_NOT_FOUND_TESTS([regexec])])
+
+	PKG_CHECK_MODULES([KMOD], [libkmod >= 18])
+	PKG_CHECK_MODULES([UDEV], [libudev >= 215])
+fi
+
+AC_ARG_ENABLE([bindings-cxx],
+	[AC_HELP_STRING([--enable-bindings-cxx],
+		[enable C++ bindings [default=no]])],
+	[if test "x$enableval" = xyes; then with_bindings_cxx=true; fi],
+	[with_bindings_cxx=false])
+AM_CONDITIONAL([WITH_BINDINGS_CXX], [test "x$with_bindings_cxx" = xtrue])
+
+if test "x$with_bindings_cxx" = xtrue
+then
+	AC_LIBTOOL_CXX
+	# This needs autoconf-archive
+	AX_CXX_COMPILE_STDCXX_11([ext], [mandatory])
+fi
+
+AC_ARG_ENABLE([bindings-python],
+	[AC_HELP_STRING([--enable-bindings-python],
+		[enable python3 bindings [default=no]])],
+	[if test "x$enableval" = xyes; then with_bindings_python=true; fi],
+	[with_bindings_python=false])
+AM_CONDITIONAL([WITH_BINDINGS_PYTHON], [test "x$with_bindings_python" = xtrue])
+
+if test "x$with_bindings_python" = xtrue
+then
+	AM_PATH_PYTHON([3.0], [],
+		[AC_MSG_ERROR([python3 not found - needed for python bindings])])
+	AS_IF([test -z "$PYTHON_CPPFLAGS"],
+		[AC_SUBST(PYTHON_CPPFLAGS, [`$PYTHON-config --includes`])])
+	AS_IF([test -z "$PYTHON_LIBS"],
+		[AC_SUBST(PYTHON_LIBS, [`$PYTHON-config --libs`])])
+fi
+
+AC_CHECK_PROG([has_doxygen], [doxygen], [true], [false])
+AM_CONDITIONAL([HAS_DOXYGEN], [test "x$has_doxygen" = xtrue])
+if test "x$has_doxygen" = xfalse
+then
+	AC_MSG_NOTICE([doxygen not found - documentation cannot be generated])
+fi
+
+if test "x$cross_compiling" = xno
+then
+	AC_CHECK_PROG([has_help2man], [help2man], [true], [false])
+fi
+AM_CONDITIONAL([WITH_MANPAGES], [test "x$has_help2man" = xtrue])
+if test "x$has_help2man" = xfalse
+then
+	AC_MSG_NOTICE([help2man not found - man pages cannot be generated automatically])
+fi
+
+AC_CONFIG_FILES([libgpiod.pc
+		 Makefile
+		 include/Makefile
+		 lib/Makefile
+		 tools/Makefile
+		 tests/Makefile
+		 bindings/cxx/libgpiodcxx.pc
+		 bindings/Makefile
+		 bindings/cxx/Makefile
+		 bindings/cxx/examples/Makefile
+		 bindings/python/Makefile
+		 bindings/python/examples/Makefile
+		 man/Makefile])
+
+AC_OUTPUT
diff --git a/include/Makefile.am b/include/Makefile.am
new file mode 100644
index 0000000..dc1afa9
--- /dev/null
+++ b/include/Makefile.am
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+include_HEADERS = gpiod.h
diff --git a/include/config.h b/include/config.h
new file mode 100644
index 0000000..cad5a54
--- /dev/null
+++ b/include/config.h
@@ -0,0 +1,96 @@
+/* config.h.  Generated from config.h.in by configure.  */
+/* config.h.in.  Generated from configure.ac by autoheader.  */
+
+/* Full library version string. */
+#define GPIOD_VERSION_STR "1.3"
+
+/* define if the compiler supports basic C++11 syntax */
+/* #undef HAVE_CXX11 */
+
+/* Define to 1 if you have the <dirent.h> header file. */
+#define HAVE_DIRENT_H 1
+
+/* Define to 1 if you have the <dlfcn.h> header file. */
+#define HAVE_DLFCN_H 1
+
+/* Define to 1 if you have the <getopt.h> header file. */
+#define HAVE_GETOPT_H 1
+
+/* Define to 1 if you have the <inttypes.h> header file. */
+#define HAVE_INTTYPES_H 1
+
+/* Define to 1 if you have the <linux/gpio.h> header file. */
+#define HAVE_LINUX_GPIO_H 1
+
+/* Define to 1 if your system has a GNU libc compatible `malloc' function, and
+   to 0 otherwise. */
+#define HAVE_MALLOC 1
+
+/* Define to 1 if you have the <memory.h> header file. */
+#define HAVE_MEMORY_H 1
+
+/* Define to 1 if you have the <stdint.h> header file. */
+#define HAVE_STDINT_H 1
+
+/* Define to 1 if you have the <stdlib.h> header file. */
+#define HAVE_STDLIB_H 1
+
+/* Define to 1 if you have the <strings.h> header file. */
+#define HAVE_STRINGS_H 1
+
+/* Define to 1 if you have the <string.h> header file. */
+#define HAVE_STRING_H 1
+
+/* Define to 1 if you have the <sys/poll.h> header file. */
+#define HAVE_SYS_POLL_H 1
+
+/* Define to 1 if you have the <sys/signalfd.h> header file. */
+#define HAVE_SYS_SIGNALFD_H 1
+
+/* Define to 1 if you have the <sys/stat.h> header file. */
+#define HAVE_SYS_STAT_H 1
+
+/* Define to 1 if you have the <sys/sysmacros.h> header file. */
+#define HAVE_SYS_SYSMACROS_H 1
+
+/* Define to 1 if you have the <sys/types.h> header file. */
+#define HAVE_SYS_TYPES_H 1
+
+/* Define to 1 if you have the <unistd.h> header file. */
+#define HAVE_UNISTD_H 1
+
+/* Define to the sub-directory where libtool stores uninstalled libraries. */
+#define LT_OBJDIR ".libs/"
+
+/* Name of package */
+#define PACKAGE "libgpiod"
+
+/* Define to the address where bug reports for this package should be sent. */
+#define PACKAGE_BUGREPORT ""
+
+/* Define to the full name of this package. */
+#define PACKAGE_NAME "libgpiod"
+
+/* Define to the full name and version of this package. */
+#define PACKAGE_STRING "libgpiod 1.3"
+
+/* Define to the one symbol short name of this package. */
+#define PACKAGE_TARNAME "libgpiod"
+
+/* Define to the home page for this package. */
+#define PACKAGE_URL ""
+
+/* Define to the version of this package. */
+#define PACKAGE_VERSION "1.3"
+
+/* Define to 1 if you have the ANSI C header files. */
+#define STDC_HEADERS 1
+
+/* Version number of package */
+#define VERSION "1.3"
+
+/* We want GNU extensions */
+#define _GNU_SOURCE /**/
+
+/* Define to rpl_malloc if the replacement function should be used. */
+/* #undef malloc */
diff --git a/include/gpiod.h b/include/gpiod.h
new file mode 100644
index 0000000..2679478
--- /dev/null
+++ b/include/gpiod.h
@@ -0,0 +1,1404 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#ifndef __LIBGPIOD_GPIOD_H__
+#define __LIBGPIOD_GPIOD_H__
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <time.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @mainpage libgpiod public API
+ *
+ * This is the complete documentation of the public API made available to
+ * users of libgpiod.
+ *
+ * <p>The public header is logically split into two high-level parts: the
+ * simple API and the low-level API. The former allows users to easily
+ * interact with the GPIOs in the system without dealing with the low-level
+ * data structures and resource control. The latter gives the user much more
+ * fine-grained control over the GPIO interface.
+ *
+ * <p>The low-level API is further logically split into several parts such
+ * as: GPIO chip & line operators, iterators, GPIO events handling etc.
+ *
+ * <p>General note on error handling: all routines exported by libgpiod  set
+ * errno to one of the error values defined in errno.h upon failure. The way
+ * of notifying the caller that an error occurred varies between functions,
+ * but in general a function that returns an int, returns -1 on error, while
+ * a function returning a pointer bails out on error condition by returning
+ * a NULL pointer.
+ */
+
+struct gpiod_chip;
+struct gpiod_line;
+struct gpiod_chip_iter;
+struct gpiod_line_iter;
+struct gpiod_line_bulk;
+
+/**
+ * @defgroup __common__ Common helper macros
+ * @{
+ *
+ * Commonly used utility macros.
+ */
+
+/**
+ * @brief Makes symbol visible.
+ */
+#define GPIOD_API		__attribute__((visibility("default")))
+
+/**
+ * @brief Marks a function argument or variable as potentially unused.
+ */
+#define GPIOD_UNUSED		__attribute__((unused))
+
+/**
+ * @brief Shift 1 by given offset.
+ * @param nr Bit position.
+ * @return 1 shifted by nr.
+ */
+#define GPIOD_BIT(nr)		(1UL << (nr))
+
+/**
+ * @brief Marks a public function as deprecated.
+ */
+#define GPIOD_DEPRECATED	__attribute__((deprecated))
+
+/**
+ * @}
+ *
+ * @defgroup __high_level__ High-level API
+ * @{
+ *
+ * Simple high-level routines for straightforward GPIO manipulation without
+ * the need to use the gpiod_* structures or to keep track of resources.
+ */
+
+/**
+ * @brief Read current value from a single GPIO line.
+ * @param device Name, path, number or label of the gpiochip.
+ * @param offset Offset of the GPIO line.
+ * @param active_low The active state of this line - true if low.
+ * @param consumer Name of the consumer.
+ * @return 0 or 1 (GPIO value) if the operation succeeds, -1 on error.
+ */
+int gpiod_ctxless_get_value(const char *device, unsigned int offset,
+			    bool active_low, const char *consumer) GPIOD_API;
+
+/**
+ * @brief Read current values from a set of GPIO lines.
+ * @param device Name, path, number or label of the gpiochip.
+ * @param offsets Array of offsets of lines whose values should be read.
+ * @param values Buffer in which the values will be stored.
+ * @param num_lines Number of lines, must be > 0.
+ * @param active_low The active state of the lines - true if low.
+ * @param consumer Name of the consumer.
+ * @return 0 if the operation succeeds, -1 on error.
+ */
+int gpiod_ctxless_get_value_multiple(const char *device,
+				     const unsigned int *offsets, int *values,
+				     unsigned int num_lines, bool active_low,
+				     const char *consumer) GPIOD_API;
+
+/**
+ * @brief Simple set value callback signature.
+ */
+typedef void (*gpiod_ctxless_set_value_cb)(void *);
+
+/**
+ * @brief Set value of a single GPIO line.
+ * @param device Name, path, number or label of the gpiochip.
+ * @param offset The offset of the GPIO line.
+ * @param value New value (0 or 1).
+ * @param active_low The active state of this line - true if low.
+ * @param consumer Name of the consumer.
+ * @param cb Optional callback function that will be called right after setting
+ *           the value. Users can use this, for example, to pause the execution
+ *           after toggling a GPIO.
+ * @param data Optional user data that will be passed to the callback function.
+ * @return 0 if the operation succeeds, -1 on error.
+ */
+int gpiod_ctxless_set_value(const char *device, unsigned int offset, int value,
+			    bool active_low, const char *consumer,
+			    gpiod_ctxless_set_value_cb cb,
+			    void *data) GPIOD_API;
+
+/**
+ * @brief Set values of multiple GPIO lines.
+ * @param device Name, path, number or label of the gpiochip.
+ * @param offsets Array of offsets of lines the values of which should be set.
+ * @param values Array of integers containing new values.
+ * @param num_lines Number of lines, must be > 0.
+ * @param active_low The active state of the lines - true if low.
+ * @param consumer Name of the consumer.
+ * @param cb Optional callback function that will be called right after setting
+ *           all values. Works the same as in ::gpiod_ctxless_set_value.
+ * @param data Optional user data that will be passed to the callback function.
+ * @return 0 if the operation succeeds, -1 on error.
+ */
+int gpiod_ctxless_set_value_multiple(const char *device,
+				     const unsigned int *offsets,
+				     const int *values, unsigned int num_lines,
+				     bool active_low, const char *consumer,
+				     gpiod_ctxless_set_value_cb cb,
+				     void *data) GPIOD_API;
+
+/**
+ * @brief Event types that the ctxless event monitor can wait for.
+ */
+enum {
+	/**< Wait for rising edge events only. */
+	GPIOD_CTXLESS_EVENT_RISING_EDGE = 1,
+	/**< Wait for falling edge events only. */
+	GPIOD_CTXLESS_EVENT_FALLING_EDGE,
+	/**< Wait for both types of events. */
+	GPIOD_CTXLESS_EVENT_BOTH_EDGES,
+};
+
+/**
+ * @brief Event types that can be passed to the ctxless event callback.
+ */
+enum {
+	GPIOD_CTXLESS_EVENT_CB_TIMEOUT = 1,
+	/**< Waiting for events timed out. */
+	GPIOD_CTXLESS_EVENT_CB_RISING_EDGE,
+	/**< Rising edge event occured. */
+	GPIOD_CTXLESS_EVENT_CB_FALLING_EDGE,
+	/**< Falling edge event occured. */
+};
+
+/**
+ * @brief Return status values that the ctxless event callback can return.
+ */
+enum {
+	GPIOD_CTXLESS_EVENT_CB_RET_ERR = -1,
+	/**< Stop processing events and indicate an error. */
+	GPIOD_CTXLESS_EVENT_CB_RET_OK = 0,
+	/**< Continue processing events. */
+	GPIOD_CTXLESS_EVENT_CB_RET_STOP = 1,
+	/**< Stop processing events. */
+};
+
+/**
+ * @brief Simple event callback signature.
+ *
+ * The callback function takes the following arguments: event type (int),
+ * GPIO line offset (unsigned int), event timestamp (const struct timespec *)
+ * and a pointer to user data (void *).
+ *
+ * This callback is called by the ctxless event loop functions for each GPIO
+ * event. If the callback returns ::GPIOD_CTXLESS_EVENT_CB_RET_ERR, it should
+ * also set errno.
+ */
+typedef int (*gpiod_ctxless_event_handle_cb)(int, unsigned int,
+					     const struct timespec *, void *);
+
+/**
+ * @brief Return status values that the ctxless event poll callback can return.
+ *
+ * Positive value returned from the polling callback indicates the number of
+ * events that occurred on the set of monitored lines.
+ */
+enum {
+	GPIOD_CTXLESS_EVENT_POLL_RET_STOP = -2,
+	/**< The event loop should stop processing events. */
+	GPIOD_CTXLESS_EVENT_POLL_RET_ERR = -1,
+	/**< Polling error occurred (the polling function should set errno). */
+	GPIOD_CTXLESS_EVENT_POLL_RET_TIMEOUT = 0,
+	/**< Poll timed out. */
+};
+
+/**
+ * @brief Helper structure for the ctxless event loop poll callback.
+ */
+struct gpiod_ctxless_event_poll_fd {
+	int fd;
+	/**< File descriptor number. */
+	bool event;
+	/**< Indicates whether an event occurred on this file descriptor. */
+};
+
+/**
+ * @brief Simple event poll callback signature.
+ *
+ * The poll callback function takes the following arguments: number of lines
+ * (unsigned int), an array of file descriptors on which input events should
+ * be monitored (struct gpiod_ctxless_event_poll_fd *), poll timeout
+ * (const struct timespec *) and a pointer to user data (void *).
+ *
+ * The callback should poll for input events on the set of descriptors and
+ * return an appropriate value that can be interpreted by the event loop
+ * routine.
+ */
+typedef int (*gpiod_ctxless_event_poll_cb)(unsigned int,
+				struct gpiod_ctxless_event_poll_fd *,
+				const struct timespec *, void *);
+
+/**
+ * @brief Wait for events on a single GPIO line.
+ * @param device Name, path, number or label of the gpiochip.
+ * @param offset GPIO line offset to monitor.
+ * @param active_low The active state of this line - true if low.
+ * @param consumer Name of the consumer.
+ * @param timeout Maximum wait time for each iteration.
+ * @param poll_cb Callback function to call when waiting for events.
+ * @param event_cb Callback function to call for each line event.
+ * @param data User data passed to the callback.
+ * @return 0 if no errors were encountered, -1 if an error occurred.
+ * @note The way the ctxless event loop works is described in detail in
+ *       ::gpiod_ctxless_event_loop_multiple - this is just a wrapper aound
+ *       this routine which calls it for a single GPIO line.
+ */
+int gpiod_ctxless_event_loop(const char *device, unsigned int offset,
+			     bool active_low, const char *consumer,
+			     const struct timespec *timeout,
+			     gpiod_ctxless_event_poll_cb poll_cb,
+			     gpiod_ctxless_event_handle_cb event_cb,
+			     void *data) GPIOD_API GPIOD_DEPRECATED;
+
+/**
+ * @brief Wait for events on multiple GPIO lines.
+ * @param device Name, path, number or label of the gpiochip.
+ * @param offsets Array of GPIO line offsets to monitor.
+ * @param num_lines Number of lines to monitor.
+ * @param active_low The active state of this line - true if low.
+ * @param consumer Name of the consumer.
+ * @param timeout Maximum wait time for each iteration.
+ * @param poll_cb Callback function to call when waiting for events. Can
+ *                be NULL.
+ * @param event_cb Callback function to call on event occurrence.
+ * @param data User data passed to the callback.
+ * @return 0 no errors were encountered, -1 if an error occurred.
+ * @note The poll callback can be NULL in which case the routine will fall
+ *       back to a basic, ppoll() based callback.
+ *
+ * Internally this routine opens the GPIO chip, requests the set of lines for
+ * both-edges events and calls the polling callback in a loop. The role of the
+ * polling callback is to detect input events on a set of file descriptors and
+ * notify the caller about the fds ready for reading.
+ *
+ * The ctxless event loop then reads each queued event from marked descriptors
+ * and calls the event callback. Both callbacks can stop the loop at any
+ * point.
+ *
+ * The poll_cb argument can be NULL in which case the function falls back to
+ * a default, ppoll() based callback.
+ */
+int gpiod_ctxless_event_loop_multiple(const char *device,
+				      const unsigned int *offsets,
+				      unsigned int num_lines, bool active_low,
+				      const char *consumer,
+				      const struct timespec *timeout,
+				      gpiod_ctxless_event_poll_cb poll_cb,
+				      gpiod_ctxless_event_handle_cb event_cb,
+				      void *data) GPIOD_API GPIOD_DEPRECATED;
+
+/**
+ * @brief Wait for events on a single GPIO line.
+ * @param device Name, path, number or label of the gpiochip.
+ * @param event_type Type of events to listen for.
+ * @param offset GPIO line offset to monitor.
+ * @param active_low The active state of this line - true if low.
+ * @param consumer Name of the consumer.
+ * @param timeout Maximum wait time for each iteration.
+ * @param poll_cb Callback function to call when waiting for events.
+ * @param event_cb Callback function to call for each line event.
+ * @param data User data passed to the callback.
+ * @return 0 if no errors were encountered, -1 if an error occurred.
+ * @note The way the ctxless event loop works is described in detail in
+ *       ::gpiod_ctxless_event_monitor_multiple - this is just a wrapper aound
+ *       this routine which calls it for a single GPIO line.
+ */
+int gpiod_ctxless_event_monitor(const char *device, int event_type,
+				unsigned int offset, bool active_low,
+				const char *consumer,
+				const struct timespec *timeout,
+				gpiod_ctxless_event_poll_cb poll_cb,
+				gpiod_ctxless_event_handle_cb event_cb,
+				void *data) GPIOD_API;
+
+/**
+ * @brief Wait for events on multiple GPIO lines.
+ * @param device Name, path, number or label of the gpiochip.
+ * @param event_type Type of events to listen for.
+ * @param offsets Array of GPIO line offsets to monitor.
+ * @param num_lines Number of lines to monitor.
+ * @param active_low The active state of this line - true if low.
+ * @param consumer Name of the consumer.
+ * @param timeout Maximum wait time for each iteration.
+ * @param poll_cb Callback function to call when waiting for events. Can
+ *                be NULL.
+ * @param event_cb Callback function to call on event occurrence.
+ * @param data User data passed to the callback.
+ * @return 0 no errors were encountered, -1 if an error occurred.
+ * @note The poll callback can be NULL in which case the routine will fall
+ *       back to a basic, ppoll() based callback.
+ *
+ * Internally this routine opens the GPIO chip, requests the set of lines for
+ * the type of events specified in the event_type paramter and calls the
+ * polling callback in a loop. The role of the polling callback is to detect
+ * input events on a set of file descriptors and notify the caller about the
+ * fds ready for reading.
+ *
+ * The ctxless event loop then reads each queued event from marked descriptors
+ * and calls the event callback. Both callbacks can stop the loop at any
+ * point.
+ *
+ * The poll_cb argument can be NULL in which case the function falls back to
+ * a default, ppoll() based callback.
+ */
+int gpiod_ctxless_event_monitor_multiple(
+			const char *device, int event_type,
+			const unsigned int *offsets,
+			unsigned int num_lines, bool active_low,
+			const char *consumer, const struct timespec *timeout,
+			gpiod_ctxless_event_poll_cb poll_cb,
+			gpiod_ctxless_event_handle_cb event_cb,
+			void *data) GPIOD_API;
+
+/**
+ * @brief Determine the chip name and line offset of a line with given name.
+ * @param name The name of the GPIO line to lookup.
+ * @param chipname Buffer in which the name of the GPIO chip will be stored.
+ * @param chipname_size Size of the chip name buffer.
+ * @param offset Pointer to an integer in which the line offset will be stored.
+ * @return -1 on error, 0 if the line with given name doesn't exist and 1 if
+ *         the line was found. In the first two cases the contents of chipname
+ *         and offset remain unchanged.
+ * @note The chip name is truncated if the buffer can't hold its entire size.
+ */
+int gpiod_ctxless_find_line(const char *name, char *chipname,
+			    size_t chipname_size,
+			    unsigned int *offset) GPIOD_API;
+
+/**
+ * @}
+ *
+ * @defgroup __chips__ GPIO chip operations
+ * @{
+ *
+ * Functions and data structures dealing with GPIO chips.
+ */
+
+/**
+ * @brief Open a gpiochip by path.
+ * @param path Path to the gpiochip device file.
+ * @return GPIO chip handle or NULL if an error occurred.
+ */
+struct gpiod_chip *gpiod_chip_open(const char *path) GPIOD_API;
+
+/**
+ * @brief Open a gpiochip by name.
+ * @param name Name of the gpiochip to open.
+ * @return GPIO chip handle or NULL if an error occurred.
+ *
+ * This routine appends name to '/dev/' to create the path.
+ */
+struct gpiod_chip *gpiod_chip_open_by_name(const char *name) GPIOD_API;
+
+/**
+ * @brief Open a gpiochip by number.
+ * @param num Number of the gpiochip.
+ * @return GPIO chip handle or NULL if an error occurred.
+ *
+ * This routine appends num to '/dev/gpiochip' to create the path.
+ */
+struct gpiod_chip *gpiod_chip_open_by_number(unsigned int num) GPIOD_API;
+
+/**
+ * @brief Open a gpiochip by label.
+ * @param label Label of the gpiochip to open.
+ * @return GPIO chip handle or NULL if the chip with given label was not found
+ *         or an error occured.
+ * @note If the chip cannot be found but no other error occurred, errno is set
+ *       to ENOENT.
+ */
+struct gpiod_chip *gpiod_chip_open_by_label(const char *label) GPIOD_API;
+
+/**
+ * @brief Open a gpiochip based on the best guess what the path is.
+ * @param descr String describing the gpiochip.
+ * @return GPIO chip handle or NULL if an error occurred.
+ *
+ * This routine tries to figure out whether the user passed it the path to the
+ * GPIO chip, its name, label or number as a string. Then it tries to open it
+ * using one of the gpiod_chip_open** variants.
+ */
+struct gpiod_chip *gpiod_chip_open_lookup(const char *descr) GPIOD_API;
+
+/**
+ * @brief Close a GPIO chip handle and release all allocated resources.
+ * @param chip The GPIO chip object.
+ */
+void gpiod_chip_close(struct gpiod_chip *chip) GPIOD_API;
+
+/**
+ * @brief Get the GPIO chip name as represented in the kernel.
+ * @param chip The GPIO chip object.
+ * @return Pointer to a human-readable string containing the chip name.
+ */
+const char *gpiod_chip_name(struct gpiod_chip *chip) GPIOD_API;
+
+/**
+ * @brief Get the GPIO chip label as represented in the kernel.
+ * @param chip The GPIO chip object.
+ * @return Pointer to a human-readable string containing the chip label.
+ */
+const char *gpiod_chip_label(struct gpiod_chip *chip) GPIOD_API;
+
+/**
+ * @brief Get the number of GPIO lines exposed by this chip.
+ * @param chip The GPIO chip object.
+ * @return Number of GPIO lines.
+ */
+unsigned int gpiod_chip_num_lines(struct gpiod_chip *chip) GPIOD_API;
+
+/**
+ * @brief Get the handle to the GPIO line at given offset.
+ * @param chip The GPIO chip object.
+ * @param offset The offset of the GPIO line.
+ * @return Pointer to the GPIO line handle or NULL if an error occured.
+ */
+struct gpiod_line *
+gpiod_chip_get_line(struct gpiod_chip *chip, unsigned int offset) GPIOD_API;
+
+/**
+ * @brief Retrieve a set of lines and store them in a line bulk object.
+ * @param chip The GPIO chip object.
+ * @param offsets Array of offsets of lines to retrieve.
+ * @param num_offsets Number of lines to retrieve.
+ * @param bulk Line bulk object in which to store the line handles.
+ * @return 0 on success, -1 on error.
+ */
+int gpiod_chip_get_lines(struct gpiod_chip *chip,
+			 unsigned int *offsets, unsigned int num_offsets,
+			 struct gpiod_line_bulk *bulk) GPIOD_API;
+
+/**
+ * @brief Retrieve all lines exposed by a chip and store them in a bulk object.
+ * @param chip The GPIO chip object.
+ * @param bulk Line bulk object in which to store the line handles.
+ * @return 0 on success, -1 on error.
+ */
+int gpiod_chip_get_all_lines(struct gpiod_chip *chip,
+			     struct gpiod_line_bulk *bulk) GPIOD_API;
+
+/**
+ * @brief Find a GPIO line by name among lines associated with given GPIO chip.
+ * @param chip The GPIO chip object.
+ * @param name The name of the GPIO line.
+ * @return Pointer to the GPIO line handle or NULL if the line could not be
+ *         found or an error occurred.
+ * @note In case a line with given name is not associated with given chip, the
+ *       function sets errno to ENOENT.
+ */
+struct gpiod_line *
+gpiod_chip_find_line(struct gpiod_chip *chip, const char *name) GPIOD_API;
+
+/**
+ * @brief Find a set of GPIO lines by names among lines exposed by this chip.
+ * @param chip The GPIO chip object.
+ * @param names Array of pointers to C-strings containing the names of the
+ *              lines to lookup. Must end with a NULL-pointer.
+ * @param bulk Line bulk object in which the located lines will be stored.
+ * @return 0 if all lines were located, -1 on error.
+ * @note If at least one line from the list could not be found among the lines
+ *       exposed by this chip, the function sets errno to ENOENT.
+ */
+int gpiod_chip_find_lines(struct gpiod_chip *chip, const char **names,
+			  struct gpiod_line_bulk *bulk) GPIOD_API;
+
+/**
+ * @}
+ *
+ * @defgroup __lines__ GPIO line operations
+ * @{
+ *
+ * Functions and data structures dealing with GPIO lines.
+ *
+ * @defgroup __line_bulk__ Operating on multiple lines
+ * @{
+ */
+
+/**
+ * @brief Maximum number of GPIO lines that can be requested at once.
+ */
+#define GPIOD_LINE_BULK_MAX_LINES	64
+
+/**
+ * @brief Helper structure for storing a set of GPIO line objects.
+ *
+ * This structure is used in all operations involving sets of GPIO lines. If
+ * a bulk object is being passed to a function while containing zero lines,
+ * the result is undefined.
+ */
+struct gpiod_line_bulk {
+	struct gpiod_line *lines[GPIOD_LINE_BULK_MAX_LINES];
+	/**< Buffer for line pointers. */
+	unsigned int num_lines;
+	/**< Number of lines currently held in this structure. */
+};
+
+/**
+ * @brief Static initializer for GPIO bulk objects.
+ *
+ * This macro simply sets the internally held number of lines to 0.
+ */
+#define GPIOD_LINE_BULK_INITIALIZER	{ { NULL }, 0 }
+
+/**
+ * @brief Initialize a GPIO bulk object.
+ * @param bulk Line bulk object.
+ *
+ * This routine simply sets the internally held number of lines to 0.
+ */
+static inline void gpiod_line_bulk_init(struct gpiod_line_bulk *bulk)
+{
+	bulk->num_lines = 0;
+}
+
+/**
+ * @brief Add a single line to a GPIO bulk object.
+ * @param bulk Line bulk object.
+ * @param line Line to add.
+ */
+static inline void gpiod_line_bulk_add(struct gpiod_line_bulk *bulk,
+				       struct gpiod_line *line)
+{
+	bulk->lines[bulk->num_lines++] = line;
+}
+
+/**
+ * @brief Retrieve the line handle from a line bulk object at given offset.
+ * @param bulk Line bulk object.
+ * @param offset Line offset.
+ * @return Line handle at given offset.
+ */
+static inline struct gpiod_line *
+gpiod_line_bulk_get_line(struct gpiod_line_bulk *bulk, unsigned int offset)
+{
+	return bulk->lines[offset];
+}
+
+/**
+ * @brief Retrieve the number of GPIO lines held by this line bulk object.
+ * @param bulk Line bulk object.
+ * @return Number of lines held by this line bulk.
+ */
+static inline unsigned int
+gpiod_line_bulk_num_lines(struct gpiod_line_bulk *bulk)
+{
+	return bulk->num_lines;
+}
+
+/**
+ * @brief Iterate over all line handles held by a line bulk object.
+ * @param bulk Line bulk object.
+ * @param line GPIO line handle. On each iteration, the subsequent line handle
+ *             is assigned to this pointer.
+ * @param lineptr Pointer to a GPIO line handle used to store the loop state.
+ */
+#define gpiod_line_bulk_foreach_line(bulk, line, lineptr)		\
+	for ((lineptr) = (bulk)->lines, (line) = *(lineptr);		\
+	     (lineptr) <= (bulk)->lines + ((bulk)->num_lines - 1);	\
+	     (lineptr)++, (line) = *(lineptr))
+
+/**
+ * @brief Iterate over all line handles held by a line bulk object (integer
+ *        counter variant).
+ * @param bulk Line bulk object.
+ * @param line GPIO line handle. On each iteration, the subsequent line handle
+ *             is assigned to this pointer.
+ * @param offset An integer variable used to store the loop state.
+ *
+ * This is a variant of ::gpiod_line_bulk_foreach_line which uses an integer
+ * variable (either signed or unsigned) to store the loop state. This offset
+ * variable is guaranteed to correspond with the offset of the current line in
+ * the bulk->lines array.
+ */
+#define gpiod_line_bulk_foreach_line_off(bulk, line, offset)		\
+	for ((offset) = 0, (line) = (bulk)->lines[0];			\
+	     (offset) < (bulk)->num_lines;				\
+	     (offset)++, (line) = (bulk)->lines[(offset)])
+
+/**
+ * @}
+ *
+ * @defgroup __line_info__ Line info
+ * @{
+ */
+
+/**
+ * @brief Possible direction settings.
+ */
+enum {
+	GPIOD_LINE_DIRECTION_INPUT = 1,
+	/**< Direction is input - we're reading the state of a GPIO line. */
+	GPIOD_LINE_DIRECTION_OUTPUT,
+	/**< Direction is output - we're driving the GPIO line. */
+};
+
+/**
+ * @brief Possible active state settings.
+ */
+enum {
+	GPIOD_LINE_ACTIVE_STATE_HIGH = 1,
+	/**< The active state of a GPIO is active-high. */
+	GPIOD_LINE_ACTIVE_STATE_LOW,
+	/**< The active state of a GPIO is active-low. */
+};
+
+/**
+ * @brief Read the GPIO line offset.
+ * @param line GPIO line object.
+ * @return Line offset.
+ */
+unsigned int gpiod_line_offset(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Read the GPIO line name.
+ * @param line GPIO line object.
+ * @return Name of the GPIO line as it is represented in the kernel. This
+ *         routine returns a pointer to a null-terminated string or NULL if
+ *         the line is unnamed.
+ */
+const char *gpiod_line_name(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Read the GPIO line consumer name.
+ * @param line GPIO line object.
+ * @return Name of the GPIO consumer name as it is represented in the
+ *         kernel. This routine returns a pointer to a null-terminated string
+ *         or NULL if the line is not used.
+ */
+const char *gpiod_line_consumer(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Read the GPIO line direction setting.
+ * @param line GPIO line object.
+ * @return Returns GPIOD_LINE_DIRECTION_INPUT or GPIOD_LINE_DIRECTION_OUTPUT.
+ */
+int gpiod_line_direction(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Read the GPIO line active state setting.
+ * @param line GPIO line object.
+ * @return Returns GPIOD_LINE_ACTIVE_STATE_HIGH or GPIOD_LINE_ACTIVE_STATE_LOW.
+ */
+int gpiod_line_active_state(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Check if the line is currently in use.
+ * @param line GPIO line object.
+ * @return True if the line is in use, false otherwise.
+ *
+ * The user space can't know exactly why a line is busy. It may have been
+ * requested by another process or hogged by the kernel. It only matters that
+ * the line is used and we can't request it.
+ */
+bool gpiod_line_is_used(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Check if the line is an open-drain GPIO.
+ * @param line GPIO line object.
+ * @return True if the line is an open-drain GPIO, false otherwise.
+ */
+bool gpiod_line_is_open_drain(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Check if the line is an open-source GPIO.
+ * @param line GPIO line object.
+ * @return True if the line is an open-source GPIO, false otherwise.
+ */
+bool gpiod_line_is_open_source(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Re-read the line info.
+ * @param line GPIO line object.
+ * @return 0 is the operation succeeds. In case of an error this routine
+ *         returns -1 and sets the last error number.
+ *
+ * The line info is initially retrieved from the kernel by
+ * gpiod_chip_get_line(). Users can use this line to manually re-read the line
+ * info.
+ */
+int gpiod_line_update(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Check if the line info needs to be updated.
+ * @param line GPIO line object.
+ * @return Returns false if the line is up-to-date. True otherwise.
+ *
+ * The line is updated by calling gpiod_line_update() from within
+ * gpiod_chip_get_line() and on every line request/release. However: an error
+ * returned from gpiod_line_update() only breaks the execution of the former.
+ * The request/release routines only set the internal up-to-date flag to false
+ * and continue their execution. This routine allows to check if a line info
+ * update failed at some point and we should call gpiod_line_update()
+ * explicitly.
+ */
+bool gpiod_line_needs_update(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @}
+ *
+ * @defgroup __line_request__ Line requests
+ * @{
+ */
+
+/**
+ * @brief Available types of requests.
+ */
+enum {
+	GPIOD_LINE_REQUEST_DIRECTION_AS_IS = 1,
+	/**< Request the line(s), but don't change current direction. */
+	GPIOD_LINE_REQUEST_DIRECTION_INPUT,
+	/**< Request the line(s) for reading the GPIO line state. */
+	GPIOD_LINE_REQUEST_DIRECTION_OUTPUT,
+	/**< Request the line(s) for setting the GPIO line state. */
+	GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE,
+	/**< Monitor both types of events. */
+	GPIOD_LINE_REQUEST_EVENT_RISING_EDGE,
+	/**< Only watch rising edge events. */
+	GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES,
+	/**< Only watch falling edge events. */
+};
+
+/**
+ * @brief Miscellaneous GPIO request flags.
+ */
+enum {
+	GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN	= GPIOD_BIT(0),
+	/**< The line is an open-drain port. */
+	GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE	= GPIOD_BIT(1),
+	/**< The line is an open-source port. */
+	GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW	= GPIOD_BIT(2),
+	/**< The active state of the line is low (high is the default). */
+};
+
+/**
+ * @brief Structure holding configuration of a line request.
+ */
+struct gpiod_line_request_config {
+	const char *consumer;
+	/**< Name of the consumer. */
+	int request_type;
+	/**< Request type. */
+	int flags;
+	/**< Other configuration flags. */
+};
+
+/**
+ * @brief Reserve a single line.
+ * @param line GPIO line object.
+ * @param config Request options.
+ * @param default_val Initial line value - only relevant if we're setting
+ *                    the direction to output.
+ * @return 0 if the line was properly reserved. In case of an error this
+ *         routine returns -1 and sets the last error number.
+ *
+ * If this routine succeeds, the caller takes ownership of the GPIO line until
+ * it's released.
+ */
+int gpiod_line_request(struct gpiod_line *line,
+		       const struct gpiod_line_request_config *config,
+		       int default_val) GPIOD_API;
+
+/**
+ * @brief Reserve a single line, set the direction to input.
+ * @param line GPIO line object.
+ * @param consumer Name of the consumer.
+ * @return 0 if the line was properly reserved, -1 on failure.
+ */
+int gpiod_line_request_input(struct gpiod_line *line,
+			     const char *consumer) GPIOD_API;
+
+/**
+ * @brief Reserve a single line, set the direction to output.
+ * @param line GPIO line object.
+ * @param consumer Name of the consumer.
+ * @param default_val Initial line value.
+ * @return 0 if the line was properly reserved, -1 on failure.
+ */
+int gpiod_line_request_output(struct gpiod_line *line,
+			      const char *consumer, int default_val) GPIOD_API;
+
+/**
+ * @brief Request rising edge event notifications on a single line.
+ * @param line GPIO line object.
+ * @param consumer Name of the consumer.
+ * @return 0 if the operation succeeds, -1 on failure.
+ */
+int gpiod_line_request_rising_edge_events(struct gpiod_line *line,
+					  const char *consumer) GPIOD_API;
+
+/**
+ * @brief Request falling edge event notifications on a single line.
+ * @param line GPIO line object.
+ * @param consumer Name of the consumer.
+ * @return 0 if the operation succeeds, -1 on failure.
+ */
+int gpiod_line_request_falling_edge_events(struct gpiod_line *line,
+					   const char *consumer) GPIOD_API;
+
+/**
+ * @brief Request all event type notifications on a single line.
+ * @param line GPIO line object.
+ * @param consumer Name of the consumer.
+ * @return 0 if the operation succeeds, -1 on failure.
+ */
+int gpiod_line_request_both_edges_events(struct gpiod_line *line,
+					 const char *consumer) GPIOD_API;
+
+/**
+ * @brief Reserve a single line, set the direction to input.
+ * @param line GPIO line object.
+ * @param consumer Name of the consumer.
+ * @param flags Additional request flags.
+ * @return 0 if the line was properly reserved, -1 on failure.
+ */
+int gpiod_line_request_input_flags(struct gpiod_line *line,
+				   const char *consumer, int flags) GPIOD_API;
+
+/**
+ * @brief Reserve a single line, set the direction to output.
+ * @param line GPIO line object.
+ * @param consumer Name of the consumer.
+ * @param flags Additional request flags.
+ * @param default_val Initial line value.
+ * @return 0 if the line was properly reserved, -1 on failure.
+ */
+int gpiod_line_request_output_flags(struct gpiod_line *line,
+				    const char *consumer, int flags,
+				    int default_val) GPIOD_API;
+
+/**
+ * @brief Request rising edge event notifications on a single line.
+ * @param line GPIO line object.
+ * @param consumer Name of the consumer.
+ * @param flags Additional request flags.
+ * @return 0 if the operation succeeds, -1 on failure.
+ */
+int gpiod_line_request_rising_edge_events_flags(struct gpiod_line *line,
+						const char *consumer,
+						int flags) GPIOD_API;
+
+/**
+ * @brief Request falling edge event notifications on a single line.
+ * @param line GPIO line object.
+ * @param consumer Name of the consumer.
+ * @param flags Additional request flags.
+ * @return 0 if the operation succeeds, -1 on failure.
+ */
+int gpiod_line_request_falling_edge_events_flags(struct gpiod_line *line,
+						 const char *consumer,
+						 int flags) GPIOD_API;
+
+/**
+ * @brief Request all event type notifications on a single line.
+ * @param line GPIO line object.
+ * @param consumer Name of the consumer.
+ * @param flags Additional request flags.
+ * @return 0 if the operation succeeds, -1 on failure.
+ */
+int gpiod_line_request_both_edges_events_flags(struct gpiod_line *line,
+					       const char *consumer,
+					       int flags) GPIOD_API;
+
+/**
+ * @brief Reserve a set of GPIO lines.
+ * @param bulk Set of GPIO lines to reserve.
+ * @param config Request options.
+ * @param default_vals Initial line values - only relevant if we're setting
+ *                     the direction to output.
+ * @return 0 if the all lines were properly requested. In case of an error
+ *         this routine returns -1 and sets the last error number.
+ *
+ * If this routine succeeds, the caller takes ownership of the GPIO lines
+ * until they're released. All the requested lines must be prodivided by the
+ * same gpiochip.
+ */
+int gpiod_line_request_bulk(struct gpiod_line_bulk *bulk,
+			    const struct gpiod_line_request_config *config,
+			    const int *default_vals) GPIOD_API;
+
+/**
+ * @brief Reserve a set of GPIO lines, set the direction to input.
+ * @param bulk Set of GPIO lines to reserve.
+ * @param consumer Name of the consumer.
+ * @return 0 if the lines were properly reserved, -1 on failure.
+ */
+int gpiod_line_request_bulk_input(struct gpiod_line_bulk *bulk,
+				  const char *consumer) GPIOD_API;
+
+/**
+ * @brief Reserve a set of GPIO lines, set the direction to output.
+ * @param bulk Set of GPIO lines to reserve.
+ * @param consumer Name of the consumer.
+ * @param default_vals Initial line values.
+ * @return 0 if the lines were properly reserved, -1 on failure.
+ */
+int gpiod_line_request_bulk_output(struct gpiod_line_bulk *bulk,
+				   const char *consumer,
+				   const int *default_vals) GPIOD_API;
+
+/**
+ * @brief Request rising edge event notifications on a set of lines.
+ * @param bulk Set of GPIO lines to request.
+ * @param consumer Name of the consumer.
+ * @return 0 if the operation succeeds, -1 on failure.
+ */
+int gpiod_line_request_bulk_rising_edge_events(struct gpiod_line_bulk *bulk,
+					       const char *consumer) GPIOD_API;
+
+/**
+ * @brief Request falling edge event notifications on a set of lines.
+ * @param bulk Set of GPIO lines to request.
+ * @param consumer Name of the consumer.
+ * @return 0 if the operation succeeds, -1 on failure.
+ */
+int gpiod_line_request_bulk_falling_edge_events(struct gpiod_line_bulk *bulk,
+						const char *consumer) GPIOD_API;
+
+/**
+ * @brief Request all event type notifications on a set of lines.
+ * @param bulk Set of GPIO lines to request.
+ * @param consumer Name of the consumer.
+ * @return 0 if the operation succeeds, -1 on failure.
+ */
+int gpiod_line_request_bulk_both_edges_events(struct gpiod_line_bulk *bulk,
+					      const char *consumer) GPIOD_API;
+
+/**
+ * @brief Reserve a set of GPIO lines, set the direction to input.
+ * @param bulk Set of GPIO lines to reserve.
+ * @param consumer Name of the consumer.
+ * @param flags Additional request flags.
+ * @return 0 if the lines were properly reserved, -1 on failure.
+ */
+int gpiod_line_request_bulk_input_flags(struct gpiod_line_bulk *bulk,
+					const char *consumer,
+					int flags) GPIOD_API;
+
+/**
+ * @brief Reserve a set of GPIO lines, set the direction to output.
+ * @param bulk Set of GPIO lines to reserve.
+ * @param consumer Name of the consumer.
+ * @param flags Additional request flags.
+ * @param default_vals Initial line values.
+ * @return 0 if the lines were properly reserved, -1 on failure.
+ */
+int gpiod_line_request_bulk_output_flags(struct gpiod_line_bulk *bulk,
+					 const char *consumer, int flags,
+					 const int *default_vals) GPIOD_API;
+
+/**
+ * @brief Request rising edge event notifications on a set of lines.
+ * @param bulk Set of GPIO lines to request.
+ * @param consumer Name of the consumer.
+ * @param flags Additional request flags.
+ * @return 0 if the operation succeeds, -1 on failure.
+ */
+int gpiod_line_request_bulk_rising_edge_events_flags(
+					struct gpiod_line_bulk *bulk,
+					const char *consumer,
+					int flags) GPIOD_API;
+
+/**
+ * @brief Request falling edge event notifications on a set of lines.
+ * @param bulk Set of GPIO lines to request.
+ * @param consumer Name of the consumer.
+ * @param flags Additional request flags.
+ * @return 0 if the operation succeeds, -1 on failure.
+ */
+int gpiod_line_request_bulk_falling_edge_events_flags(
+					struct gpiod_line_bulk *bulk,
+					const char *consumer,
+					int flags) GPIOD_API;
+
+/**
+ * @brief Request all event type notifications on a set of lines.
+ * @param bulk Set of GPIO lines to request.
+ * @param consumer Name of the consumer.
+ * @param flags Additional request flags.
+ * @return 0 if the operation succeeds, -1 on failure.
+ */
+int gpiod_line_request_bulk_both_edges_events_flags(
+					struct gpiod_line_bulk *bulk,
+					const char *consumer,
+					int flags) GPIOD_API;
+
+/**
+ * @brief Release a previously reserved line.
+ * @param line GPIO line object.
+ */
+void gpiod_line_release(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Release a set of previously reserved lines.
+ * @param bulk Set of GPIO lines to release.
+ *
+ * If the lines were not previously requested together, the behavior is
+ * undefined.
+ */
+void gpiod_line_release_bulk(struct gpiod_line_bulk *bulk) GPIOD_API;
+
+/**
+ * @brief Check if the calling user has ownership of this line.
+ * @param line GPIO line object.
+ * @return True if given line was requested, false otherwise.
+ */
+bool gpiod_line_is_requested(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Check if the calling user has neither requested ownership of this
+ *        line nor configured any event notifications.
+ * @param line GPIO line object.
+ * @return True if given line is free, false otherwise.
+ */
+bool gpiod_line_is_free(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @}
+ *
+ * @defgroup __line_value__ Reading & setting line values
+ * @{
+ */
+
+/**
+ * @brief Read current value of a single GPIO line.
+ * @param line GPIO line object.
+ * @return 0 or 1 if the operation succeeds. On error this routine returns -1
+ *         and sets the last error number.
+ */
+int gpiod_line_get_value(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Read current values of a set of GPIO lines.
+ * @param bulk Set of GPIO lines to reserve.
+ * @param values An array big enough to hold line_bulk->num_lines values.
+ * @return 0 is the operation succeeds. In case of an error this routine
+ *         returns -1 and sets the last error number.
+ *
+ * If succeeds, this routine fills the values array with a set of values in
+ * the same order, the lines are added to line_bulk. If the lines were not
+ * previously requested together, the behavior is undefined.
+ */
+int gpiod_line_get_value_bulk(struct gpiod_line_bulk *bulk,
+			      int *values) GPIOD_API;
+
+/**
+ * @brief Set the value of a single GPIO line.
+ * @param line GPIO line object.
+ * @param value New value.
+ * @return 0 is the operation succeeds. In case of an error this routine
+ *         returns -1 and sets the last error number.
+ */
+int gpiod_line_set_value(struct gpiod_line *line, int value) GPIOD_API;
+
+/**
+ * @brief Set the values of a set of GPIO lines.
+ * @param bulk Set of GPIO lines to reserve.
+ * @param values An array holding line_bulk->num_lines new values for lines.
+ * @return 0 is the operation succeeds. In case of an error this routine
+ *         returns -1 and sets the last error number.
+ *
+ * If the lines were not previously requested together, the behavior is
+ * undefined.
+ */
+int gpiod_line_set_value_bulk(struct gpiod_line_bulk *bulk,
+			      const int *values) GPIOD_API;
+
+/**
+ * @}
+ *
+ * @defgroup __line_event__ Line events handling
+ * @{
+ */
+
+/**
+ * @brief Event types.
+ */
+enum {
+	GPIOD_LINE_EVENT_RISING_EDGE = 1,
+	/**< Rising edge event. */
+	GPIOD_LINE_EVENT_FALLING_EDGE,
+	/**< Falling edge event. */
+};
+
+/**
+ * @brief Structure holding event info.
+ */
+struct gpiod_line_event {
+	struct timespec ts;
+	/**< Best estimate of time of event occurrence. */
+	int event_type;
+	/**< Type of the event that occurred. */
+};
+
+/**
+ * @brief Wait for an event on a single line.
+ * @param line GPIO line object.
+ * @param timeout Wait time limit.
+ * @return 0 if wait timed out, -1 if an error occurred, 1 if an event
+ *         occurred.
+ */
+int gpiod_line_event_wait(struct gpiod_line *line,
+			  const struct timespec *timeout) GPIOD_API;
+
+/**
+ * @brief Wait for events on a set of lines.
+ * @param bulk Set of GPIO lines to monitor.
+ * @param timeout Wait time limit.
+ * @param event_bulk Bulk object in which to store the line handles on which
+ *                   events occurred. Can be NULL.
+ * @return 0 if wait timed out, -1 if an error occurred, 1 if at least one
+ *         event occurred.
+ */
+int gpiod_line_event_wait_bulk(struct gpiod_line_bulk *bulk,
+			       const struct timespec *timeout,
+			       struct gpiod_line_bulk *event_bulk) GPIOD_API;
+
+/**
+ * @brief Read the last event from the GPIO line.
+ * @param line GPIO line object.
+ * @param event Buffer to which the event data will be copied.
+ * @return 0 if the event was read correctly, -1 on error.
+ * @note This function will block if no event was queued for this line.
+ */
+int gpiod_line_event_read(struct gpiod_line *line,
+			  struct gpiod_line_event *event) GPIOD_API;
+
+/**
+ * @brief Get the event file descriptor.
+ * @param line GPIO line object.
+ * @return Number of the event file descriptor or -1 if the user tries to
+ *         retrieve the descriptor from a line that wasn't configured for
+ *         event monitoring.
+ *
+ * Users may want to poll the event file descriptor on their own. This routine
+ * allows to access it.
+ */
+int gpiod_line_event_get_fd(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Read the last GPIO event directly from a file descriptor.
+ * @param fd File descriptor.
+ * @param event Buffer in which the event data will be stored.
+ * @return 0 if the event was read correctly, -1 on error.
+ *
+ * Users who directly poll the file descriptor for incoming events can also
+ * directly read the event data from it using this routine. This function
+ * translates the kernel representation of the event to the libgpiod format.
+ */
+int gpiod_line_event_read_fd(int fd, struct gpiod_line_event *event) GPIOD_API;
+
+/**
+ * @}
+ *
+ * @defgroup __line_misc__ Misc line functions
+ * @{
+ */
+
+/**
+ * @brief Get a GPIO line handle by GPIO chip description and offset.
+ * @param device String describing the gpiochip.
+ * @param offset The offset of the GPIO line.
+ * @return GPIO line handle or NULL if an error occurred.
+ *
+ * This routine provides a shorter alternative to calling
+ * ::gpiod_chip_open_lookup and ::gpiod_chip_get_line.
+ *
+ * If this function succeeds, the caller is responsible for closing the
+ * associated GPIO chip.
+ */
+struct gpiod_line *
+gpiod_line_get(const char *device, unsigned int offset) GPIOD_API;
+
+/**
+ * @brief Find a GPIO line by its name.
+ * @param name Name of the GPIO line.
+ * @return Returns the GPIO line handle if the line exists in the system or
+ *         NULL if it couldn't be located or an error occurred.
+ *
+ * If this routine succeeds, the user must manually close the GPIO chip owning
+ * this line to avoid memory leaks. If the line could not be found, this
+ * functions sets errno to ENOENT.
+ */
+struct gpiod_line *gpiod_line_find(const char *name) GPIOD_API;
+
+/**
+ * @brief Close a GPIO chip owning this line and release all resources.
+ * @param line GPIO line object
+ *
+ * After this function returns, the line must no longer be used.
+ */
+void gpiod_line_close_chip(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @brief Get the handle to the GPIO chip controlling this line.
+ * @param line The GPIO line object.
+ * @return Pointer to the GPIO chip handle controlling this line.
+ */
+struct gpiod_chip *gpiod_line_get_chip(struct gpiod_line *line) GPIOD_API;
+
+/**
+ * @}
+ *
+ * @}
+ *
+ * @defgroup __iterators__ Iterators for GPIO chips and lines
+ * @{
+ *
+ * These functions and data structures allow easy iterating over GPIO
+ * chips and lines.
+ */
+
+/**
+ * @brief Create a new gpiochip iterator.
+ * @return Pointer to a new chip iterator object or NULL if an error occurred.
+ *
+ * Internally this routine scans the /dev/ directory for GPIO chip device
+ * files, opens them and stores their the handles until ::gpiod_chip_iter_free
+ * or ::gpiod_chip_iter_free_noclose is called.
+ */
+struct gpiod_chip_iter *gpiod_chip_iter_new(void) GPIOD_API;
+
+/**
+ * @brief Release all resources allocated for the gpiochip iterator and close
+ *        the most recently opened gpiochip (if any).
+ * @param iter The gpiochip iterator object.
+ */
+void gpiod_chip_iter_free(struct gpiod_chip_iter *iter) GPIOD_API;
+
+/**
+ * @brief Release all resources allocated for the gpiochip iterator but
+ *        don't close the most recently opened gpiochip (if any).
+ * @param iter The gpiochip iterator object.
+ *
+ * Users may want to break the loop when iterating over gpiochips and keep
+ * the most recently opened chip active while freeing the iterator data.
+ * This routine enables that.
+ */
+void gpiod_chip_iter_free_noclose(struct gpiod_chip_iter *iter) GPIOD_API;
+
+/**
+ * @brief Get the next gpiochip handle.
+ * @param iter The gpiochip iterator object.
+ * @return Pointer to the next open gpiochip handle or NULL if no more chips
+ *         are present in the system.
+ * @note The previous chip handle will be closed using ::gpiod_chip_iter_free.
+ */
+struct gpiod_chip *
+gpiod_chip_iter_next(struct gpiod_chip_iter *iter) GPIOD_API;
+
+/**
+ * @brief Get the next gpiochip handle without closing the previous one.
+ * @param iter The gpiochip iterator object.
+ * @return Pointer to the next open gpiochip handle or NULL if no more chips
+ *         are present in the system.
+ * @note This function works just like ::gpiod_chip_iter_next but doesn't
+ *       close the most recently opened chip handle.
+ */
+struct gpiod_chip *
+gpiod_chip_iter_next_noclose(struct gpiod_chip_iter *iter) GPIOD_API;
+
+/**
+ * @brief Iterate over all GPIO chips present in the system.
+ * @param iter An initialized GPIO chip iterator.
+ * @param chip Pointer to a GPIO chip handle. On each iteration the newly
+ *             opened chip handle is assigned to this argument.
+ *
+ * The user must not close the GPIO chip manually - instead the previous chip
+ * handle is closed automatically on the next iteration. The last chip to be
+ * opened is closed internally by ::gpiod_chip_iter_free.
+ */
+#define gpiod_foreach_chip(iter, chip)					\
+	for ((chip) = gpiod_chip_iter_next(iter);			\
+	     (chip);							\
+	     (chip) = gpiod_chip_iter_next(iter))
+
+/**
+ * @brief Iterate over all chips present in the system without closing them.
+ * @param iter An initialized GPIO chip iterator.
+ * @param chip Pointer to a GPIO chip handle. On each iteration the newly
+ *             opened chip handle is assigned to this argument.
+ *
+ * The user must close all the GPIO chips manually after use, until then, the
+ * chips remain open. Free the iterator by calling
+ * ::gpiod_chip_iter_free_noclose to avoid closing the last chip automatically.
+ */
+#define gpiod_foreach_chip_noclose(iter, chip)				\
+	for ((chip) = gpiod_chip_iter_next_noclose(iter);		\
+	     (chip);							\
+	     (chip) = gpiod_chip_iter_next_noclose(iter))
+
+/**
+ * @brief Create a new line iterator.
+ * @param chip Active gpiochip handle over the lines of which we want
+ *             to iterate.
+ * @return New line iterator or NULL if an error occurred.
+ */
+struct gpiod_line_iter *
+gpiod_line_iter_new(struct gpiod_chip *chip) GPIOD_API;
+
+/**
+ * @brief Free all resources associated with a GPIO line iterator.
+ * @param iter Line iterator object.
+ */
+void gpiod_line_iter_free(struct gpiod_line_iter *iter) GPIOD_API;
+
+/**
+ * @brief Get the next GPIO line handle.
+ * @param iter The GPIO line iterator object.
+ * @return Pointer to the next GPIO line handle or NULL if there are no more
+ *         lines left.
+ */
+struct gpiod_line *
+gpiod_line_iter_next(struct gpiod_line_iter *iter) GPIOD_API;
+
+/**
+ * @brief Iterate over all GPIO lines of a single chip.
+ * @param iter An initialized GPIO line iterator.
+ * @param line Pointer to a GPIO line handle - on each iteration, the
+ *             next GPIO line will be assigned to this argument.
+ */
+#define gpiod_foreach_line(iter, line)					\
+	for ((line) = gpiod_line_iter_next(iter);			\
+	     (line);							\
+	     (line) = gpiod_line_iter_next(iter))
+
+/**
+ * @}
+ *
+ * @defgroup __misc__ Stuff that didn't fit anywhere else
+ * @{
+ *
+ * Various libgpiod-related functions.
+ */
+
+/**
+ * @brief Get the API version of the library as a human-readable string.
+ * @return Human-readable string containing the library version.
+ */
+const char *gpiod_version_string(void) GPIOD_API;
+
+/**
+ * @}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* __LIBGPIOD_GPIOD_H__ */
diff --git a/lib/Makefile.am b/lib/Makefile.am
new file mode 100644
index 0000000..3f797c4
--- /dev/null
+++ b/lib/Makefile.am
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+lib_LTLIBRARIES = libgpiod.la
+libgpiod_la_SOURCES = core.c ctxless.c helpers.c iter.c misc.c
+libgpiod_la_CFLAGS = -Wall -Wextra -g
+libgpiod_la_CFLAGS += -fvisibility=hidden -I$(top_srcdir)/include/
+libgpiod_la_CFLAGS += -include $(top_builddir)/config.h
+libgpiod_la_LDFLAGS = -version-info $(subst .,:,$(ABI_VERSION))
diff --git a/lib/core.c b/lib/core.c
new file mode 100644
index 0000000..05e5a46
--- /dev/null
+++ b/lib/core.c
@@ -0,0 +1,861 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Low-level, core library code. */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <gpiod.h>
+#include <linux/gpio.h>
+#include <poll.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+enum {
+	LINE_FREE = 0,
+	LINE_REQUESTED_VALUES,
+	LINE_REQUESTED_EVENTS,
+};
+
+struct line_fd_handle {
+	int fd;
+	int refcount;
+};
+
+struct gpiod_line {
+	unsigned int offset;
+	int direction;
+	int active_state;
+	bool used;
+	bool open_source;
+	bool open_drain;
+
+	int state;
+	bool up_to_date;
+
+	struct gpiod_chip *chip;
+	struct line_fd_handle *fd_handle;
+
+	char name[32];
+	char consumer[32];
+};
+
+struct gpiod_chip {
+	struct gpiod_line **lines;
+	unsigned int num_lines;
+
+	int fd;
+
+	char name[32];
+	char label[32];
+};
+
+static bool is_gpiochip_cdev(const char *path)
+{
+	char *name, *pathcpy, *sysfsp, sysfsdev[16], devstr[16];
+	struct stat statbuf;
+	bool ret = false;
+	int rv, fd;
+	ssize_t rd;
+
+	rv = lstat(path, &statbuf);
+	if (rv)
+		goto out;
+
+	/* Is it a character device? */
+	if (!S_ISCHR(statbuf.st_mode)) {
+		/*
+		 * Passing a file descriptor not associated with a character
+		 * device to ioctl() makes it set errno to ENOTTY. Let's do
+		 * the same in order to stay compatible with the versions of
+		 * libgpiod from before the introduction of this routine.
+		 */
+		errno = ENOTTY;
+		goto out;
+	}
+
+	/* Get the basename. */
+	pathcpy = strdup(path);
+	if (!pathcpy)
+		goto out;
+
+	name = basename(pathcpy);
+
+	/* Do we have a corresponding sysfs attribute? */
+	rv = asprintf(&sysfsp, "/sys/bus/gpio/devices/%s/dev", name);
+	if (rv < 0)
+		goto out_free_pathcpy;
+
+	if (access(sysfsp, R_OK) != 0) {
+		/*
+		 * This is a character device but not the one we're after.
+		 * Before the introduction of this function, we'd fail with
+		 * ENOTTY on the first GPIO ioctl() call for this file
+		 * descriptor. Let's stay compatible here and keep returning
+		 * the same error code.
+		 */
+		errno = ENOTTY;
+		goto out_free_sysfsp;
+	}
+
+	/*
+	 * Make sure the major and minor numbers of the character device
+	 * correspond with the ones in the dev attribute in sysfs.
+	 */
+	snprintf(devstr, sizeof(devstr), "%u:%u",
+		 major(statbuf.st_rdev), minor(statbuf.st_rdev));
+
+	fd = open(sysfsp, O_RDONLY);
+	if (fd < 0)
+		goto out_free_sysfsp;
+
+	memset(sysfsdev, 0, sizeof(sysfsdev));
+	rd = read(fd, sysfsdev, strlen(devstr));
+	close(fd);
+	if (rd < 0)
+		goto out_free_sysfsp;
+
+	if (strcmp(sysfsdev, devstr) != 0) {
+		errno = ENODEV;
+		goto out_free_sysfsp;
+	}
+
+	ret = true;
+
+out_free_sysfsp:
+	free(sysfsp);
+out_free_pathcpy:
+	free(pathcpy);
+out:
+	return ret;
+}
+
+struct gpiod_chip *gpiod_chip_open(const char *path)
+{
+	struct gpiochip_info info;
+	struct gpiod_chip *chip;
+	int rv, fd;
+
+	fd = open(path, O_RDWR | O_CLOEXEC);
+	if (fd < 0)
+		return NULL;
+
+	/*
+	 * We were able to open the file but is it really a gpiochip character
+	 * device?
+	 */
+	if (!is_gpiochip_cdev(path)) {
+		close(fd);
+		return NULL;
+	}
+
+	chip = malloc(sizeof(*chip));
+	if (!chip)
+		goto err_close_fd;
+
+	memset(chip, 0, sizeof(*chip));
+	memset(&info, 0, sizeof(info));
+
+	rv = ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &info);
+	if (rv < 0)
+		goto err_free_chip;
+
+	chip->fd = fd;
+	chip->num_lines = info.lines;
+
+	/*
+	 * GPIO device must have a name - don't bother checking this field. In
+	 * the worst case (would have to be a weird kernel bug) it'll be empty.
+	 */
+	strncpy(chip->name, info.name, sizeof(chip->name));
+
+	/*
+	 * The kernel sets the label of a GPIO device to "unknown" if it
+	 * hasn't been defined in DT, board file etc. On the off-chance that
+	 * we got an empty string, do the same.
+	 */
+	if (info.label[0] == '\0')
+		strncpy(chip->label, "unknown", sizeof(chip->label));
+	else
+		strncpy(chip->label, info.label, sizeof(chip->label));
+
+	return chip;
+
+err_free_chip:
+	free(chip);
+err_close_fd:
+	close(fd);
+
+	return NULL;
+}
+
+void gpiod_chip_close(struct gpiod_chip *chip)
+{
+	struct gpiod_line *line;
+	unsigned int i;
+
+	if (chip->lines) {
+		for (i = 0; i < chip->num_lines; i++) {
+			line = chip->lines[i];
+			if (line) {
+				gpiod_line_release(line);
+				free(line);
+			}
+		}
+
+		free(chip->lines);
+	}
+
+	close(chip->fd);
+	free(chip);
+}
+
+const char *gpiod_chip_name(struct gpiod_chip *chip)
+{
+	return chip->name;
+}
+
+const char *gpiod_chip_label(struct gpiod_chip *chip)
+{
+	return chip->label;
+}
+
+unsigned int gpiod_chip_num_lines(struct gpiod_chip *chip)
+{
+	return chip->num_lines;
+}
+
+struct gpiod_line *
+gpiod_chip_get_line(struct gpiod_chip *chip, unsigned int offset)
+{
+	struct gpiod_line *line;
+	int rv;
+
+	if (offset >= chip->num_lines) {
+		errno = EINVAL;
+		return NULL;
+	}
+
+	if (!chip->lines) {
+		chip->lines = calloc(chip->num_lines,
+				     sizeof(struct gpiod_line *));
+		if (!chip->lines)
+			return NULL;
+	}
+
+	if (!chip->lines[offset]) {
+		line = malloc(sizeof(*line));
+		if (!line)
+			return NULL;
+
+		memset(line, 0, sizeof(*line));
+
+		line->offset = offset;
+		line->chip = chip;
+
+		chip->lines[offset] = line;
+	} else {
+		line = chip->lines[offset];
+	}
+
+	rv = gpiod_line_update(line);
+	if (rv < 0)
+		return NULL;
+
+	return line;
+}
+
+static struct line_fd_handle *line_make_fd_handle(int fd)
+{
+	struct line_fd_handle *handle;
+
+	handle = malloc(sizeof(*handle));
+	if (!handle)
+		return NULL;
+
+	handle->fd = fd;
+	handle->refcount = 0;
+
+	return handle;
+}
+
+static void line_fd_incref(struct gpiod_line *line)
+{
+	line->fd_handle->refcount++;
+}
+
+static void line_fd_decref(struct gpiod_line *line)
+{
+	struct line_fd_handle *handle = line->fd_handle;
+
+	handle->refcount--;
+
+	if (handle->refcount == 0) {
+		close(handle->fd);
+		free(handle);
+		line->fd_handle = NULL;
+	}
+}
+
+static void line_set_fd(struct gpiod_line *line, struct line_fd_handle *handle)
+{
+	line->fd_handle = handle;
+	line_fd_incref(line);
+}
+
+static int line_get_fd(struct gpiod_line *line)
+{
+	return line->fd_handle->fd;
+}
+
+static void line_maybe_update(struct gpiod_line *line)
+{
+	int rv;
+
+	rv = gpiod_line_update(line);
+	if (rv < 0)
+		line->up_to_date = false;
+}
+
+struct gpiod_chip *gpiod_line_get_chip(struct gpiod_line *line)
+{
+	return line->chip;
+}
+
+unsigned int gpiod_line_offset(struct gpiod_line *line)
+{
+	return line->offset;
+}
+
+const char *gpiod_line_name(struct gpiod_line *line)
+{
+	return line->name[0] == '\0' ? NULL : line->name;
+}
+
+const char *gpiod_line_consumer(struct gpiod_line *line)
+{
+	return line->consumer[0] == '\0' ? NULL : line->consumer;
+}
+
+int gpiod_line_direction(struct gpiod_line *line)
+{
+	return line->direction;
+}
+
+int gpiod_line_active_state(struct gpiod_line *line)
+{
+	return line->active_state;
+}
+
+bool gpiod_line_is_used(struct gpiod_line *line)
+{
+	return line->used;
+}
+
+bool gpiod_line_is_open_drain(struct gpiod_line *line)
+{
+	return line->open_drain;
+}
+
+bool gpiod_line_is_open_source(struct gpiod_line *line)
+{
+	return line->open_source;
+}
+
+bool gpiod_line_needs_update(struct gpiod_line *line)
+{
+	return !line->up_to_date;
+}
+
+int gpiod_line_update(struct gpiod_line *line)
+{
+	struct gpioline_info info;
+	int rv;
+
+	memset(&info, 0, sizeof(info));
+	info.line_offset = line->offset;
+
+	rv = ioctl(line->chip->fd, GPIO_GET_LINEINFO_IOCTL, &info);
+	if (rv < 0)
+		return -1;
+
+	line->direction = info.flags & GPIOLINE_FLAG_IS_OUT
+						? GPIOD_LINE_DIRECTION_OUTPUT
+						: GPIOD_LINE_DIRECTION_INPUT;
+	line->active_state = info.flags & GPIOLINE_FLAG_ACTIVE_LOW
+						? GPIOD_LINE_ACTIVE_STATE_LOW
+						: GPIOD_LINE_ACTIVE_STATE_HIGH;
+
+	line->used = info.flags & GPIOLINE_FLAG_KERNEL;
+	line->open_drain = info.flags & GPIOLINE_FLAG_OPEN_DRAIN;
+	line->open_source = info.flags & GPIOLINE_FLAG_OPEN_SOURCE;
+
+	strncpy(line->name, info.name, sizeof(line->name));
+	strncpy(line->consumer, info.consumer, sizeof(line->consumer));
+
+	line->up_to_date = true;
+
+	return 0;
+}
+
+static bool line_bulk_same_chip(struct gpiod_line_bulk *bulk)
+{
+	struct gpiod_line *first_line, *line;
+	struct gpiod_chip *first_chip, *chip;
+	unsigned int i;
+
+	if (bulk->num_lines == 1)
+		return true;
+
+	first_line = gpiod_line_bulk_get_line(bulk, 0);
+	first_chip = gpiod_line_get_chip(first_line);
+
+	for (i = 1; i < bulk->num_lines; i++) {
+		line = bulk->lines[i];
+		chip = gpiod_line_get_chip(line);
+
+		if (first_chip != chip) {
+			errno = EINVAL;
+			return false;
+		}
+	}
+
+	return true;
+}
+
+static bool line_bulk_all_requested(struct gpiod_line_bulk *bulk)
+{
+	struct gpiod_line *line, **lineptr;
+
+	gpiod_line_bulk_foreach_line(bulk, line, lineptr) {
+		if (!gpiod_line_is_requested(line)) {
+			errno = EPERM;
+			return false;
+		}
+	}
+
+	return true;
+}
+
+static bool line_bulk_all_free(struct gpiod_line_bulk *bulk)
+{
+	struct gpiod_line *line, **lineptr;
+
+	gpiod_line_bulk_foreach_line(bulk, line, lineptr) {
+		if (!gpiod_line_is_free(line)) {
+			errno = EBUSY;
+			return false;
+		}
+	}
+
+	return true;
+}
+
+static int line_request_values(struct gpiod_line_bulk *bulk,
+			       const struct gpiod_line_request_config *config,
+			       const int *default_vals)
+{
+	struct gpiod_line *line, **lineptr;
+	struct line_fd_handle *line_fd;
+	struct gpiohandle_request req;
+	unsigned int i;
+	int rv, fd;
+
+	if ((config->request_type != GPIOD_LINE_REQUEST_DIRECTION_OUTPUT) &&
+	    (config->flags & (GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN |
+			      GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE))) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	if ((config->flags & GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN) &&
+	    (config->flags & GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE)) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	memset(&req, 0, sizeof(req));
+
+	if (config->flags & GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN)
+		req.flags |= GPIOHANDLE_REQUEST_OPEN_DRAIN;
+	if (config->flags & GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE)
+		req.flags |= GPIOHANDLE_REQUEST_OPEN_SOURCE;
+	if (config->flags & GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW)
+		req.flags |= GPIOHANDLE_REQUEST_ACTIVE_LOW;
+
+	if (config->request_type == GPIOD_LINE_REQUEST_DIRECTION_INPUT)
+		req.flags |= GPIOHANDLE_REQUEST_INPUT;
+	else if (config->request_type == GPIOD_LINE_REQUEST_DIRECTION_OUTPUT)
+		req.flags |= GPIOHANDLE_REQUEST_OUTPUT;
+
+	req.lines = gpiod_line_bulk_num_lines(bulk);
+
+	gpiod_line_bulk_foreach_line_off(bulk, line, i) {
+		req.lineoffsets[i] = gpiod_line_offset(line);
+		if (config->request_type ==
+				GPIOD_LINE_REQUEST_DIRECTION_OUTPUT &&
+		    default_vals)
+			req.default_values[i] = !!default_vals[i];
+	}
+
+	if (config->consumer)
+		strncpy(req.consumer_label, config->consumer,
+			sizeof(req.consumer_label) - 1);
+
+	line = gpiod_line_bulk_get_line(bulk, 0);
+	fd = line->chip->fd;
+
+	rv = ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &req);
+	if (rv < 0)
+		return -1;
+
+	line_fd = line_make_fd_handle(req.fd);
+	if (!line_fd)
+		return -1;
+
+	gpiod_line_bulk_foreach_line(bulk, line, lineptr) {
+		line->state = LINE_REQUESTED_VALUES;
+		line_set_fd(line, line_fd);
+		line_maybe_update(line);
+	}
+
+	return 0;
+}
+
+static int line_request_event_single(struct gpiod_line *line,
+			const struct gpiod_line_request_config *config)
+{
+	struct line_fd_handle *line_fd;
+	struct gpioevent_request req;
+	int rv;
+
+	memset(&req, 0, sizeof(req));
+
+	if (config->consumer)
+		strncpy(req.consumer_label, config->consumer,
+			sizeof(req.consumer_label) - 1);
+
+	req.lineoffset = gpiod_line_offset(line);
+	req.handleflags |= GPIOHANDLE_REQUEST_INPUT;
+
+	if (config->flags & GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN)
+		req.handleflags |= GPIOHANDLE_REQUEST_OPEN_DRAIN;
+	if (config->flags & GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE)
+		req.handleflags |= GPIOHANDLE_REQUEST_OPEN_SOURCE;
+	if (config->flags & GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW)
+		req.handleflags |= GPIOHANDLE_REQUEST_ACTIVE_LOW;
+
+	if (config->request_type == GPIOD_LINE_REQUEST_EVENT_RISING_EDGE)
+		req.eventflags |= GPIOEVENT_REQUEST_RISING_EDGE;
+	else if (config->request_type == GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE)
+		req.eventflags |= GPIOEVENT_REQUEST_FALLING_EDGE;
+	else if (config->request_type == GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES)
+		req.eventflags |= GPIOEVENT_REQUEST_BOTH_EDGES;
+
+	rv = ioctl(line->chip->fd, GPIO_GET_LINEEVENT_IOCTL, &req);
+	if (rv < 0)
+		return -1;
+
+	line_fd = line_make_fd_handle(req.fd);
+	if (!line_fd)
+		return -1;
+
+	line->state = LINE_REQUESTED_EVENTS;
+	line_set_fd(line, line_fd);
+	line_maybe_update(line);
+
+	return 0;
+}
+
+static int line_request_events(struct gpiod_line_bulk *bulk,
+			       const struct gpiod_line_request_config *config)
+{
+	struct gpiod_line *line;
+	unsigned int off;
+	int rv, rev;
+
+	gpiod_line_bulk_foreach_line_off(bulk, line, off) {
+		rv = line_request_event_single(line, config);
+		if (rv) {
+			for (rev = off - 1; rev >= 0; rev--) {
+				line = gpiod_line_bulk_get_line(bulk, rev);
+				gpiod_line_release(line);
+			}
+
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
+int gpiod_line_request(struct gpiod_line *line,
+		       const struct gpiod_line_request_config *config,
+		       int default_val)
+{
+	struct gpiod_line_bulk bulk;
+
+	gpiod_line_bulk_init(&bulk);
+	gpiod_line_bulk_add(&bulk, line);
+
+	return gpiod_line_request_bulk(&bulk, config, &default_val);
+}
+
+static bool line_request_is_direction(int request)
+{
+	return request == GPIOD_LINE_REQUEST_DIRECTION_AS_IS ||
+	       request == GPIOD_LINE_REQUEST_DIRECTION_INPUT ||
+	       request == GPIOD_LINE_REQUEST_DIRECTION_OUTPUT;
+}
+
+static bool line_request_is_events(int request)
+{
+	return request == GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE ||
+	       request == GPIOD_LINE_REQUEST_EVENT_RISING_EDGE ||
+	       request == GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES;
+}
+
+int gpiod_line_request_bulk(struct gpiod_line_bulk *bulk,
+			    const struct gpiod_line_request_config *config,
+			    const int *default_vals)
+{
+	if (!line_bulk_same_chip(bulk) || !line_bulk_all_free(bulk))
+		return -1;
+
+	if (line_request_is_direction(config->request_type))
+		return line_request_values(bulk, config, default_vals);
+	else if (line_request_is_events(config->request_type))
+		return line_request_events(bulk, config);
+
+	errno = EINVAL;
+	return -1;
+}
+
+void gpiod_line_release(struct gpiod_line *line)
+{
+	struct gpiod_line_bulk bulk;
+
+	gpiod_line_bulk_init(&bulk);
+	gpiod_line_bulk_add(&bulk, line);
+
+	gpiod_line_release_bulk(&bulk);
+}
+
+void gpiod_line_release_bulk(struct gpiod_line_bulk *bulk)
+{
+	struct gpiod_line *line, **lineptr;
+
+	gpiod_line_bulk_foreach_line(bulk, line, lineptr) {
+		if (line->state != LINE_FREE) {
+			line_fd_decref(line);
+			line->state = LINE_FREE;
+		}
+	}
+}
+
+bool gpiod_line_is_requested(struct gpiod_line *line)
+{
+	return (line->state == LINE_REQUESTED_VALUES ||
+		line->state == LINE_REQUESTED_EVENTS);
+}
+
+bool gpiod_line_is_free(struct gpiod_line *line)
+{
+	return line->state == LINE_FREE;
+}
+
+int gpiod_line_get_value(struct gpiod_line *line)
+{
+	struct gpiod_line_bulk bulk;
+	int rv, value;
+
+	gpiod_line_bulk_init(&bulk);
+	gpiod_line_bulk_add(&bulk, line);
+
+	rv = gpiod_line_get_value_bulk(&bulk, &value);
+	if (rv < 0)
+		return -1;
+
+	return value;
+}
+
+int gpiod_line_get_value_bulk(struct gpiod_line_bulk *bulk, int *values)
+{
+	struct gpiohandle_data data;
+	struct gpiod_line *first;
+	unsigned int i;
+	int rv, fd;
+
+	if (!line_bulk_same_chip(bulk) || !line_bulk_all_requested(bulk))
+		return -1;
+
+	first = gpiod_line_bulk_get_line(bulk, 0);
+
+	memset(&data, 0, sizeof(data));
+
+	fd = line_get_fd(first);
+
+	rv = ioctl(fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &data);
+	if (rv < 0)
+		return -1;
+
+	for (i = 0; i < gpiod_line_bulk_num_lines(bulk); i++)
+		values[i] = data.values[i];
+
+	return 0;
+}
+
+int gpiod_line_set_value(struct gpiod_line *line, int value)
+{
+	struct gpiod_line_bulk bulk;
+
+	gpiod_line_bulk_init(&bulk);
+	gpiod_line_bulk_add(&bulk, line);
+
+	return gpiod_line_set_value_bulk(&bulk, &value);
+}
+
+int gpiod_line_set_value_bulk(struct gpiod_line_bulk *bulk, const int *values)
+{
+	struct gpiohandle_data data;
+	struct gpiod_line *line;
+	unsigned int i;
+	int rv, fd;
+
+	if (!line_bulk_same_chip(bulk) || !line_bulk_all_requested(bulk))
+		return -1;
+
+	memset(&data, 0, sizeof(data));
+
+	for (i = 0; i < gpiod_line_bulk_num_lines(bulk); i++)
+		data.values[i] = (uint8_t)!!values[i];
+
+	line = gpiod_line_bulk_get_line(bulk, 0);
+	fd = line_get_fd(line);
+
+	rv = ioctl(fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
+	if (rv < 0)
+		return -1;
+
+	return 0;
+}
+
+int gpiod_line_event_wait(struct gpiod_line *line,
+			  const struct timespec *timeout)
+{
+	struct gpiod_line_bulk bulk;
+
+	gpiod_line_bulk_init(&bulk);
+	gpiod_line_bulk_add(&bulk, line);
+
+	return gpiod_line_event_wait_bulk(&bulk, timeout, NULL);
+}
+
+int gpiod_line_event_wait_bulk(struct gpiod_line_bulk *bulk,
+			       const struct timespec *timeout,
+			       struct gpiod_line_bulk *event_bulk)
+{
+	struct pollfd fds[GPIOD_LINE_BULK_MAX_LINES];
+	unsigned int off, num_lines;
+	struct gpiod_line *line;
+	int rv;
+
+	if (!line_bulk_same_chip(bulk) || !line_bulk_all_requested(bulk))
+		return -1;
+
+	memset(fds, 0, sizeof(fds));
+	num_lines = gpiod_line_bulk_num_lines(bulk);
+
+	gpiod_line_bulk_foreach_line_off(bulk, line, off) {
+		fds[off].fd = line_get_fd(line);
+		fds[off].events = POLLIN | POLLPRI;
+	}
+
+	rv = ppoll(fds, num_lines, timeout, NULL);
+	if (rv < 0)
+		return -1;
+	else if (rv == 0)
+		return 0;
+
+	if (event_bulk)
+		gpiod_line_bulk_init(event_bulk);
+
+	for (off = 0; off < num_lines; off++) {
+		if (fds[off].revents) {
+			if (fds[off].revents & POLLNVAL) {
+				errno = EINVAL;
+				return -1;
+			}
+
+			if (event_bulk) {
+				line = gpiod_line_bulk_get_line(bulk, off);
+				gpiod_line_bulk_add(event_bulk, line);
+			}
+
+			if (!--rv)
+				break;
+		}
+	}
+
+	return 1;
+}
+
+int gpiod_line_event_read(struct gpiod_line *line,
+			  struct gpiod_line_event *event)
+{
+	int fd;
+
+	if (line->state != LINE_REQUESTED_EVENTS) {
+		errno = EPERM;
+		return -1;
+	}
+
+	fd = line_get_fd(line);
+
+	return gpiod_line_event_read_fd(fd, event);
+}
+
+int gpiod_line_event_get_fd(struct gpiod_line *line)
+{
+	if (line->state != LINE_REQUESTED_EVENTS) {
+		errno = EPERM;
+		return -1;
+	}
+
+	return line_get_fd(line);
+}
+
+int gpiod_line_event_read_fd(int fd, struct gpiod_line_event *event)
+{
+	struct gpioevent_data evdata;
+	ssize_t rd;
+
+	memset(&evdata, 0, sizeof(evdata));
+
+	rd = read(fd, &evdata, sizeof(evdata));
+	if (rd < 0) {
+		return -1;
+	} else if (rd != sizeof(evdata)) {
+		errno = EIO;
+		return -1;
+	}
+
+	event->event_type = evdata.id == GPIOEVENT_EVENT_RISING_EDGE
+						? GPIOD_LINE_EVENT_RISING_EDGE
+						: GPIOD_LINE_EVENT_FALLING_EDGE;
+
+	event->ts.tv_sec = evdata.timestamp / 1000000000ULL;
+	event->ts.tv_nsec = evdata.timestamp % 1000000000ULL;
+
+	return 0;
+}
diff --git a/lib/ctxless.c b/lib/ctxless.c
new file mode 100644
index 0000000..ba85018
--- /dev/null
+++ b/lib/ctxless.c
@@ -0,0 +1,369 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Implementation of the high-level API. */
+
+
+#include <errno.h>
+#include <gpiod.h>
+#include <poll.h>
+#include <stdio.h>
+#include <string.h>
+
+int gpiod_ctxless_get_value(const char *device, unsigned int offset,
+			    bool active_low, const char *consumer)
+{
+	int value, rv;
+
+	rv = gpiod_ctxless_get_value_multiple(device, &offset, &value,
+					      1, active_low, consumer);
+	if (rv < 0)
+		return rv;
+
+	return value;
+}
+
+int gpiod_ctxless_get_value_multiple(const char *device,
+				     const unsigned int *offsets, int *values,
+				     unsigned int num_lines, bool active_low,
+				     const char *consumer)
+{
+	struct gpiod_line_bulk bulk;
+	struct gpiod_chip *chip;
+	struct gpiod_line *line;
+	unsigned int i;
+	int rv, flags;
+
+	if (!num_lines || num_lines > GPIOD_LINE_BULK_MAX_LINES) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	chip = gpiod_chip_open_lookup(device);
+	if (!chip)
+		return -1;
+
+	gpiod_line_bulk_init(&bulk);
+
+	for (i = 0; i < num_lines; i++) {
+		line = gpiod_chip_get_line(chip, offsets[i]);
+		if (!line) {
+			gpiod_chip_close(chip);
+			return -1;
+		}
+
+		gpiod_line_bulk_add(&bulk, line);
+	}
+
+	flags = active_low ? GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW : 0;
+
+	rv = gpiod_line_request_bulk_input_flags(&bulk, consumer, flags);
+	if (rv < 0) {
+		gpiod_chip_close(chip);
+		return -1;
+	}
+
+	memset(values, 0, sizeof(*values) * num_lines);
+	rv = gpiod_line_get_value_bulk(&bulk, values);
+
+	gpiod_chip_close(chip);
+
+	return rv;
+}
+
+int gpiod_ctxless_set_value(const char *device, unsigned int offset, int value,
+			    bool active_low, const char *consumer,
+			    gpiod_ctxless_set_value_cb cb, void *data)
+{
+	return gpiod_ctxless_set_value_multiple(device, &offset, &value, 1,
+						active_low, consumer, cb, data);
+}
+
+int gpiod_ctxless_set_value_multiple(const char *device,
+				     const unsigned int *offsets,
+				     const int *values, unsigned int num_lines,
+				     bool active_low, const char *consumer,
+				     gpiod_ctxless_set_value_cb cb, void *data)
+{
+	struct gpiod_line_bulk bulk;
+	struct gpiod_chip *chip;
+	struct gpiod_line *line;
+	unsigned int i;
+	int rv, flags;
+
+	if (!num_lines || num_lines > GPIOD_LINE_BULK_MAX_LINES) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	chip = gpiod_chip_open_lookup(device);
+	if (!chip)
+		return -1;
+
+	gpiod_line_bulk_init(&bulk);
+
+	for (i = 0; i < num_lines; i++) {
+		line = gpiod_chip_get_line(chip, offsets[i]);
+		if (!line) {
+			gpiod_chip_close(chip);
+			return -1;
+		}
+
+		gpiod_line_bulk_add(&bulk, line);
+	}
+
+	flags = active_low ? GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW : 0;
+
+	rv = gpiod_line_request_bulk_output_flags(&bulk, consumer,
+						  flags, values);
+	if (rv < 0) {
+		gpiod_chip_close(chip);
+		return -1;
+	}
+
+	if (cb)
+		cb(data);
+
+	gpiod_chip_close(chip);
+
+	return 0;
+}
+
+static int basic_event_poll(unsigned int num_lines,
+			    struct gpiod_ctxless_event_poll_fd *fds,
+			    const struct timespec *timeout,
+			    void *data GPIOD_UNUSED)
+{
+	struct pollfd poll_fds[GPIOD_LINE_BULK_MAX_LINES];
+	unsigned int i;
+	int rv, ret;
+
+	if (!num_lines || num_lines > GPIOD_LINE_BULK_MAX_LINES)
+		return GPIOD_CTXLESS_EVENT_POLL_RET_ERR;
+
+	memset(poll_fds, 0, sizeof(poll_fds));
+
+	for (i = 0; i < num_lines; i++) {
+		poll_fds[i].fd = fds[i].fd;
+		poll_fds[i].events = POLLIN | POLLPRI;
+	}
+
+	rv = ppoll(poll_fds, num_lines, timeout, NULL);
+	if (rv < 0) {
+		if (errno == EINTR)
+			return GPIOD_CTXLESS_EVENT_POLL_RET_TIMEOUT;
+		else
+			return GPIOD_CTXLESS_EVENT_POLL_RET_ERR;
+	} else if (rv == 0) {
+		return GPIOD_CTXLESS_EVENT_POLL_RET_TIMEOUT;
+	}
+
+	ret = rv;
+	for (i = 0; i < num_lines; i++) {
+		if (poll_fds[i].revents) {
+			fds[i].event = true;
+			if (!--rv)
+				break;
+		}
+	}
+
+	return ret;
+}
+
+int gpiod_ctxless_event_loop(const char *device, unsigned int offset,
+			     bool active_low, const char *consumer,
+			     const struct timespec *timeout,
+			     gpiod_ctxless_event_poll_cb poll_cb,
+			     gpiod_ctxless_event_handle_cb event_cb,
+			     void *data)
+{
+	return gpiod_ctxless_event_monitor(device,
+					   GPIOD_CTXLESS_EVENT_BOTH_EDGES,
+					   offset, active_low, consumer,
+					   timeout, poll_cb, event_cb, data);
+}
+
+int gpiod_ctxless_event_loop_multiple(const char *device,
+				      const unsigned int *offsets,
+				      unsigned int num_lines, bool active_low,
+				      const char *consumer,
+				      const struct timespec *timeout,
+				      gpiod_ctxless_event_poll_cb poll_cb,
+				      gpiod_ctxless_event_handle_cb event_cb,
+				      void *data)
+{
+	return gpiod_ctxless_event_monitor_multiple(
+				device, GPIOD_CTXLESS_EVENT_BOTH_EDGES,
+				offsets, num_lines, active_low, consumer,
+				timeout, poll_cb, event_cb, data);
+}
+
+int gpiod_ctxless_event_monitor(const char *device, int event_type,
+				unsigned int offset, bool active_low,
+				const char *consumer,
+				const struct timespec *timeout,
+				gpiod_ctxless_event_poll_cb poll_cb,
+				gpiod_ctxless_event_handle_cb event_cb,
+				void *data)
+{
+	return gpiod_ctxless_event_monitor_multiple(device, event_type,
+						    &offset, 1, active_low,
+						    consumer, timeout,
+						    poll_cb, event_cb, data);
+}
+
+int gpiod_ctxless_event_monitor_multiple(
+			const char *device, int event_type,
+			const unsigned int *offsets,
+			unsigned int num_lines, bool active_low,
+			const char *consumer,
+			const struct timespec *timeout,
+			gpiod_ctxless_event_poll_cb poll_cb,
+			gpiod_ctxless_event_handle_cb event_cb,
+			void *data)
+{
+	struct gpiod_ctxless_event_poll_fd fds[GPIOD_LINE_BULK_MAX_LINES];
+	struct gpiod_line_request_config conf;
+	struct gpiod_line_event event;
+	struct gpiod_line_bulk bulk;
+	int rv, ret, evtype, cnt;
+	struct gpiod_chip *chip;
+	struct gpiod_line *line;
+	unsigned int i;
+
+	if (!num_lines || num_lines > GPIOD_LINE_BULK_MAX_LINES) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	if (!poll_cb)
+		poll_cb = basic_event_poll;
+
+	chip = gpiod_chip_open_lookup(device);
+	if (!chip)
+		return -1;
+
+	gpiod_line_bulk_init(&bulk);
+
+	for (i = 0; i < num_lines; i++) {
+		line = gpiod_chip_get_line(chip, offsets[i]);
+		if (!line) {
+			gpiod_chip_close(chip);
+			return -1;
+		}
+
+		gpiod_line_bulk_add(&bulk, line);
+	}
+
+	conf.flags = active_low ? GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW : 0;
+	conf.consumer = consumer;
+
+	if (event_type == GPIOD_CTXLESS_EVENT_RISING_EDGE) {
+		conf.request_type = GPIOD_LINE_REQUEST_EVENT_RISING_EDGE;
+	} else if (event_type == GPIOD_CTXLESS_EVENT_FALLING_EDGE) {
+		conf.request_type = GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE;
+	} else if (event_type == GPIOD_CTXLESS_EVENT_BOTH_EDGES) {
+		conf.request_type = GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES;
+	} else {
+		errno = -EINVAL;
+		ret = -1;
+		goto out;
+	}
+
+	rv = gpiod_line_request_bulk(&bulk, &conf, NULL);
+	if (rv) {
+		ret = -1;
+		goto out;
+	}
+
+	memset(fds, 0, sizeof(fds));
+	for (i = 0; i < num_lines; i++) {
+		line = gpiod_line_bulk_get_line(&bulk, i);
+		fds[i].fd = gpiod_line_event_get_fd(line);
+	}
+
+	for (;;) {
+		for (i = 0; i < num_lines; i++)
+			fds[i].event = false;
+
+		cnt = poll_cb(num_lines, fds, timeout, data);
+		if (cnt == GPIOD_CTXLESS_EVENT_POLL_RET_ERR) {
+			ret = -1;
+			goto out;
+		} else if (cnt == GPIOD_CTXLESS_EVENT_POLL_RET_TIMEOUT) {
+			rv = event_cb(GPIOD_CTXLESS_EVENT_CB_TIMEOUT,
+				      0, &event.ts, data);
+			if (rv == GPIOD_CTXLESS_EVENT_CB_RET_ERR) {
+				ret = -1;
+				goto out;
+			} else if (rv == GPIOD_CTXLESS_EVENT_CB_RET_STOP) {
+				ret = 0;
+				goto out;
+			}
+		} else if (cnt == GPIOD_CTXLESS_EVENT_POLL_RET_STOP) {
+			ret = 0;
+			goto out;
+		}
+
+		for (i = 0; i < num_lines; i++) {
+			if (!fds[i].event)
+				continue;
+
+			line = gpiod_line_bulk_get_line(&bulk, i);
+			rv = gpiod_line_event_read(line, &event);
+			if (rv < 0) {
+				ret = rv;
+				goto out;
+			}
+
+			if (event.event_type == GPIOD_LINE_EVENT_RISING_EDGE)
+				evtype = GPIOD_CTXLESS_EVENT_CB_RISING_EDGE;
+			else
+				evtype = GPIOD_CTXLESS_EVENT_CB_FALLING_EDGE;
+
+			rv = event_cb(evtype, gpiod_line_offset(line),
+				      &event.ts, data);
+			if (rv == GPIOD_CTXLESS_EVENT_CB_RET_ERR) {
+				ret = -1;
+				goto out;
+			} else if (rv == GPIOD_CTXLESS_EVENT_CB_RET_STOP) {
+				ret = 0;
+				goto out;
+			}
+
+			if (!--cnt)
+				break;
+		}
+	}
+
+out:
+	gpiod_chip_close(chip);
+
+	return ret;
+}
+
+int gpiod_ctxless_find_line(const char *name, char *chipname,
+			    size_t chipname_size, unsigned int *offset)
+{
+	struct gpiod_chip *chip;
+	struct gpiod_line *line;
+
+	line = gpiod_line_find(name);
+	if (!line) {
+		if (errno == ENOENT)
+			return 0;
+		else
+			return -1;
+	}
+
+	chip = gpiod_line_get_chip(line);
+	snprintf(chipname, chipname_size, "%s", gpiod_chip_name(chip));
+	*offset = gpiod_line_offset(line);
+	gpiod_chip_close(chip);
+
+	return 1;
+}
diff --git a/lib/helpers.c b/lib/helpers.c
new file mode 100644
index 0000000..479f370
--- /dev/null
+++ b/lib/helpers.c
@@ -0,0 +1,443 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/*
+ * More specific variants of the core API and misc functions that don't need
+ * access to neither the internal library data structures nor the kernel UAPI.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <string.h>
+
+static bool isuint(const char *str)
+{
+	for (; *str && isdigit(*str); str++)
+		;
+
+	return *str == '\0';
+}
+
+struct gpiod_chip *gpiod_chip_open_by_name(const char *name)
+{
+	struct gpiod_chip *chip;
+	char *path;
+	int rv;
+
+	rv = asprintf(&path, "/dev/%s", name);
+	if (rv < 0)
+		return NULL;
+
+	chip = gpiod_chip_open(path);
+	free(path);
+
+	return chip;
+}
+
+struct gpiod_chip *gpiod_chip_open_by_number(unsigned int num)
+{
+	struct gpiod_chip *chip;
+	char *path;
+	int rv;
+
+	rv = asprintf(&path, "/dev/gpiochip%u", num);
+	if (!rv)
+		return NULL;
+
+	chip = gpiod_chip_open(path);
+	free(path);
+
+	return chip;
+}
+
+struct gpiod_chip *gpiod_chip_open_by_label(const char *label)
+{
+	struct gpiod_chip_iter *iter;
+	struct gpiod_chip *chip;
+
+	iter = gpiod_chip_iter_new();
+	if (!iter)
+		return NULL;
+
+	gpiod_foreach_chip(iter, chip) {
+		if (strcmp(label, gpiod_chip_label(chip)) == 0) {
+			gpiod_chip_iter_free_noclose(iter);
+			return chip;
+		}
+	}
+
+	errno = ENOENT;
+	gpiod_chip_iter_free(iter);
+
+	return NULL;
+}
+
+struct gpiod_chip *gpiod_chip_open_lookup(const char *descr)
+{
+	struct gpiod_chip *chip;
+
+	if (isuint(descr)) {
+		chip = gpiod_chip_open_by_number(strtoul(descr, NULL, 10));
+	} else {
+		chip = gpiod_chip_open_by_label(descr);
+		if (!chip) {
+			if (strncmp(descr, "/dev/", 5))
+				chip = gpiod_chip_open_by_name(descr);
+			else
+				chip = gpiod_chip_open(descr);
+		}
+	}
+
+	return chip;
+}
+
+int gpiod_chip_get_lines(struct gpiod_chip *chip, unsigned int *offsets,
+			 unsigned int num_offsets, struct gpiod_line_bulk *bulk)
+{
+	struct gpiod_line *line;
+	unsigned int i;
+
+	gpiod_line_bulk_init(bulk);
+
+	for (i = 0; i < num_offsets; i++) {
+		line = gpiod_chip_get_line(chip, offsets[i]);
+		if (!line)
+			return -1;
+
+		gpiod_line_bulk_add(bulk, line);
+	}
+
+	return 0;
+}
+
+int gpiod_chip_get_all_lines(struct gpiod_chip *chip,
+			     struct gpiod_line_bulk *bulk)
+{
+	struct gpiod_line_iter *iter;
+	struct gpiod_line *line;
+
+	gpiod_line_bulk_init(bulk);
+
+	iter = gpiod_line_iter_new(chip);
+	if (!iter)
+		return -1;
+
+	gpiod_foreach_line(iter, line)
+		gpiod_line_bulk_add(bulk, line);
+
+	gpiod_line_iter_free(iter);
+
+	return 0;
+}
+
+struct gpiod_line *
+gpiod_chip_find_line(struct gpiod_chip *chip, const char *name)
+{
+	struct gpiod_line_iter *iter;
+	struct gpiod_line *line;
+	const char *tmp;
+
+	iter = gpiod_line_iter_new(chip);
+	if (!iter)
+		return NULL;
+
+	gpiod_foreach_line(iter, line) {
+		tmp = gpiod_line_name(line);
+		if (tmp && strcmp(tmp, name) == 0) {
+			gpiod_line_iter_free(iter);
+			return line;
+		}
+	}
+
+	errno = ENOENT;
+	gpiod_line_iter_free(iter);
+
+	return NULL;
+}
+
+int gpiod_chip_find_lines(struct gpiod_chip *chip,
+			  const char **names, struct gpiod_line_bulk *bulk)
+{
+	struct gpiod_line *line;
+	int i;
+
+	gpiod_line_bulk_init(bulk);
+
+	for (i = 0; names[i]; i++) {
+		line = gpiod_chip_find_line(chip, names[i]);
+		if (!line)
+			return -1;
+
+		gpiod_line_bulk_add(bulk, line);
+	}
+
+	return 0;
+}
+
+int gpiod_line_request_input(struct gpiod_line *line, const char *consumer)
+{
+	struct gpiod_line_request_config config = {
+		.consumer = consumer,
+		.request_type = GPIOD_LINE_REQUEST_DIRECTION_INPUT,
+	};
+
+	return gpiod_line_request(line, &config, 0);
+}
+
+int gpiod_line_request_output(struct gpiod_line *line,
+			      const char *consumer, int default_val)
+{
+	struct gpiod_line_request_config config = {
+		.consumer = consumer,
+		.request_type = GPIOD_LINE_REQUEST_DIRECTION_OUTPUT,
+	};
+
+	return gpiod_line_request(line, &config, default_val);
+}
+
+int gpiod_line_request_input_flags(struct gpiod_line *line,
+				   const char *consumer, int flags)
+{
+	struct gpiod_line_request_config config = {
+		.consumer = consumer,
+		.request_type = GPIOD_LINE_REQUEST_DIRECTION_INPUT,
+		.flags = flags,
+	};
+
+	return gpiod_line_request(line, &config, 0);
+}
+
+int gpiod_line_request_output_flags(struct gpiod_line *line,
+				    const char *consumer, int flags,
+				    int default_val)
+{
+	struct gpiod_line_request_config config = {
+		.consumer = consumer,
+		.request_type = GPIOD_LINE_REQUEST_DIRECTION_OUTPUT,
+		.flags = flags,
+	};
+
+	return gpiod_line_request(line, &config, default_val);
+}
+
+static int line_event_request_type(struct gpiod_line *line,
+				   const char *consumer, int flags, int type)
+{
+	struct gpiod_line_request_config config = {
+		.consumer = consumer,
+		.request_type = type,
+		.flags = flags,
+	};
+
+	return gpiod_line_request(line, &config, 0);
+}
+
+int gpiod_line_request_rising_edge_events(struct gpiod_line *line,
+					  const char *consumer)
+{
+	return line_event_request_type(line, consumer, 0,
+				       GPIOD_LINE_REQUEST_EVENT_RISING_EDGE);
+}
+
+int gpiod_line_request_falling_edge_events(struct gpiod_line *line,
+					   const char *consumer)
+{
+	return line_event_request_type(line, consumer, 0,
+				       GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE);
+}
+
+int gpiod_line_request_both_edges_events(struct gpiod_line *line,
+					 const char *consumer)
+{
+	return line_event_request_type(line, consumer, 0,
+				       GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES);
+}
+
+int gpiod_line_request_rising_edge_events_flags(struct gpiod_line *line,
+						const char *consumer,
+						int flags)
+{
+	return line_event_request_type(line, consumer, flags,
+				       GPIOD_LINE_REQUEST_EVENT_RISING_EDGE);
+}
+
+int gpiod_line_request_falling_edge_events_flags(struct gpiod_line *line,
+						 const char *consumer,
+						 int flags)
+{
+	return line_event_request_type(line, consumer, flags,
+				       GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE);
+}
+
+int gpiod_line_request_both_edges_events_flags(struct gpiod_line *line,
+					       const char *consumer, int flags)
+{
+	return line_event_request_type(line, consumer, flags,
+				       GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES);
+}
+
+int gpiod_line_request_bulk_input(struct gpiod_line_bulk *bulk,
+				  const char *consumer)
+{
+	struct gpiod_line_request_config config = {
+		.consumer = consumer,
+		.request_type = GPIOD_LINE_REQUEST_DIRECTION_INPUT,
+	};
+
+	return gpiod_line_request_bulk(bulk, &config, 0);
+}
+
+int gpiod_line_request_bulk_output(struct gpiod_line_bulk *bulk,
+				   const char *consumer,
+				   const int *default_vals)
+{
+	struct gpiod_line_request_config config = {
+		.consumer = consumer,
+		.request_type = GPIOD_LINE_REQUEST_DIRECTION_OUTPUT,
+	};
+
+	return gpiod_line_request_bulk(bulk, &config, default_vals);
+}
+
+static int line_event_request_type_bulk(struct gpiod_line_bulk *bulk,
+					const char *consumer,
+					int flags, int type)
+{
+	struct gpiod_line_request_config config = {
+		.consumer = consumer,
+		.request_type = type,
+		.flags = flags,
+	};
+
+	return gpiod_line_request_bulk(bulk, &config, 0);
+}
+
+int gpiod_line_request_bulk_rising_edge_events(struct gpiod_line_bulk *bulk,
+					       const char *consumer)
+{
+	return line_event_request_type_bulk(bulk, consumer, 0,
+					GPIOD_LINE_REQUEST_EVENT_RISING_EDGE);
+}
+
+int gpiod_line_request_bulk_falling_edge_events(struct gpiod_line_bulk *bulk,
+						const char *consumer)
+{
+	return line_event_request_type_bulk(bulk, consumer, 0,
+					GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE);
+}
+
+int gpiod_line_request_bulk_both_edges_events(struct gpiod_line_bulk *bulk,
+					      const char *consumer)
+{
+	return line_event_request_type_bulk(bulk, consumer, 0,
+					GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES);
+}
+
+int gpiod_line_request_bulk_input_flags(struct gpiod_line_bulk *bulk,
+					const char *consumer, int flags)
+{
+	struct gpiod_line_request_config config = {
+		.consumer = consumer,
+		.request_type = GPIOD_LINE_REQUEST_DIRECTION_INPUT,
+		.flags = flags,
+	};
+
+	return gpiod_line_request_bulk(bulk, &config, 0);
+}
+
+int gpiod_line_request_bulk_output_flags(struct gpiod_line_bulk *bulk,
+					 const char *consumer, int flags,
+					 const int *default_vals)
+{
+	struct gpiod_line_request_config config = {
+		.consumer = consumer,
+		.request_type = GPIOD_LINE_REQUEST_DIRECTION_OUTPUT,
+		.flags = flags,
+	};
+
+	return gpiod_line_request_bulk(bulk, &config, default_vals);
+}
+
+int gpiod_line_request_bulk_rising_edge_events_flags(
+					struct gpiod_line_bulk *bulk,
+					const char *consumer, int flags)
+{
+	return line_event_request_type_bulk(bulk, consumer, flags,
+					GPIOD_LINE_REQUEST_EVENT_RISING_EDGE);
+}
+
+int gpiod_line_request_bulk_falling_edge_events_flags(
+					struct gpiod_line_bulk *bulk,
+					const char *consumer, int flags)
+{
+	return line_event_request_type_bulk(bulk, consumer, flags,
+					GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE);
+}
+
+int gpiod_line_request_bulk_both_edges_events_flags(
+					struct gpiod_line_bulk *bulk,
+					const char *consumer, int flags)
+{
+	return line_event_request_type_bulk(bulk, consumer, flags,
+					GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES);
+}
+
+struct gpiod_line *gpiod_line_get(const char *device, unsigned int offset)
+{
+	struct gpiod_chip *chip;
+	struct gpiod_line *line;
+
+	chip = gpiod_chip_open_lookup(device);
+	if (!chip)
+		return NULL;
+
+	line = gpiod_chip_get_line(chip, offset);
+	if (!line) {
+		gpiod_chip_close(chip);
+		return NULL;
+	}
+
+	return line;
+}
+
+struct gpiod_line *gpiod_line_find(const char *name)
+{
+	struct gpiod_chip_iter *iter;
+	struct gpiod_chip *chip;
+	struct gpiod_line *line;
+
+	iter = gpiod_chip_iter_new();
+	if (!iter)
+		return NULL;
+
+	gpiod_foreach_chip(iter, chip) {
+		line = gpiod_chip_find_line(chip, name);
+		if (line) {
+			gpiod_chip_iter_free_noclose(iter);
+			return line;
+		}
+
+		if (errno != ENOENT)
+			goto out;
+	}
+
+	errno = ENOENT;
+
+out:
+	gpiod_chip_iter_free(iter);
+
+	return NULL;
+}
+
+void gpiod_line_close_chip(struct gpiod_line *line)
+{
+	struct gpiod_chip *chip = gpiod_line_get_chip(line);
+
+	gpiod_chip_close(chip);
+}
diff --git a/lib/iter.c b/lib/iter.c
new file mode 100644
index 0000000..a4d883a
--- /dev/null
+++ b/lib/iter.c
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* GPIO chip and line iterators. */
+
+#include <dirent.h>
+#include <gpiod.h>
+#include <string.h>
+
+struct gpiod_chip_iter {
+	struct gpiod_chip **chips;
+	unsigned int num_chips;
+	unsigned int offset;
+};
+
+struct gpiod_line_iter {
+	struct gpiod_line **lines;
+	unsigned int num_lines;
+	unsigned int offset;
+};
+
+static int dir_filter(const struct dirent *dir)
+{
+	return !strncmp(dir->d_name, "gpiochip", 8);
+}
+
+static void free_dirs(struct dirent ***dirs, unsigned int num_dirs)
+{
+	unsigned int i;
+
+	for (i = 0; i < num_dirs; i++)
+		free((*dirs)[i]);
+	free(*dirs);
+}
+
+struct gpiod_chip_iter *gpiod_chip_iter_new(void)
+{
+	struct gpiod_chip_iter *iter;
+	struct dirent **dirs;
+	int i, num_chips;
+
+	num_chips = scandir("/dev", &dirs, dir_filter, alphasort);
+	if (num_chips < 0)
+		return NULL;
+
+	iter = malloc(sizeof(*iter));
+	if (!iter)
+		goto err_free_dirs;
+
+	iter->num_chips = num_chips;
+	iter->offset = 0;
+
+	if (num_chips == 0) {
+		iter->chips = NULL;
+		return iter;
+	}
+
+	iter->chips = calloc(num_chips, sizeof(*iter->chips));
+	if (!iter->chips)
+		goto err_free_iter;
+
+	for (i = 0; i < num_chips; i++) {
+		iter->chips[i] = gpiod_chip_open_by_name(dirs[i]->d_name);
+		if (!iter->chips[i])
+			goto err_close_chips;
+	}
+
+	free_dirs(&dirs, num_chips);
+
+	return iter;
+
+err_close_chips:
+	for (i = 0; i < num_chips; i++) {
+		if (iter->chips[i])
+			gpiod_chip_close(iter->chips[i]);
+	}
+
+	free(iter->chips);
+
+err_free_iter:
+	free(iter);
+
+err_free_dirs:
+	free_dirs(&dirs, num_chips);
+
+	return NULL;
+}
+
+void gpiod_chip_iter_free(struct gpiod_chip_iter *iter)
+{
+	if (iter->offset > 0 && iter->offset < iter->num_chips)
+		gpiod_chip_close(iter->chips[iter->offset - 1]);
+	gpiod_chip_iter_free_noclose(iter);
+}
+
+void gpiod_chip_iter_free_noclose(struct gpiod_chip_iter *iter)
+{
+	unsigned int i;
+
+	for (i = iter->offset; i < iter->num_chips; i++) {
+		if (iter->chips[i])
+			gpiod_chip_close(iter->chips[i]);
+	}
+
+	if (iter->chips)
+		free(iter->chips);
+
+	free(iter);
+}
+
+struct gpiod_chip *gpiod_chip_iter_next(struct gpiod_chip_iter *iter)
+{
+	if (iter->offset > 0) {
+		gpiod_chip_close(iter->chips[iter->offset - 1]);
+		iter->chips[iter->offset - 1] = NULL;
+	}
+
+	return gpiod_chip_iter_next_noclose(iter);
+}
+
+struct gpiod_chip *gpiod_chip_iter_next_noclose(struct gpiod_chip_iter *iter)
+{
+	return iter->offset < (iter->num_chips)
+					? iter->chips[iter->offset++] : NULL;
+}
+
+struct gpiod_line_iter *gpiod_line_iter_new(struct gpiod_chip *chip)
+{
+	struct gpiod_line_iter *iter;
+	unsigned int i;
+
+	iter = malloc(sizeof(*iter));
+	if (!iter)
+		return NULL;
+
+	iter->num_lines = gpiod_chip_num_lines(chip);
+	iter->offset = 0;
+
+	iter->lines = calloc(iter->num_lines, sizeof(*iter->lines));
+	if (!iter->lines) {
+		free(iter);
+		return NULL;
+	}
+
+	for (i = 0; i < iter->num_lines; i++) {
+		iter->lines[i] = gpiod_chip_get_line(chip, i);
+		if (!iter->lines[i]) {
+			free(iter->lines);
+			free(iter);
+			return NULL;
+		}
+	}
+
+	return iter;
+}
+
+void gpiod_line_iter_free(struct gpiod_line_iter *iter)
+{
+	free(iter->lines);
+	free(iter);
+}
+
+struct gpiod_line *gpiod_line_iter_next(struct gpiod_line_iter *iter)
+{
+	return iter->offset < (iter->num_lines)
+					? iter->lines[iter->offset++] : NULL;
+}
diff --git a/lib/misc.c b/lib/misc.c
new file mode 100644
index 0000000..e3533bd
--- /dev/null
+++ b/lib/misc.c
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Misc code that didn't fit anywhere else. */
+
+#include <gpiod.h>
+#include <config.h>
+
+const char *gpiod_version_string(void)
+{
+	return GPIOD_VERSION_STR;
+}
diff --git a/libgpiod.pc.in b/libgpiod.pc.in
new file mode 100644
index 0000000..48ff113
--- /dev/null
+++ b/libgpiod.pc.in
@@ -0,0 +1,11 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: libgpiod
+Description: Library and tools for the Linux GPIO chardev
+URL: @PACKAGE_URL@
+Version: @PACKAGE_VERSION@
+Libs: -L${libdir} -lgpiod
+Cflags: -I${includedir}
diff --git a/man/Makefile.am b/man/Makefile.am
new file mode 100644
index 0000000..8877858
--- /dev/null
+++ b/man/Makefile.am
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2019 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+if WITH_MANPAGES
+
+dist_man1_MANS = gpiodetect.man gpioinfo.man gpioget.man gpioset.man gpiofind.man gpiomon.man
+
+%.man: $(top_srcdir)/tools/$(*F)
+	help2man $(top_srcdir)/tools/$(*F) --include=./template --output=./$@ --no-info
+
+clean-local:
+	rm -f $(dist_man1_MANS)
+
+endif
+
+EXTRA_DIST = template
diff --git a/man/template b/man/template
new file mode 100644
index 0000000..f1040ff
--- /dev/null
+++ b/man/template
@@ -0,0 +1,7 @@
+[AUTHOR]
+Bartosz Golaszewski <bartekgola@gmail.com>
+
+[REPORTING BUGS]
+Report bugs to:
+    Bartosz Golaszewski <bartekgola@gmail.com>
+    linux-gpio <linux-gpio@vger.kernel.org>
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644
index 0000000..4d1e6d5
--- /dev/null
+++ b/tests/Makefile.am
@@ -0,0 +1,50 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+AM_CFLAGS = -I$(top_srcdir)/include/ -include $(top_builddir)/config.h
+AM_CFLAGS += -Wall -Wextra -g $(KMOD_CFLAGS) $(UDEV_CFLAGS)
+AM_LDFLAGS = -pthread
+LDADD = $(top_builddir)/lib/libgpiod.la $(KMOD_LIBS) $(UDEV_LIBS)
+
+if WITH_INSTALL_TESTS
+bin_PROGRAMS = gpiod-test
+else
+check_PROGRAMS = gpiod-test
+endif
+
+gpiod_test_SOURCES =	gpiod-test.c \
+			gpiod-test.h \
+			tests-chip.c \
+			tests-ctxless.c \
+			tests-event.c \
+			tests-iter.c \
+			tests-line.c \
+			tests-misc.c
+
+if WITH_TOOLS
+
+gpiod_test_SOURCES +=	tests-gpiodetect.c \
+			tests-gpiofind.c \
+			tests-gpioget.c \
+			tests-gpioinfo.c \
+			tests-gpiomon.c \
+			tests-gpioset.c
+
+endif
+
+check: check-am
+	@echo " ********************************************************"
+	@echo " * Tests have been built as tests/gpio-test.            *"
+	@echo " *                                                      *"
+	@echo " * They require a recent linux kernel version and the   *"
+	@echo " * gpio-mockup module (must not be built-in).           *"
+	@echo " *                                                      *"
+	@echo " * Run the test executable with superuser privileges or *"
+	@echo " * make sure /dev/gpiochipX files are readable and      *"
+	@echo " * writable by normal users.                            *"
+	@echo " ********************************************************"
diff --git a/tests/gpiod-test.c b/tests/gpiod-test.c
new file mode 100644
index 0000000..9cb9072
--- /dev/null
+++ b/tests/gpiod-test.c
@@ -0,0 +1,1226 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <libkmod.h>
+#include <libudev.h>
+#include <poll.h>
+#include <pthread.h>
+#include <regex.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/signalfd.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "gpiod-test.h"
+
+#define NORETURN	__attribute__((noreturn))
+#define MALLOC		__attribute__((malloc))
+
+static const unsigned int min_kern_major = 4;
+static const unsigned int min_kern_minor = 16;
+static const unsigned int min_kern_release = 0;
+
+struct mockup_chip {
+	char *path;
+	char *name;
+	unsigned int number;
+};
+
+struct event_thread {
+	pthread_t thread_id;
+	pthread_mutex_t lock;
+	pthread_cond_t cond;
+	bool running;
+	bool should_stop;
+	unsigned int chip_index;
+	unsigned int line_offset;
+	unsigned int freq;
+	int event_type;
+};
+
+struct gpiotool_proc {
+	pid_t pid;
+	bool running;
+	int stdin_fd;
+	int stdout_fd;
+	int stderr_fd;
+	char *stdout;
+	char *stderr;
+	int sig_fd;
+	int exit_status;
+};
+
+struct test_context {
+	struct mockup_chip **chips;
+	size_t num_chips;
+	bool test_failed;
+	char *failed_msg;
+	char *custom_str;
+	struct event_thread event;
+	struct gpiotool_proc tool_proc;
+	bool running;
+};
+
+static struct {
+	struct _test_case *test_list_head;
+	struct _test_case *test_list_tail;
+	unsigned int num_tests;
+	unsigned int tests_failed;
+	struct kmod_ctx *module_ctx;
+	struct kmod_module *module;
+	struct test_context test_ctx;
+	pid_t main_pid;
+	int pipesize;
+	char *pipebuf;
+	char *toolpath;
+} globals;
+
+enum {
+	CNORM = 0,
+	CGREEN,
+	CRED,
+	CREDBOLD,
+	CYELLOW,
+};
+
+static const char *const term_colors[] = {
+	"\033[0m",
+	"\033[32m",
+	"\033[31m",
+	"\033[1m\033[31m",
+	"\033[33m",
+};
+
+static void set_color(int color)
+{
+	fprintf(stderr, "%s", term_colors[color]);
+}
+
+static void reset_color(void)
+{
+	fprintf(stderr, "%s", term_colors[CNORM]);
+}
+
+static void pr_raw_v(const char *fmt, va_list va)
+{
+	vfprintf(stderr, fmt, va);
+}
+
+static TEST_PRINTF(1, 2) void pr_raw(const char *fmt, ...)
+{
+	va_list va;
+
+	va_start(va, fmt);
+	pr_raw_v(fmt, va);
+	va_end(va);
+}
+
+static void print_header(const char *hdr, int color)
+{
+	char buf[9];
+
+	snprintf(buf, sizeof(buf), "[%s]", hdr);
+
+	set_color(color);
+	pr_raw("%-8s", buf);
+	reset_color();
+}
+
+static void vmsgn(const char *hdr, int hdr_clr, const char *fmt, va_list va)
+{
+	print_header(hdr, hdr_clr);
+	pr_raw_v(fmt, va);
+}
+
+static void vmsg(const char *hdr, int hdr_clr, const char *fmt, va_list va)
+{
+	vmsgn(hdr, hdr_clr, fmt, va);
+	pr_raw("\n");
+}
+
+static TEST_PRINTF(1, 2) void msg(const char *fmt, ...)
+{
+	va_list va;
+
+	va_start(va, fmt);
+	vmsg("INFO", CGREEN, fmt, va);
+	va_end(va);
+}
+
+static TEST_PRINTF(1, 2) void err(const char *fmt, ...)
+{
+	va_list va;
+
+	va_start(va, fmt);
+	vmsg("ERROR", CRED, fmt, va);
+	va_end(va);
+}
+
+static void die_test_cleanup(void)
+{
+	struct gpiotool_proc *proc = &globals.test_ctx.tool_proc;
+	int rv;
+
+	if (proc->running) {
+		kill(proc->pid, SIGKILL);
+		waitpid(proc->pid, &rv, 0);
+	}
+
+	if (globals.test_ctx.running)
+		pr_raw("\n");
+}
+
+static TEST_PRINTF(1, 2) NORETURN void die(const char *fmt, ...)
+{
+	va_list va;
+
+	die_test_cleanup();
+
+	va_start(va, fmt);
+	vmsg("FATAL", CRED, fmt, va);
+	va_end(va);
+
+	exit(EXIT_FAILURE);
+}
+
+static TEST_PRINTF(1, 2) NORETURN void die_perr(const char *fmt, ...)
+{
+	va_list va;
+
+	die_test_cleanup();
+
+	va_start(va, fmt);
+	vmsgn("FATAL", CRED, fmt, va);
+	pr_raw(": %s\n", strerror(errno));
+	va_end(va);
+
+	exit(EXIT_FAILURE);
+}
+
+static MALLOC void *xmalloc(size_t size)
+{
+	void *ptr;
+
+	ptr = malloc(size);
+	if (!ptr)
+		die("out of memory");
+
+	return ptr;
+}
+
+static MALLOC void *xzalloc(size_t size)
+{
+	void *ptr;
+
+	ptr = xmalloc(size);
+	memset(ptr, 0, size);
+
+	return ptr;
+}
+
+static MALLOC void *xcalloc(size_t nmemb, size_t size)
+{
+	void *ptr;
+
+	ptr = calloc(nmemb, size);
+	if (!ptr)
+		die("out of memory");
+
+	return ptr;
+}
+
+static MALLOC char *xstrdup(const char *str)
+{
+	char *ret;
+
+	ret = strdup(str);
+	if (!ret)
+		die("out of memory");
+
+	return ret;
+}
+
+static TEST_PRINTF(2, 3) char *xappend(char *str, const char *fmt, ...)
+{
+	char *new, *ret;
+	va_list va;
+	int rv;
+
+	va_start(va, fmt);
+	rv = vasprintf(&new, fmt, va);
+	va_end(va);
+	if (rv < 0)
+		die_perr("vasprintf");
+
+	if (!str)
+		return new;
+
+	ret = realloc(str, strlen(str) + strlen(new) + 1);
+	if (!ret)
+		die("out of memory");
+
+	strcat(ret, new);
+	free(new);
+
+	return ret;
+}
+
+static int get_pipesize(void)
+{
+	int pipe_fds[2], rv;
+
+	rv = pipe(pipe_fds);
+	if (rv < 0)
+		die_perr("unable to create a pipe");
+
+	/*
+	 * Since linux v2.6.11 the default pipe capacity is 16 system pages.
+	 * We make an assumption here that gpio-tools won't output more than
+	 * that, so we can read everything after the program terminated. If
+	 * they did output more than the pipe capacity however, the write()
+	 * system call would block and the process would be killed by the
+	 * testing framework.
+	 */
+	rv = fcntl(pipe_fds[0], F_GETPIPE_SZ);
+	if (rv < 0)
+		die_perr("unable to retrieve the pipe capacity");
+
+	close(pipe_fds[0]);
+	close(pipe_fds[1]);
+
+	return rv;
+}
+
+static void check_chip_index(unsigned int index)
+{
+	if (index >= globals.test_ctx.num_chips)
+		die("invalid chip number requested from test code");
+}
+
+static bool mockup_loaded(void)
+{
+	int state;
+
+	if (!globals.module_ctx || !globals.module)
+		return false;
+
+	state = kmod_module_get_initstate(globals.module);
+
+	return state == KMOD_MODULE_LIVE;
+}
+
+static void cleanup_func(void)
+{
+	/* Don't cleanup from child processes. */
+	if (globals.main_pid != getpid())
+		return;
+
+	msg("cleaning up");
+
+	free(globals.pipebuf);
+
+	if (mockup_loaded())
+		kmod_module_remove_module(globals.module, 0);
+
+	if (globals.module)
+		kmod_module_unref(globals.module);
+
+	if (globals.module_ctx)
+		kmod_unref(globals.module_ctx);
+}
+
+static void event_lock(void)
+{
+	pthread_mutex_lock(&globals.test_ctx.event.lock);
+}
+
+static void event_unlock(void)
+{
+	pthread_mutex_unlock(&globals.test_ctx.event.lock);
+}
+
+static void *event_worker(void *data TEST_UNUSED)
+{
+	struct event_thread *ev = &globals.test_ctx.event;
+	struct timeval tv_now, tv_add, tv_res;
+	struct timespec ts;
+	int rv, i, fd;
+	char *path;
+	ssize_t rd;
+	char buf;
+
+	for (i = 0;; i++) {
+		event_lock();
+		if (ev->should_stop) {
+			event_unlock();
+			break;
+		}
+
+		gettimeofday(&tv_now, NULL);
+		tv_add.tv_sec = 0;
+		tv_add.tv_usec = ev->freq * 1000;
+		timeradd(&tv_now, &tv_add, &tv_res);
+		ts.tv_sec = tv_res.tv_sec;
+		ts.tv_nsec = tv_res.tv_usec * 1000;
+
+		rv = pthread_cond_timedwait(&ev->cond, &ev->lock, &ts);
+		if (rv == ETIMEDOUT) {
+			path = xappend(NULL,
+				       "/sys/kernel/debug/gpio-mockup-event/gpio-mockup-%c/%u",
+				       'A' + ev->chip_index, ev->line_offset);
+
+			fd = open(path, O_RDWR);
+			free(path);
+			if (fd < 0)
+				die_perr("error opening gpio event file");
+
+			if (ev->event_type == TEST_EVENT_RISING)
+				buf = '1';
+			else if (ev->event_type == TEST_EVENT_FALLING)
+				buf = '0';
+			else
+				buf = i % 2 == 0 ? '1' : '0';
+
+			rd = write(fd, &buf, 1);
+			close(fd);
+			if (rd < 0)
+				die_perr("error writing to gpio event file");
+			else if (rd != 1)
+				die("invalid write size to gpio event file");
+		} else if (rv != 0) {
+			die("error waiting for conditional variable: %s",
+			    strerror(rv));
+		}
+
+		event_unlock();
+	}
+
+	return NULL;
+}
+
+static void gpiotool_proc_make_pipes(int *in_fds, int *out_fds, int *err_fds)
+{
+	int rv, i, *fds[3];
+
+	fds[0] = in_fds;
+	fds[1] = out_fds;
+	fds[2] = err_fds;
+
+	for (i = 0; i < 3; i++) {
+		rv = pipe(fds[i]);
+		if (rv < 0)
+			die_perr("unable to create a pipe");
+	}
+}
+
+static void gpiotool_proc_dup_fds(int in_fd, int out_fd, int err_fd)
+{
+	int old_fds[3], new_fds[3], i, rv;
+
+	old_fds[0] = in_fd;
+	old_fds[1] = out_fd;
+	old_fds[2] = err_fd;
+
+	new_fds[0] = STDIN_FILENO;
+	new_fds[1] = STDOUT_FILENO;
+	new_fds[2] = STDERR_FILENO;
+
+	for (i = 0; i < 3; i++) {
+		rv = dup2(old_fds[i], new_fds[i]);
+		if (rv < 0)
+			die_perr("unable to duplicate a file descriptor");
+
+		close(old_fds[i]);
+	}
+}
+
+static NORETURN void gpiotool_proc_exec(const char *path, va_list va)
+{
+	size_t num_args;
+	unsigned int i;
+	char **argv;
+	va_list va2;
+
+	va_copy(va2, va);
+	for (num_args = 2; va_arg(va2, char *) != NULL; num_args++)
+		;
+	va_end(va2);
+
+	argv = xcalloc(num_args, sizeof(char *));
+
+	argv[0] = (char *)path;
+	for (i = 1; i < num_args; i++)
+		argv[i] = va_arg(va, char *);
+	va_end(va);
+
+	execv(path, argv);
+	die_perr("unable to exec '%s'", path);
+}
+
+static void gpiotool_proc_cleanup(void)
+{
+	struct gpiotool_proc *proc = &globals.test_ctx.tool_proc;
+
+	if (proc->stdout)
+		free(proc->stdout);
+
+	if (proc->stderr)
+		free(proc->stderr);
+
+	proc->stdout = proc->stderr = NULL;
+}
+
+void test_tool_signal(int signum)
+{
+	struct gpiotool_proc *proc = &globals.test_ctx.tool_proc;
+	int rv;
+
+	rv = kill(proc->pid, signum);
+	if (rv)
+		die_perr("unable to send signal to process %d", proc->pid);
+}
+
+void test_tool_stdin_write(const char *fmt, ...)
+{
+	struct gpiotool_proc *proc = &globals.test_ctx.tool_proc;
+	ssize_t written;
+	va_list va;
+	char *in;
+	int rv;
+
+	va_start(va, fmt);
+	rv = vasprintf(&in, fmt, va);
+	va_end(va);
+	if (rv < 0)
+		die_perr("error building string");
+
+	written = write(proc->stdin_fd, in, rv);
+	free(in);
+	if (written < 0)
+		die_perr("error writing to child process' stdin");
+	if (written != rv)
+		die("unable to write all data to child process' stdin");
+}
+
+void test_tool_run(char *tool, ...)
+{
+	int in_fds[2], out_fds[2], err_fds[2], rv;
+	struct gpiotool_proc *proc;
+	sigset_t sigmask;
+	char *path;
+	va_list va;
+
+	proc = &globals.test_ctx.tool_proc;
+	if (proc->running)
+		die("unable to start %s - another tool already running", tool);
+
+	if (proc->pid)
+		gpiotool_proc_cleanup();
+
+	event_lock();
+	if (globals.test_ctx.event.running)
+		die("refusing to fork when the event thread is running");
+	if (!globals.toolpath)
+		die("asked to run tests for gpio-tools, but the executables were not found");
+
+	gpiotool_proc_make_pipes(in_fds, out_fds, err_fds);
+
+	path = xappend(NULL, "%s/%s", globals.toolpath, tool);
+	rv = access(path, R_OK | X_OK);
+	if (rv)
+		die_perr("unable to execute '%s'", path);
+
+	sigemptyset(&sigmask);
+	sigaddset(&sigmask, SIGCHLD);
+
+	rv = sigprocmask(SIG_BLOCK, &sigmask, NULL);
+	if (rv)
+		die_perr("unable to block SIGCHLD");
+
+	proc->sig_fd = signalfd(-1, &sigmask, 0);
+	if (proc->sig_fd < 0)
+		die_perr("unable to create signalfd");
+
+	proc->pid = fork();
+	if (proc->pid < 0) {
+		die_perr("unable to fork");
+	} else if (proc->pid == 0) {
+		/* Child process. */
+		close(proc->sig_fd);
+		gpiotool_proc_dup_fds(in_fds[0], out_fds[1], err_fds[1]);
+		va_start(va, tool);
+		gpiotool_proc_exec(path, va);
+	} else {
+		/* Parent process. */
+		close(in_fds[0]);
+		proc->stdin_fd = in_fds[1];
+		close(out_fds[1]);
+		proc->stdout_fd = out_fds[0];
+		close(err_fds[1]);
+		proc->stderr_fd = err_fds[0];
+
+		proc->running = true;
+	}
+
+	event_unlock();
+	free(path);
+}
+
+static void gpiotool_readall(int fd, char **out)
+{
+	ssize_t rd;
+	int i;
+
+	memset(globals.pipebuf, 0, globals.pipesize);
+	rd = read(fd, globals.pipebuf, globals.pipesize);
+	if (rd < 0) {
+		die_perr("error reading output from subprocess");
+	} else if (rd == 0) {
+		*out = NULL;
+	} else {
+		for (i = 0; i < rd; i++) {
+			if (!isascii(globals.pipebuf[i]))
+				die("GPIO tool program printed a non-ASCII character");
+		}
+
+		*out = xzalloc(rd + 1);
+		memcpy(*out, globals.pipebuf, rd);
+	}
+}
+
+void test_tool_wait(void)
+{
+	struct signalfd_siginfo sinfo;
+	struct gpiotool_proc *proc;
+	struct pollfd pfd;
+	sigset_t sigmask;
+	ssize_t rd;
+	int rv;
+
+	proc = &globals.test_ctx.tool_proc;
+
+	if (!proc->running)
+		die("gpiotool process not running");
+
+	pfd.fd = proc->sig_fd;
+	pfd.events = POLLIN | POLLPRI;
+
+	rv = poll(&pfd, 1, 5000);
+	if (rv == 0)
+		die("tool program is taking too long to terminate");
+	else if (rv < 0)
+		die_perr("error when polling the signalfd");
+
+	rd = read(proc->sig_fd, &sinfo, sizeof(sinfo));
+	close(proc->sig_fd);
+	if (rd < 0)
+		die_perr("error reading signal info");
+	else if (rd != sizeof(sinfo))
+		die("invalid size of signal info");
+
+	sigemptyset(&sigmask);
+	sigaddset(&sigmask, SIGCHLD);
+
+	rv = sigprocmask(SIG_UNBLOCK, &sigmask, NULL);
+	if (rv)
+		die_perr("unable to unblock SIGCHLD");
+
+	gpiotool_readall(proc->stdout_fd, &proc->stdout);
+	gpiotool_readall(proc->stderr_fd, &proc->stderr);
+
+	waitpid(proc->pid, &proc->exit_status, 0);
+
+	close(proc->stdin_fd);
+	close(proc->stdout_fd);
+	close(proc->stderr_fd);
+
+	proc->running = false;
+}
+
+const char *test_tool_stdout(void)
+{
+	struct gpiotool_proc *proc = &globals.test_ctx.tool_proc;
+
+	return proc->stdout;
+}
+
+const char *test_tool_stderr(void)
+{
+	struct gpiotool_proc *proc = &globals.test_ctx.tool_proc;
+
+	return proc->stderr;
+}
+
+bool test_tool_exited(void)
+{
+	struct gpiotool_proc *proc = &globals.test_ctx.tool_proc;
+
+	return WIFEXITED(proc->exit_status);
+}
+
+int test_tool_exit_status(void)
+{
+	struct gpiotool_proc *proc = &globals.test_ctx.tool_proc;
+
+	return WEXITSTATUS(proc->exit_status);
+}
+
+static void check_kernel(void)
+{
+	unsigned int major, minor, release;
+	struct utsname un;
+	int rv;
+
+	msg("checking the linux kernel version");
+
+	rv = uname(&un);
+	if (rv)
+		die_perr("uname");
+
+	rv = sscanf(un.release, "%u.%u.%u", &major, &minor, &release);
+	if (rv != 3)
+		die("error reading kernel release version");
+
+	if (major < min_kern_major) {
+		goto bad_version;
+	} else if (major > min_kern_major) {
+		goto good_version;
+	} else {
+		if (minor < min_kern_minor) {
+			goto bad_version;
+		} else if (minor > min_kern_minor) {
+			goto good_version;
+		} else {
+			if (release < min_kern_release)
+				goto bad_version;
+			else
+				goto good_version;
+		}
+	}
+
+good_version:
+	msg("kernel release is v%u.%u.%u - ok to run tests",
+	    major, minor, release);
+
+	return;
+
+bad_version:
+	die("linux kernel version must be at least v%u.%u.%u - got v%u.%u.%u",
+	    min_kern_major, min_kern_minor, min_kern_release,
+	    major, minor, release);
+}
+
+static void check_gpio_mockup(void)
+{
+	const char *modpath;
+	int rv;
+
+	msg("checking gpio-mockup availability");
+
+	globals.module_ctx = kmod_new(NULL, NULL);
+	if (!globals.module_ctx)
+		die_perr("error creating kernel module context");
+
+	rv = kmod_module_new_from_name(globals.module_ctx,
+				       "gpio-mockup", &globals.module);
+	if (rv)
+		die_perr("error allocating module info");
+
+	/* First see if we can find the module. */
+	modpath = kmod_module_get_path(globals.module);
+	if (!modpath)
+		die("the gpio-mockup module does not exist in the system or is built into the kernel");
+
+	/* Then see if we can freely load and unload it. */
+	rv = kmod_module_probe_insert_module(globals.module, 0,
+					     "gpio_mockup_ranges=-1,4",
+					     NULL, NULL, NULL);
+	if (rv)
+		die_perr("unable to load gpio-mockup");
+
+	rv = kmod_module_remove_module(globals.module, 0);
+	if (rv)
+		die_perr("unable to remove gpio-mockup");
+
+	msg("gpio-mockup ok");
+}
+
+static void check_tool_path(void)
+{
+	/*
+	 * Let's check gpiodetect only and assume all the other tools are in
+	 * the same directory.
+	 */
+	static const char *const tool = "gpiodetect";
+
+	char *progpath, *progdir, *toolpath, *pathenv, *tok;
+
+	/* First check if we're running the from the top source directory. */
+	progpath = xstrdup(program_invocation_name);
+	progdir = dirname(progpath);
+
+	toolpath = xappend(NULL, "%s/../../tools/%s", progdir, tool);
+	if (access(toolpath, R_OK | X_OK) == 0) {
+		free(progpath);
+		goto out;
+	}
+	free(toolpath);
+
+	/* Is the tool in the same directory maybe? */
+	toolpath = xappend(NULL, "%s/%s", progdir, tool);
+	free(progpath);
+	if (access(toolpath, R_OK | X_OK) == 0)
+		goto out;
+	free(toolpath);
+
+	/* Next iterate over directories in $PATH. */
+	pathenv = getenv("PATH");
+	if (!pathenv)
+		return;
+
+	progpath = xstrdup(pathenv);
+	tok = strtok(progpath, ":");
+	while (tok) {
+		toolpath = xappend(NULL, "%s/%s", tok, tool);
+		if (access(toolpath, R_OK) == 0) {
+			free(progpath);
+			goto out;
+		}
+
+		free(toolpath);
+		tok = strtok(NULL, ":");
+	}
+
+	free(progpath);
+	toolpath = NULL;
+
+out:
+	if (toolpath) {
+		toolpath = dirname(toolpath);
+		msg("using gpio-tools from '%s'", toolpath);
+		globals.toolpath = toolpath;
+	}
+}
+
+static void load_module(struct _test_chip_descr *descr)
+{
+	unsigned int i;
+	char *modarg;
+	int rv;
+
+	if (descr->num_chips == 0)
+		return;
+
+	modarg = xappend(NULL, "gpio_mockup_ranges=");
+	for (i = 0; i < descr->num_chips; i++)
+		modarg = xappend(modarg, "-1,%u,", descr->num_lines[i]);
+	modarg[strlen(modarg) - 1] = '\0'; /* Remove the last comma. */
+
+	if (descr->flags & TEST_FLAG_NAMED_LINES)
+		modarg = xappend(modarg, " gpio_mockup_named_lines");
+
+	rv = kmod_module_probe_insert_module(globals.module, 0,
+					     modarg, NULL, NULL, NULL);
+	if (rv)
+		die_perr("unable to load gpio-mockup");
+
+	free(modarg);
+}
+
+static int chipcmp(const void *c1, const void *c2)
+{
+	const struct mockup_chip *chip1 = *(const struct mockup_chip **)c1;
+	const struct mockup_chip *chip2 = *(const struct mockup_chip **)c2;
+
+	return chip1->number > chip2->number;
+}
+
+static bool devpath_is_mockup(const char *devpath)
+{
+	static const char mockup_devpath[] = "/devices/platform/gpio-mockup";
+
+	return !strncmp(devpath, mockup_devpath, sizeof(mockup_devpath) - 1);
+}
+
+static void prepare_test(struct _test_chip_descr *descr)
+{
+	const char *devpath, *devnode, *sysname, *action;
+	struct udev_monitor *monitor;
+	unsigned int detected = 0;
+	struct test_context *ctx;
+	struct mockup_chip *chip;
+	struct udev_device *dev;
+	struct udev *udev_ctx;
+	struct pollfd pfd;
+	int rv;
+
+	ctx = &globals.test_ctx;
+	memset(ctx, 0, sizeof(*ctx));
+	ctx->num_chips = descr->num_chips;
+	ctx->chips = xcalloc(ctx->num_chips, sizeof(*ctx->chips));
+	pthread_mutex_init(&ctx->event.lock, NULL);
+	pthread_cond_init(&ctx->event.cond, NULL);
+
+	/*
+	 * We'll setup the udev monitor, insert the module and wait for the
+	 * mockup gpiochips to appear.
+	 */
+
+	udev_ctx = udev_new();
+	if (!udev_ctx)
+		die_perr("error creating udev context");
+
+	monitor = udev_monitor_new_from_netlink(udev_ctx, "udev");
+	if (!monitor)
+		die_perr("error creating udev monitor");
+
+	rv = udev_monitor_filter_add_match_subsystem_devtype(monitor,
+							     "gpio", NULL);
+	if (rv < 0)
+		die_perr("error adding udev filters");
+
+	rv = udev_monitor_enable_receiving(monitor);
+	if (rv < 0)
+		die_perr("error enabling udev event receiving");
+
+	load_module(descr);
+
+	pfd.fd = udev_monitor_get_fd(monitor);
+	pfd.events = POLLIN | POLLPRI;
+
+	while (ctx->num_chips > detected) {
+		rv = poll(&pfd, 1, 5000);
+		if (rv < 0)
+			die_perr("error polling for uevents");
+		else if (rv == 0)
+			die("timeout waiting for gpiochips");
+
+		dev = udev_monitor_receive_device(monitor);
+		if (!dev)
+			die_perr("error reading device info");
+
+		devpath = udev_device_get_devpath(dev);
+		devnode = udev_device_get_devnode(dev);
+		sysname = udev_device_get_sysname(dev);
+		action = udev_device_get_action(dev);
+
+		if (!devpath || !devnode || !sysname ||
+		    !devpath_is_mockup(devpath) ||
+		    strcmp(action, "add") != 0) {
+			udev_device_unref(dev);
+			continue;
+		}
+
+		chip = xzalloc(sizeof(*chip));
+		chip->name = xstrdup(sysname);
+		chip->path = xstrdup(devnode);
+		rv = sscanf(sysname, "gpiochip%u", &chip->number);
+		if (rv != 1)
+			die("unable to determine chip number");
+
+		ctx->chips[detected++] = chip;
+		udev_device_unref(dev);
+	}
+
+	udev_monitor_unref(monitor);
+	udev_unref(udev_ctx);
+
+	/*
+	 * We can't assume that the order in which the mockup gpiochip
+	 * devices are created will be deterministic, yet we want the
+	 * index passed to the test_chip_*() functions to correspond with the
+	 * order in which the chips were defined in the TEST_DEFINE()
+	 * macro.
+	 *
+	 * Once all gpiochips are there, sort them by chip number.
+	 */
+	qsort(ctx->chips, ctx->num_chips, sizeof(*ctx->chips), chipcmp);
+}
+
+static void run_test(struct _test_case *test)
+{
+	errno = 0;
+
+	print_header("TEST", CYELLOW);
+	pr_raw("'%s': ", test->name);
+
+	globals.test_ctx.running = true;
+	test->func();
+	globals.test_ctx.running = false;
+
+	if (globals.test_ctx.test_failed) {
+		globals.tests_failed++;
+		set_color(CREDBOLD);
+		pr_raw("FAILED:");
+		reset_color();
+		set_color(CRED);
+		pr_raw("\n\t\t'%s': %s\n",
+		       test->name, globals.test_ctx.failed_msg);
+		reset_color();
+		free(globals.test_ctx.failed_msg);
+	} else {
+		set_color(CGREEN);
+		pr_raw("PASS\n");
+		reset_color();
+	}
+}
+
+static void teardown_test(void)
+{
+	struct gpiotool_proc *tool_proc;
+	struct mockup_chip *chip;
+	struct event_thread *ev;
+	unsigned int i;
+	int rv;
+
+	event_lock();
+	ev = &globals.test_ctx.event;
+
+	if (ev->running) {
+		ev->should_stop = true;
+		pthread_cond_broadcast(&ev->cond);
+		event_unlock();
+
+		rv = pthread_join(globals.test_ctx.event.thread_id, NULL);
+		if (rv != 0)
+			die("error joining event thread: %s",
+			    strerror(rv));
+
+		pthread_mutex_destroy(&globals.test_ctx.event.lock);
+		pthread_cond_destroy(&globals.test_ctx.event.cond);
+	} else {
+		event_unlock();
+	}
+
+	tool_proc = &globals.test_ctx.tool_proc;
+	if (tool_proc->running) {
+		test_tool_signal(SIGKILL);
+		test_tool_wait();
+	}
+
+	if (tool_proc->pid)
+		gpiotool_proc_cleanup();
+
+	for (i = 0; i < globals.test_ctx.num_chips; i++) {
+		chip = globals.test_ctx.chips[i];
+
+		free(chip->path);
+		free(chip->name);
+		free(chip);
+	}
+
+	free(globals.test_ctx.chips);
+
+	if (globals.test_ctx.custom_str)
+		free(globals.test_ctx.custom_str);
+
+	if (mockup_loaded()) {
+		rv = kmod_module_remove_module(globals.module, 0);
+		if (rv)
+			die_perr("unable to remove gpio-mockup");
+	}
+}
+
+int main(int argc TEST_UNUSED, char **argv TEST_UNUSED)
+{
+	struct _test_case *test;
+
+	globals.main_pid = getpid();
+	globals.pipesize = get_pipesize();
+	globals.pipebuf = xmalloc(globals.pipesize);
+	atexit(cleanup_func);
+
+	msg("libgpiod test suite");
+	msg("%u tests registered", globals.num_tests);
+
+	check_kernel();
+	check_gpio_mockup();
+	check_tool_path();
+
+	msg("running tests");
+
+	for (test = globals.test_list_head; test; test = test->_next) {
+		prepare_test(&test->chip_descr);
+		run_test(test);
+		teardown_test();
+	}
+
+	if (!globals.tests_failed)
+		msg("all tests passed");
+	else
+		err("%u out of %u tests failed",
+		    globals.tests_failed, globals.num_tests);
+
+	return globals.tests_failed ? EXIT_FAILURE : EXIT_SUCCESS;
+}
+
+void test_close_chip(struct gpiod_chip **chip)
+{
+	if (*chip)
+		gpiod_chip_close(*chip);
+}
+
+void test_line_close_chip(struct gpiod_line **line)
+{
+	if (*line)
+		gpiod_line_close_chip(*line);
+}
+
+void test_free_chip_iter(struct gpiod_chip_iter **iter)
+{
+	if (*iter)
+		gpiod_chip_iter_free(*iter);
+}
+
+void test_free_line_iter(struct gpiod_line_iter **iter)
+{
+	if (*iter)
+		gpiod_line_iter_free(*iter);
+}
+
+void test_free_chip_iter_noclose(struct gpiod_chip_iter **iter)
+{
+	if (*iter)
+		gpiod_chip_iter_free_noclose(*iter);
+}
+
+const char *test_chip_path(unsigned int index)
+{
+	check_chip_index(index);
+
+	return globals.test_ctx.chips[index]->path;
+}
+
+const char *test_chip_name(unsigned int index)
+{
+	check_chip_index(index);
+
+	return globals.test_ctx.chips[index]->name;
+}
+
+unsigned int test_chip_num(unsigned int index)
+{
+	check_chip_index(index);
+
+	return globals.test_ctx.chips[index]->number;
+}
+
+void _test_register(struct _test_case *test)
+{
+	struct _test_case *tmp;
+
+	if (!globals.test_list_head) {
+		globals.test_list_head = globals.test_list_tail = test;
+		test->_next = NULL;
+	} else {
+		tmp = globals.test_list_tail;
+		globals.test_list_tail = test;
+		test->_next = NULL;
+		tmp->_next = test;
+	}
+
+	globals.num_tests++;
+}
+
+void _test_print_failed(const char *fmt, ...)
+{
+	va_list va;
+	int rv;
+
+	va_start(va, fmt);
+	rv = vasprintf(&globals.test_ctx.failed_msg, fmt, va);
+	va_end(va);
+	if (rv < 0)
+		die_perr("vasprintf");
+
+	globals.test_ctx.test_failed = true;
+}
+
+void test_set_event(unsigned int chip_index, unsigned int line_offset,
+		    int event_type, unsigned int freq)
+{
+	struct event_thread *ev = &globals.test_ctx.event;
+	int rv;
+
+	event_lock();
+
+	if (!ev->running) {
+		rv = pthread_create(&ev->thread_id, NULL, event_worker, NULL);
+		if (rv != 0)
+			die("error creating event thread: %s",
+			    strerror(rv));
+
+		ev->running = true;
+	}
+
+	ev->chip_index = chip_index;
+	ev->line_offset = line_offset;
+	ev->event_type = event_type;
+	ev->freq = freq;
+
+	pthread_cond_broadcast(&ev->cond);
+
+	event_unlock();
+}
+
+bool test_regex_match(const char *str, const char *pattern)
+{
+	char errbuf[128];
+	regex_t regex;
+	bool ret;
+	int rv;
+
+	rv = regcomp(&regex, pattern, REG_EXTENDED | REG_NEWLINE);
+	if (rv) {
+		regerror(rv, &regex, errbuf, sizeof(errbuf));
+		die("unable to compile regex '%s': %s", pattern, errbuf);
+	}
+
+	rv = regexec(&regex, str, 0, 0, 0);
+	if (rv == REG_NOERROR) {
+		ret = true;
+	} else if (rv == REG_NOMATCH) {
+		ret = false;
+	} else {
+		regerror(rv, &regex, errbuf, sizeof(errbuf));
+		die("unable to run a regex match: %s", errbuf);
+	}
+
+	regfree(&regex);
+
+	return ret;
+}
+
+const char *test_build_str(const char *fmt, ...)
+{
+	va_list va;
+	char *str;
+	int rv;
+
+	if (globals.test_ctx.custom_str)
+		free(globals.test_ctx.custom_str);
+
+	va_start(va, fmt);
+	rv = vasprintf(&str, fmt, va);
+	va_end(va);
+	if (rv < 0)
+		die_perr("error creating custom string");
+
+	globals.test_ctx.custom_str = str;
+
+	return str;
+}
diff --git a/tests/gpiod-test.h b/tests/gpiod-test.h
new file mode 100644
index 0000000..7b02408
--- /dev/null
+++ b/tests/gpiod-test.h
@@ -0,0 +1,164 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Testing framework - functions and definitions used by test cases. */
+
+#ifndef __GPIOD_TEST_H__
+#define __GPIOD_TEST_H__
+
+#include <gpiod.h>
+#include <string.h>
+
+#define TEST_CONSUMER		"gpiod-test"
+
+#define TEST_INIT		__attribute__((constructor))
+#define TEST_UNUSED		__attribute__((unused))
+#define TEST_PRINTF(fmt, arg)	__attribute__((format(printf, fmt, arg)))
+#define TEST_CLEANUP(func)	__attribute__((cleanup(func)))
+#define TEST_BIT(nr)		(1UL << (nr))
+
+#define TEST_ARRAY_SIZE(x)	(sizeof(x) / sizeof(*(x)))
+
+typedef void (*_test_func)(void);
+
+struct _test_chip_descr {
+	unsigned int num_chips;
+	unsigned int *num_lines;
+	int flags;
+};
+
+struct _test_case {
+	struct _test_case *_next;
+
+	const char *name;
+	_test_func func;
+
+	struct _test_chip_descr chip_descr;
+};
+
+void _test_register(struct _test_case *test);
+void _test_print_failed(const char *fmt, ...) TEST_PRINTF(1, 2);
+
+enum {
+	TEST_FLAG_NAMED_LINES = TEST_BIT(0),
+};
+
+/*
+ * This macro should be used for code brevity instead of manually declaring
+ * the _test_case structure.
+ *
+ * The macro accepts the following arguments:
+ *   _a_func: name of the test function
+ *   _a_name: name of the test case (will be shown to user)
+ *   _a_flags: various switches for the test case
+ *
+ * The last argument must be an array of unsigned integers specifying the
+ * number of GPIO lines in each subsequent mockup chip. The size of this
+ * array will at the same time specify the number of gpiochips to create.
+ */
+#define TEST_DEFINE(_a_func, _a_name, _a_flags, ...)			\
+	static unsigned int _##_a_func##_lines[] = __VA_ARGS__;		\
+	static struct _test_case _##_a_func##_descr = {			\
+		.name = _a_name,					\
+		.func = _a_func,					\
+		.chip_descr = {						\
+			.num_chips = TEST_ARRAY_SIZE(			\
+						_##_a_func##_lines),	\
+			.num_lines = _##_a_func##_lines,		\
+			.flags = _a_flags,				\
+		},							\
+	};								\
+	static TEST_INIT void _test_register_##_a_func##_test(void)	\
+	{								\
+		_test_register(&_##_a_func##_descr);			\
+	}								\
+	static int _test_##_a_func##_sentinel TEST_UNUSED
+
+/*
+ * We want libgpiod tests to co-exist with gpiochips created by other GPIO
+ * drivers. For that reason we can't just use hardcoded device file paths or
+ * gpiochip names.
+ *
+ * The test suite detects the chips that were exported by the gpio-mockup
+ * module and stores them in the internal test context structure. Test cases
+ * should use the routines declared below to access the gpiochip path, name
+ * or number by index corresponding with the order in which the mockup chips
+ * were requested in the TEST_DEFINE() macro.
+ */
+const char *test_chip_path(unsigned int index);
+const char *test_chip_name(unsigned int index);
+unsigned int test_chip_num(unsigned int index);
+
+enum {
+	TEST_EVENT_FALLING,
+	TEST_EVENT_RISING,
+	TEST_EVENT_ALTERNATING,
+};
+
+void test_set_event(unsigned int chip_index, unsigned int line_offset,
+		    int event_type, unsigned int freq);
+
+void test_tool_run(char *tool, ...);
+void test_tool_wait(void);
+const char *test_tool_stdout(void);
+const char *test_tool_stderr(void);
+bool test_tool_exited(void);
+int test_tool_exit_status(void);
+void test_tool_signal(int signum);
+void test_tool_stdin_write(const char *fmt, ...) TEST_PRINTF(1, 2);
+
+/*
+ * Every TEST_ASSERT_*() macro expansion can make a test function return, so
+ * it would be quite difficult to keep track of every resource allocation. At
+ * the same time we don't want any deliberate memory leaks as we also use this
+ * test suite to find actual memory leaks in the library with valgrind.
+ *
+ * For this reason, the tests should generally always use the TEST_CLEANUP()
+ * macro for dynamically allocated variables and objects that need closing.
+ *
+ * The functions below can be reused by different tests for common use cases.
+ */
+void test_close_chip(struct gpiod_chip **chip);
+void test_line_close_chip(struct gpiod_line **line);
+void test_free_chip_iter(struct gpiod_chip_iter **iter);
+void test_free_chip_iter_noclose(struct gpiod_chip_iter **iter);
+void test_free_line_iter(struct gpiod_line_iter **iter);
+
+#define TEST_CLEANUP_CHIP TEST_CLEANUP(test_close_chip)
+
+bool test_regex_match(const char *str, const char *pattern);
+
+/*
+ * Return a custom string built according to printf() formatting rules. The
+ * returned string is valid until the next call to this routine.
+ */
+const char *test_build_str(const char *fmt, ...) TEST_PRINTF(1, 2);
+
+#define TEST_ASSERT(statement)						\
+	do {								\
+		if (!(statement)) {					\
+			_test_print_failed(				\
+				"assertion failed (%s:%d): '%s'",	\
+				__FILE__, __LINE__, #statement);	\
+			return;						\
+		}							\
+	} while (0)
+
+#define TEST_ASSERT_FALSE(statement)	TEST_ASSERT(!(statement))
+#define TEST_ASSERT_NOT_NULL(ptr)	TEST_ASSERT((ptr) != NULL)
+#define TEST_ASSERT_RET_OK(status)	TEST_ASSERT((status) == 0)
+#define TEST_ASSERT_NULL(ptr)		TEST_ASSERT((ptr) == NULL)
+#define TEST_ASSERT_ERRNO_IS(errnum)	TEST_ASSERT(errno == (errnum))
+#define TEST_ASSERT_EQ(a1, a2)		TEST_ASSERT((a1) == (a2))
+#define TEST_ASSERT_NOTEQ(a1, a2)	TEST_ASSERT((a1) != (a2))
+#define TEST_ASSERT_STR_EQ(s1, s2)	TEST_ASSERT(strcmp(s1, s2) == 0)
+#define TEST_ASSERT_STR_CONTAINS(s, p)	TEST_ASSERT(strstr(s, p) != NULL)
+#define TEST_ASSERT_STR_NOTCONT(s, p)	TEST_ASSERT(strstr(s, p) == NULL)
+#define TEST_ASSERT_REGEX_MATCH(s, p)	TEST_ASSERT(test_regex_match(s, p))
+#define TEST_ASSERT_REGEX_NOMATCH(s, p)	TEST_ASSERT(!test_regex_match(s, p))
+
+#endif /* __GPIOD_TEST_H__ */
diff --git a/tests/tests-chip.c b/tests/tests-chip.c
new file mode 100644
index 0000000..0acb5c7
--- /dev/null
+++ b/tests/tests-chip.c
@@ -0,0 +1,336 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Test cases for GPIO chip handling. */
+
+#include <errno.h>
+#include <stdio.h>
+
+#include "gpiod-test.h"
+
+static void chip_open_good(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+}
+TEST_DEFINE(chip_open_good,
+	    "gpiod_chip_open() - good",
+	    0, { 8 });
+
+static void chip_open_nonexistent(void)
+{
+	struct gpiod_chip *chip;
+
+	chip = gpiod_chip_open("/dev/nonexistent_gpiochip");
+	TEST_ASSERT_NULL(chip);
+	TEST_ASSERT_ERRNO_IS(ENOENT);
+}
+TEST_DEFINE(chip_open_nonexistent,
+	    "gpiod_chip_open() - nonexistent chip",
+	    0, { 8 });
+
+static void chip_open_notty(void)
+{
+	struct gpiod_chip *chip;
+
+	chip = gpiod_chip_open("/dev/null");
+	TEST_ASSERT_NULL(chip);
+	TEST_ASSERT_ERRNO_IS(ENOTTY);
+}
+TEST_DEFINE(chip_open_notty,
+	    "gpiod_chip_open() - notty",
+	    0, { 8 });
+
+static void chip_open_by_name_good(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+
+	chip = gpiod_chip_open_by_name(test_chip_name(0));
+	TEST_ASSERT_NOT_NULL(chip);
+}
+TEST_DEFINE(chip_open_by_name_good,
+	    "gpiod_chip_open_by_name() - good",
+	    0, { 8 });
+
+static void chip_open_by_number_good(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+
+	chip = gpiod_chip_open_by_number(test_chip_num(0));
+	TEST_ASSERT_NOT_NULL(chip);
+}
+TEST_DEFINE(chip_open_by_number_good,
+	    "gpiod_chip_open_by_number() - good",
+	    0, { 8 });
+
+static void chip_open_lookup(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip_by_label = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip_by_name = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip_by_path = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip_by_num = NULL;
+
+	chip_by_name = gpiod_chip_open_lookup(test_chip_name(1));
+	chip_by_path = gpiod_chip_open_lookup(test_chip_path(1));
+	chip_by_num = gpiod_chip_open_lookup(
+				test_build_str("%u", test_chip_num(1)));
+	chip_by_label = gpiod_chip_open_lookup("gpio-mockup-B");
+
+	TEST_ASSERT_NOT_NULL(chip_by_name);
+	TEST_ASSERT_NOT_NULL(chip_by_path);
+	TEST_ASSERT_NOT_NULL(chip_by_num);
+	TEST_ASSERT_NOT_NULL(chip_by_label);
+
+	TEST_ASSERT_STR_EQ(gpiod_chip_name(chip_by_name), test_chip_name(1));
+	TEST_ASSERT_STR_EQ(gpiod_chip_name(chip_by_path), test_chip_name(1));
+	TEST_ASSERT_STR_EQ(gpiod_chip_name(chip_by_num), test_chip_name(1));
+	TEST_ASSERT_STR_EQ(gpiod_chip_name(chip_by_label), test_chip_name(1));
+}
+TEST_DEFINE(chip_open_lookup,
+	    "gpiod_chip_open_lookup() - good",
+	    0, { 8, 8, 8 });
+
+static void chip_open_by_label_good(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+
+	chip = gpiod_chip_open_by_label("gpio-mockup-D");
+	TEST_ASSERT_NOT_NULL(chip);
+	TEST_ASSERT_STR_EQ(gpiod_chip_name(chip), test_chip_name(3));
+}
+TEST_DEFINE(chip_open_by_label_good,
+	    "gpiod_chip_open_by_label() - good",
+	    0, { 4, 4, 4, 4, 4 });
+
+static void chip_open_by_label_bad(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+
+	chip = gpiod_chip_open_by_label("nonexistent_gpio_chip");
+	TEST_ASSERT_NULL(chip);
+	TEST_ASSERT_ERRNO_IS(ENOENT);
+}
+TEST_DEFINE(chip_open_by_label_bad,
+	    "gpiod_chip_open_by_label() - bad",
+	    0, { 4, 4, 4, 4, 4 });
+
+static void chip_name(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip0 = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip1 = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip2 = NULL;
+
+	chip0 = gpiod_chip_open(test_chip_path(0));
+	chip1 = gpiod_chip_open(test_chip_path(1));
+	chip2 = gpiod_chip_open(test_chip_path(2));
+	TEST_ASSERT_NOT_NULL(chip0);
+	TEST_ASSERT_NOT_NULL(chip1);
+	TEST_ASSERT_NOT_NULL(chip2);
+
+	TEST_ASSERT_STR_EQ(gpiod_chip_name(chip0), test_chip_name(0));
+	TEST_ASSERT_STR_EQ(gpiod_chip_name(chip1), test_chip_name(1));
+	TEST_ASSERT_STR_EQ(gpiod_chip_name(chip2), test_chip_name(2));
+}
+TEST_DEFINE(chip_name,
+	    "gpiod_chip_name()",
+	    0, { 8, 8, 8 });
+
+static void chip_label(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip0 = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip1 = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip2 = NULL;
+
+	chip0 = gpiod_chip_open(test_chip_path(0));
+	chip1 = gpiod_chip_open(test_chip_path(1));
+	chip2 = gpiod_chip_open(test_chip_path(2));
+	TEST_ASSERT_NOT_NULL(chip0);
+	TEST_ASSERT_NOT_NULL(chip1);
+	TEST_ASSERT_NOT_NULL(chip2);
+
+	TEST_ASSERT_STR_EQ(gpiod_chip_label(chip0), "gpio-mockup-A");
+	TEST_ASSERT_STR_EQ(gpiod_chip_label(chip1), "gpio-mockup-B");
+	TEST_ASSERT_STR_EQ(gpiod_chip_label(chip2), "gpio-mockup-C");
+}
+TEST_DEFINE(chip_label,
+	    "gpiod_chip_label()",
+	    0, { 8, 8, 8 });
+
+static void chip_num_lines(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip0 = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip1 = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip2 = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip3 = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip4 = NULL;
+
+	chip0 = gpiod_chip_open(test_chip_path(0));
+	chip1 = gpiod_chip_open(test_chip_path(1));
+	chip2 = gpiod_chip_open(test_chip_path(2));
+	chip3 = gpiod_chip_open(test_chip_path(3));
+	chip4 = gpiod_chip_open(test_chip_path(4));
+	TEST_ASSERT_NOT_NULL(chip0);
+	TEST_ASSERT_NOT_NULL(chip1);
+	TEST_ASSERT_NOT_NULL(chip2);
+	TEST_ASSERT_NOT_NULL(chip3);
+	TEST_ASSERT_NOT_NULL(chip4);
+
+	TEST_ASSERT_EQ(gpiod_chip_num_lines(chip0), 1);
+	TEST_ASSERT_EQ(gpiod_chip_num_lines(chip1), 4);
+	TEST_ASSERT_EQ(gpiod_chip_num_lines(chip2), 8);
+	TEST_ASSERT_EQ(gpiod_chip_num_lines(chip3), 16);
+	TEST_ASSERT_EQ(gpiod_chip_num_lines(chip4), 32);
+}
+TEST_DEFINE(chip_num_lines,
+	    "gpiod_chip_num_lines()",
+	    0, { 1, 4, 8, 16, 32 });
+
+static void chip_get_lines(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line_bulk bulk;
+	unsigned int offsets[4];
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	offsets[0] = 1;
+	offsets[1] = 3;
+	offsets[2] = 4;
+	offsets[3] = 7;
+
+	rv = gpiod_chip_get_lines(chip, offsets, 4, &bulk);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(gpiod_line_bulk_num_lines(&bulk), 4);
+	line = gpiod_line_bulk_get_line(&bulk, 0);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 1);
+	line = gpiod_line_bulk_get_line(&bulk, 1);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 3);
+	line = gpiod_line_bulk_get_line(&bulk, 2);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 4);
+	line = gpiod_line_bulk_get_line(&bulk, 3);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 7);
+}
+TEST_DEFINE(chip_get_lines,
+	    "gpiod_chip_get_lines()",
+	    0, { 16 });
+
+static void chip_get_all_lines(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line_bulk bulk;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	rv = gpiod_chip_get_all_lines(chip, &bulk);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(gpiod_line_bulk_num_lines(&bulk), 4);
+	line = gpiod_line_bulk_get_line(&bulk, 0);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 0);
+	line = gpiod_line_bulk_get_line(&bulk, 1);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 1);
+	line = gpiod_line_bulk_get_line(&bulk, 2);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 2);
+	line = gpiod_line_bulk_get_line(&bulk, 3);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 3);
+}
+TEST_DEFINE(chip_get_all_lines,
+	    "gpiod_chip_get_all_lines()",
+	    0, { 4 });
+
+static void chip_find_line_good(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line;
+
+	chip = gpiod_chip_open(test_chip_path(1));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_find_line(chip, "gpio-mockup-B-4");
+	TEST_ASSERT_NOT_NULL(line);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 4);
+	TEST_ASSERT_STR_EQ(gpiod_line_name(line), "gpio-mockup-B-4");
+}
+TEST_DEFINE(chip_find_line_good,
+	    "gpiod_chip_find_line() - good",
+	    TEST_FLAG_NAMED_LINES, { 8, 8, 8 });
+
+static void chip_find_line_not_found(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line;
+
+	chip = gpiod_chip_open(test_chip_path(1));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_find_line(chip, "nonexistent");
+	TEST_ASSERT_NULL(line);
+	TEST_ASSERT_ERRNO_IS(ENOENT);
+}
+TEST_DEFINE(chip_find_line_not_found,
+	    "gpiod_chip_find_line() - not found",
+	    TEST_FLAG_NAMED_LINES, { 8, 8, 8 });
+
+static void chip_find_lines_good(void)
+{
+	static const char *names[] = { "gpio-mockup-B-3",
+				       "gpio-mockup-B-6",
+				       "gpio-mockup-B-7",
+				       NULL };
+
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line_bulk bulk;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(1));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	rv = gpiod_chip_find_lines(chip, names, &bulk);
+	TEST_ASSERT_RET_OK(rv);
+	TEST_ASSERT_EQ(gpiod_line_bulk_num_lines(&bulk), 3);
+	line = gpiod_line_bulk_get_line(&bulk, 0);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 3);
+	line = gpiod_line_bulk_get_line(&bulk, 1);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 6);
+	line = gpiod_line_bulk_get_line(&bulk, 2);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 7);
+}
+TEST_DEFINE(chip_find_lines_good,
+	    "gpiod_chip_find_lines() - good",
+	    TEST_FLAG_NAMED_LINES, { 8, 8, 8 });
+
+static void chip_find_lines_not_found(void)
+{
+	static const char *names[] = { "gpio-mockup-B-3",
+				       "nonexistent",
+				       "gpio-mockup-B-7",
+				       NULL };
+
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line_bulk bulk;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(1));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	rv = gpiod_chip_find_lines(chip, names, &bulk);
+	TEST_ASSERT_EQ(rv, -1);
+	TEST_ASSERT_ERRNO_IS(ENOENT);
+}
+TEST_DEFINE(chip_find_lines_not_found,
+	    "gpiod_chip_find_lines() - not found",
+	    TEST_FLAG_NAMED_LINES, { 8, 8, 8 });
diff --git a/tests/tests-ctxless.c b/tests/tests-ctxless.c
new file mode 100644
index 0000000..638274f
--- /dev/null
+++ b/tests/tests-ctxless.c
@@ -0,0 +1,311 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Test cases for the high-level API. */
+
+#include <errno.h>
+
+#include "gpiod-test.h"
+
+static void ctxless_set_get_value(void)
+{
+	int rv;
+
+	rv = gpiod_ctxless_get_value(test_chip_name(0), 3,
+				     false, TEST_CONSUMER);
+	TEST_ASSERT_EQ(rv, 0);
+
+	rv = gpiod_ctxless_set_value(test_chip_name(0), 3, 1,
+				     false, TEST_CONSUMER, NULL, NULL);
+	TEST_ASSERT_RET_OK(rv);
+
+	rv = gpiod_ctxless_get_value(test_chip_name(0), 3,
+				     false, TEST_CONSUMER);
+	TEST_ASSERT_EQ(rv, 1);
+}
+TEST_DEFINE(ctxless_set_get_value,
+	    "ctxless set/get value - single line",
+	    0, { 8 });
+
+static void ctxless_set_get_value_multiple(void)
+{
+	unsigned int offsets[] = { 0, 1, 2, 3, 4, 5, 6, 12, 13, 15 };
+	int values[10], rv;
+
+	rv = gpiod_ctxless_get_value_multiple(test_chip_name(0), offsets,
+					      values, 10, false, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(values[0], 0);
+	TEST_ASSERT_EQ(values[1], 0);
+	TEST_ASSERT_EQ(values[2], 0);
+	TEST_ASSERT_EQ(values[3], 0);
+	TEST_ASSERT_EQ(values[4], 0);
+	TEST_ASSERT_EQ(values[5], 0);
+	TEST_ASSERT_EQ(values[6], 0);
+	TEST_ASSERT_EQ(values[7], 0);
+	TEST_ASSERT_EQ(values[8], 0);
+	TEST_ASSERT_EQ(values[9], 0);
+
+	values[0] = 1;
+	values[1] = 1;
+	values[2] = 1;
+	values[3] = 0;
+	values[4] = 0;
+	values[5] = 1;
+	values[6] = 0;
+	values[7] = 1;
+	values[8] = 0;
+	values[9] = 0;
+
+	rv = gpiod_ctxless_set_value_multiple(test_chip_name(0), offsets,
+					      values, 10, false, TEST_CONSUMER,
+					      NULL, NULL);
+	TEST_ASSERT_RET_OK(rv);
+
+	rv = gpiod_ctxless_get_value_multiple(test_chip_name(0), offsets,
+					      values, 10, false, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(values[0], 1);
+	TEST_ASSERT_EQ(values[1], 1);
+	TEST_ASSERT_EQ(values[2], 1);
+	TEST_ASSERT_EQ(values[3], 0);
+	TEST_ASSERT_EQ(values[4], 0);
+	TEST_ASSERT_EQ(values[5], 1);
+	TEST_ASSERT_EQ(values[6], 0);
+	TEST_ASSERT_EQ(values[7], 1);
+	TEST_ASSERT_EQ(values[8], 0);
+	TEST_ASSERT_EQ(values[9], 0);
+}
+TEST_DEFINE(ctxless_set_get_value_multiple,
+	    "ctxless set/get value - multiple lines",
+	    0, { 16 });
+
+static void ctxless_get_value_multiple_max_lines(void)
+{
+	unsigned int offsets[GPIOD_LINE_BULK_MAX_LINES + 1];
+	int values[GPIOD_LINE_BULK_MAX_LINES + 1], rv;
+
+	rv = gpiod_ctxless_get_value_multiple(test_chip_name(0), offsets,
+					      values,
+					      GPIOD_LINE_BULK_MAX_LINES + 1,
+					      false, TEST_CONSUMER);
+	TEST_ASSERT_NOTEQ(rv, 0);
+	TEST_ASSERT_ERRNO_IS(EINVAL);
+}
+TEST_DEFINE(ctxless_get_value_multiple_max_lines,
+	    "gpiod_ctxless_get_value_multiple() exceed max lines",
+	    0, { 128 });
+
+static void ctxless_set_value_multiple_max_lines(void)
+{
+	unsigned int offsets[GPIOD_LINE_BULK_MAX_LINES + 1];
+	int values[GPIOD_LINE_BULK_MAX_LINES + 1], rv;
+
+	rv = gpiod_ctxless_set_value_multiple(test_chip_name(0), offsets,
+					      values,
+					      GPIOD_LINE_BULK_MAX_LINES + 1,
+					      false, TEST_CONSUMER,
+					      NULL, NULL);
+	TEST_ASSERT_NOTEQ(rv, 0);
+	TEST_ASSERT_ERRNO_IS(EINVAL);
+}
+TEST_DEFINE(ctxless_set_value_multiple_max_lines,
+	    "gpiod_ctxless_set_value_multiple() exceed max lines",
+	    0, { 128 });
+
+struct ctxless_event_data {
+	bool got_rising_edge;
+	bool got_falling_edge;
+	unsigned int offset;
+	unsigned int count;
+};
+
+static int ctxless_event_cb(int evtype, unsigned int offset,
+			   const struct timespec *ts TEST_UNUSED, void *data)
+{
+	struct ctxless_event_data *evdata = data;
+
+	if (evtype == GPIOD_CTXLESS_EVENT_CB_RISING_EDGE)
+		evdata->got_rising_edge = true;
+	else if (evtype == GPIOD_CTXLESS_EVENT_CB_FALLING_EDGE)
+		evdata->got_falling_edge = true;
+
+	evdata->offset = offset;
+
+	return ++evdata->count == 2 ? GPIOD_CTXLESS_EVENT_CB_RET_STOP
+				    : GPIOD_CTXLESS_EVENT_CB_RET_OK;
+}
+
+static void ctxless_event_monitor(void)
+{
+	struct ctxless_event_data evdata = { false, false, 0, 0 };
+	struct timespec ts = { 1, 0 };
+	int rv;
+
+	test_set_event(0, 3, TEST_EVENT_ALTERNATING, 100);
+
+	rv = gpiod_ctxless_event_monitor(test_chip_name(0),
+					 GPIOD_CTXLESS_EVENT_BOTH_EDGES,
+					 3, false, TEST_CONSUMER, &ts,
+					 NULL, ctxless_event_cb, &evdata);
+
+	TEST_ASSERT_RET_OK(rv);
+	TEST_ASSERT(evdata.got_rising_edge);
+	TEST_ASSERT(evdata.got_falling_edge);
+	TEST_ASSERT_EQ(evdata.count, 2);
+	TEST_ASSERT_EQ(evdata.offset, 3);
+}
+TEST_DEFINE(ctxless_event_monitor,
+	    "gpiod_ctxless_event_monitor() - single event",
+	    0, { 8 });
+
+static void ctxless_event_monitor_single_event_type(void)
+{
+	struct ctxless_event_data evdata = { false, false, 0, 0 };
+	struct timespec ts = { 1, 0 };
+	int rv;
+
+	test_set_event(0, 3, TEST_EVENT_ALTERNATING, 100);
+
+	rv = gpiod_ctxless_event_monitor(test_chip_name(0),
+					 GPIOD_CTXLESS_EVENT_FALLING_EDGE,
+					 3, false, TEST_CONSUMER, &ts,
+					 NULL, ctxless_event_cb, &evdata);
+
+	TEST_ASSERT_RET_OK(rv);
+	TEST_ASSERT(evdata.got_falling_edge);
+	TEST_ASSERT_FALSE(evdata.got_rising_edge);
+	TEST_ASSERT_EQ(evdata.count, 2);
+	TEST_ASSERT_EQ(evdata.offset, 3);
+}
+TEST_DEFINE(ctxless_event_monitor_single_event_type,
+	    "gpiod_ctxless_event_monitor() - specify event type",
+	    0, { 8 });
+
+static void ctxless_event_monitor_multiple(void)
+{
+	struct ctxless_event_data evdata = { false, false, 0, 0 };
+	struct timespec ts = { 1, 0 };
+	unsigned int offsets[4];
+	int rv;
+
+	offsets[0] = 2;
+	offsets[1] = 3;
+	offsets[2] = 5;
+	offsets[3] = 6;
+
+	test_set_event(0, 3, TEST_EVENT_ALTERNATING, 100);
+
+	rv = gpiod_ctxless_event_monitor_multiple(
+					test_chip_name(0),
+					GPIOD_CTXLESS_EVENT_BOTH_EDGES,
+					offsets, 4, false, TEST_CONSUMER,
+					&ts, NULL, ctxless_event_cb, &evdata);
+
+	TEST_ASSERT_RET_OK(rv);
+	TEST_ASSERT(evdata.got_rising_edge);
+	TEST_ASSERT(evdata.got_falling_edge);
+	TEST_ASSERT_EQ(evdata.count, 2);
+	TEST_ASSERT_EQ(evdata.offset, 3);
+}
+TEST_DEFINE(ctxless_event_monitor_multiple,
+	    "gpiod_ctxless_event_monitor_multiple() - single event",
+	    0, { 8 });
+
+static int error_event_cb(int evtype TEST_UNUSED,
+			  unsigned int offset TEST_UNUSED,
+			  const struct timespec *ts TEST_UNUSED,
+			  void *data TEST_UNUSED)
+{
+	errno = ENOTBLK;
+
+	return GPIOD_CTXLESS_EVENT_CB_RET_ERR;
+}
+
+static void ctxless_event_monitor_indicate_error(void)
+{
+	struct timespec ts = { 1, 0 };
+	int rv;
+
+	test_set_event(0, 3, TEST_EVENT_ALTERNATING, 100);
+
+	rv = gpiod_ctxless_event_monitor(test_chip_name(0),
+					 GPIOD_CTXLESS_EVENT_BOTH_EDGES,
+					 3, false, TEST_CONSUMER, &ts,
+					 NULL, error_event_cb, NULL);
+
+	TEST_ASSERT_EQ(rv, -1);
+	TEST_ASSERT_ERRNO_IS(ENOTBLK);
+}
+TEST_DEFINE(ctxless_event_monitor_indicate_error,
+	    "gpiod_ctxless_event_monitor() - error in callback",
+	    0, { 8 });
+
+static void ctxless_event_monitor_indicate_error_timeout(void)
+{
+	struct timespec ts = { 0, 100000 };
+	int rv;
+
+	rv = gpiod_ctxless_event_monitor(test_chip_name(0),
+					 GPIOD_CTXLESS_EVENT_BOTH_EDGES,
+					 3, false, TEST_CONSUMER, &ts,
+					 NULL, error_event_cb, NULL);
+
+	TEST_ASSERT_EQ(rv, -1);
+	TEST_ASSERT_ERRNO_IS(ENOTBLK);
+}
+TEST_DEFINE(ctxless_event_monitor_indicate_error_timeout,
+	    "gpiod_ctxless_event_monitor() - error in callback after timeout",
+	    0, { 8 });
+
+static void ctxless_find_line_good(void)
+{
+	unsigned int offset;
+	char chip[32];
+	int rv;
+
+	rv = gpiod_ctxless_find_line("gpio-mockup-C-14", chip,
+				     sizeof(chip), &offset);
+	TEST_ASSERT_EQ(rv, 1);
+	TEST_ASSERT_EQ(offset, 14);
+	TEST_ASSERT_STR_EQ(chip, test_chip_name(2));
+}
+TEST_DEFINE(ctxless_find_line_good,
+	    "gpiod_ctxless_find_line() - good",
+	    TEST_FLAG_NAMED_LINES, { 8, 16, 16, 8 });
+
+static void ctxless_find_line_truncated(void)
+{
+	unsigned int offset;
+	char chip[6];
+	int rv;
+
+	rv = gpiod_ctxless_find_line("gpio-mockup-C-14", chip,
+				     sizeof(chip), &offset);
+	TEST_ASSERT_EQ(rv, 1);
+	TEST_ASSERT_EQ(offset, 14);
+	TEST_ASSERT_STR_EQ(chip, "gpioc");
+}
+TEST_DEFINE(ctxless_find_line_truncated,
+	    "gpiod_ctxless_find_line() - chip name truncated",
+	    TEST_FLAG_NAMED_LINES, { 8, 16, 16, 8 });
+
+static void ctxless_find_line_not_found(void)
+{
+	unsigned int offset;
+	char chip[32];
+	int rv;
+
+	rv = gpiod_ctxless_find_line("nonexistent", chip,
+				     sizeof(chip), &offset);
+	TEST_ASSERT_EQ(rv, 0);
+}
+TEST_DEFINE(ctxless_find_line_not_found,
+	    "gpiod_ctxless_find_line() - not found",
+	    TEST_FLAG_NAMED_LINES, { 8, 16, 16, 8 });
diff --git a/tests/tests-event.c b/tests/tests-event.c
new file mode 100644
index 0000000..93e53a4
--- /dev/null
+++ b/tests/tests-event.c
@@ -0,0 +1,335 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Test cases for GPIO line events. */
+
+#include <errno.h>
+#include <unistd.h>
+
+#include "gpiod-test.h"
+
+static void event_rising_edge_good(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct timespec ts = { 1, 0 };
+	struct gpiod_line_event ev;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 7);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_rising_edge_events(line, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	test_set_event(0, 7, TEST_EVENT_RISING, 100);
+
+	rv = gpiod_line_event_wait(line, &ts);
+	TEST_ASSERT_EQ(rv, 1);
+
+	rv = gpiod_line_event_read(line, &ev);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(ev.event_type, GPIOD_LINE_EVENT_RISING_EDGE);
+}
+TEST_DEFINE(event_rising_edge_good,
+	    "events - receive single rising edge event",
+	    0, { 8 });
+
+static void event_falling_edge_good(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct timespec ts = { 1, 0 };
+	struct gpiod_line_event ev;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 7);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_falling_edge_events(line, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	test_set_event(0, 7, TEST_EVENT_FALLING, 100);
+
+	rv = gpiod_line_event_wait(line, &ts);
+	TEST_ASSERT_EQ(rv, 1);
+
+	rv = gpiod_line_event_read(line, &ev);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(ev.event_type, GPIOD_LINE_EVENT_FALLING_EDGE);
+}
+TEST_DEFINE(event_falling_edge_good,
+	    "events - receive single falling edge event",
+	    0, { 8 });
+
+static void event_rising_edge_ignore_falling(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct timespec ts = { 0, 300 };
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 7);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_rising_edge_events(line, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	test_set_event(0, 7, TEST_EVENT_FALLING, 100);
+
+	rv = gpiod_line_event_wait(line, &ts);
+	TEST_ASSERT_EQ(rv, 0);
+}
+TEST_DEFINE(event_rising_edge_ignore_falling,
+	    "events - request rising edge & ignore falling edge events",
+	    0, { 8 });
+
+static void event_rising_edge_active_low(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct timespec ts = { 1, 0 };
+	struct gpiod_line_event ev;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 7);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_rising_edge_events_flags(line, TEST_CONSUMER,
+					GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW);
+	TEST_ASSERT_RET_OK(rv);
+
+	test_set_event(0, 7, TEST_EVENT_RISING, 100);
+
+	rv = gpiod_line_event_wait(line, &ts);
+	TEST_ASSERT_EQ(rv, 1);
+
+	rv = gpiod_line_event_read(line, &ev);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(ev.event_type, GPIOD_LINE_EVENT_RISING_EDGE);
+}
+TEST_DEFINE(event_rising_edge_active_low,
+	    "events - single rising edge event with low active state",
+	    0, { 8 });
+
+static void event_get_value(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct timespec ts = { 1, 0 };
+	struct gpiod_line_event ev;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 7);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_rising_edge_events(line, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	rv = gpiod_line_get_value(line);
+	TEST_ASSERT_EQ(rv, 0);
+
+	test_set_event(0, 7, TEST_EVENT_RISING, 100);
+
+	rv = gpiod_line_event_wait(line, &ts);
+	TEST_ASSERT_EQ(rv, 1);
+
+	rv = gpiod_line_event_read(line, &ev);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(ev.event_type, GPIOD_LINE_EVENT_RISING_EDGE);
+
+	rv = gpiod_line_get_value(line);
+	TEST_ASSERT_EQ(rv, 1);
+}
+TEST_DEFINE(event_get_value,
+	    "events - mixing events and gpiod_line_get_value()",
+	    0, { 8 });
+
+static void event_get_value_active_low(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct timespec ts = { 1, 0 };
+	struct gpiod_line_event ev;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 7);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_falling_edge_events_flags(line, TEST_CONSUMER,
+					GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW);
+	TEST_ASSERT_RET_OK(rv);
+
+	rv = gpiod_line_get_value(line);
+	TEST_ASSERT_EQ(rv, 1);
+
+	test_set_event(0, 7, TEST_EVENT_FALLING, 100);
+
+	rv = gpiod_line_event_wait(line, &ts);
+	TEST_ASSERT_EQ(rv, 1);
+
+	rv = gpiod_line_event_read(line, &ev);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(ev.event_type, GPIOD_LINE_EVENT_FALLING_EDGE);
+
+	rv = gpiod_line_get_value(line);
+	TEST_ASSERT_EQ(rv, 0);
+}
+TEST_DEFINE(event_get_value_active_low,
+	    "events - mixing events and gpiod_line_get_value() (active-low flag)",
+	    0, { 8 });
+
+static void event_wait_multiple(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line_bulk bulk, event_bulk;
+	struct timespec ts = { 1, 0 };
+	struct gpiod_line *line;
+	int rv, i;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	gpiod_line_bulk_init(&bulk);
+
+	for (i = 0; i < 8; i++) {
+		line = gpiod_chip_get_line(chip, i);
+		TEST_ASSERT_NOT_NULL(line);
+
+		gpiod_line_bulk_add(&bulk, line);
+	}
+
+	rv = gpiod_line_request_bulk_both_edges_events(&bulk, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	test_set_event(0, 4, TEST_EVENT_RISING, 100);
+
+	rv = gpiod_line_event_wait_bulk(&bulk, &ts, &event_bulk);
+	TEST_ASSERT_EQ(rv, 1);
+
+	TEST_ASSERT_EQ(gpiod_line_bulk_num_lines(&event_bulk), 1);
+	line = gpiod_line_bulk_get_line(&event_bulk, 0);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 4);
+}
+TEST_DEFINE(event_wait_multiple,
+	    "events - wait for events on multiple lines",
+	    0, { 8 });
+
+static void event_get_fd_when_values_requested(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line;
+	int rv, fd;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 3);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_input(line, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	fd = gpiod_line_event_get_fd(line);
+	TEST_ASSERT_EQ(fd, -1);
+	TEST_ASSERT_ERRNO_IS(EPERM);
+}
+TEST_DEFINE(event_get_fd_when_values_requested,
+	    "events - gpiod_line_event_get_fd(): line requested for values",
+	    0, { 8 });
+
+static void event_request_bulk_fail(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line_bulk bulk = GPIOD_LINE_BULK_INITIALIZER;
+	struct gpiod_line *line;
+	int rv, i;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 5);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_input(line, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	for (i = 0; i < 8; i++) {
+		line = gpiod_chip_get_line(chip, i);
+		TEST_ASSERT_NOT_NULL(line);
+		gpiod_line_bulk_add(&bulk, line);
+	}
+
+	rv = gpiod_line_request_bulk_both_edges_events(&bulk, TEST_CONSUMER);
+	TEST_ASSERT_EQ(rv, -1);
+	TEST_ASSERT_ERRNO_IS(EBUSY);
+}
+TEST_DEFINE(event_request_bulk_fail,
+	    "events - failed bulk request (test reversed release)",
+	    0, { 8 });
+
+static void event_invalid_fd(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line_bulk bulk = GPIOD_LINE_BULK_INITIALIZER;
+	struct gpiod_line_bulk ev_bulk;
+	struct timespec ts = { 1, 0 };
+	struct gpiod_line *line;
+	int rv, fd;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 5);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_both_edges_events(line, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	fd = gpiod_line_event_get_fd(line);
+	close(fd);
+
+	rv = gpiod_line_event_wait(line, &ts);
+	TEST_ASSERT_EQ(rv, -1);
+	TEST_ASSERT_ERRNO_IS(EINVAL);
+
+	/*
+	 * The single line variant calls gpiod_line_event_wait_bulk() with the
+	 * event_bulk argument set to NULL, so test this use case explicitly
+	 * as well.
+	 */
+	gpiod_line_bulk_add(&bulk, line);
+	rv = gpiod_line_event_wait_bulk(&bulk, &ts, &ev_bulk);
+	TEST_ASSERT_EQ(rv, -1);
+	TEST_ASSERT_ERRNO_IS(EINVAL);
+}
+TEST_DEFINE(event_invalid_fd,
+	    "events - gpiod_line_event_wait() error on closed fd",
+	    0, { 8 });
diff --git a/tests/tests-gpiodetect.c b/tests/tests-gpiodetect.c
new file mode 100644
index 0000000..bd5eeb1
--- /dev/null
+++ b/tests/tests-gpiodetect.c
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Test cases for the gpiodetect program. */
+
+#include <stdio.h>
+
+#include "gpiod-test.h"
+
+static void gpiodetect_simple(void)
+{
+	test_tool_run("gpiodetect", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stdout(),
+				 test_build_str("%s [gpio-mockup-A] (4 lines)",
+						test_chip_name(0)));
+	TEST_ASSERT_STR_CONTAINS(test_tool_stdout(),
+				 test_build_str("%s [gpio-mockup-B] (8 lines)",
+						test_chip_name(1)));
+	TEST_ASSERT_STR_CONTAINS(test_tool_stdout(),
+				 test_build_str("%s [gpio-mockup-C] (16 lines)",
+						test_chip_name(2)));
+	TEST_ASSERT_NULL(test_tool_stderr());
+}
+TEST_DEFINE(gpiodetect_simple,
+	    "tools: gpiodetect - simple",
+	    0, { 4, 8, 16 });
+
+static void gpiodetect_invalid_args(void)
+{
+	test_tool_run("gpiodetect", "unused argument", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(), "unrecognized argument");
+}
+TEST_DEFINE(gpiodetect_invalid_args,
+	    "tools: gpiodetect - invalid arguments",
+	    0, { });
diff --git a/tests/tests-gpiofind.c b/tests/tests-gpiofind.c
new file mode 100644
index 0000000..6d8e12a
--- /dev/null
+++ b/tests/tests-gpiofind.c
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Test cases for the gpiofind program. */
+
+#include <stdio.h>
+
+#include "gpiod-test.h"
+
+static void gpiofind_found(void)
+{
+	test_tool_run("gpiofind", "gpio-mockup-B-7", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(),
+			   test_build_str("%s 7\n", test_chip_name(1)));
+	TEST_ASSERT_NULL(test_tool_stderr());
+}
+TEST_DEFINE(gpiofind_found,
+	    "tools: gpiofind - found",
+	    TEST_FLAG_NAMED_LINES, { 4, 8 });
+
+static void gpiofind_not_found(void)
+{
+	test_tool_run("gpiofind", "nonexistent", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+}
+TEST_DEFINE(gpiofind_not_found,
+	    "tools: gpiofind - not found",
+	    TEST_FLAG_NAMED_LINES, { 4, 8 });
+
+static void gpiofind_invalid_args(void)
+{
+	test_tool_run("gpiofind", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "exactly one GPIO line name must be specified");
+
+	test_tool_run("gpiofind", "first argument",
+			  "second argument", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "exactly one GPIO line name must be specified");
+}
+TEST_DEFINE(gpiofind_invalid_args,
+	    "tools: gpiofind - invalid arguments",
+	    0, { });
diff --git a/tests/tests-gpioget.c b/tests/tests-gpioget.c
new file mode 100644
index 0000000..2c9bcb1
--- /dev/null
+++ b/tests/tests-gpioget.c
@@ -0,0 +1,181 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Test cases for the gpioget program. */
+
+#include "gpiod-test.h"
+
+static void gpioget_read_all_lines(void)
+{
+	unsigned int offsets[4];
+	int rv, values[4];
+
+	test_tool_run("gpioget", test_chip_name(1),
+		      "0", "1", "2", "3", "4", "5", "6", "7", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "0 0 0 0 0 0 0 0\n");
+
+	offsets[0] = 2;
+	offsets[1] = 3;
+	offsets[2] = 5;
+	offsets[3] = 7;
+
+	values[0] = values[1] = values[2] = values[3] = 1;
+
+	rv = gpiod_ctxless_set_value_multiple(test_chip_name(1), offsets,
+					      values, 4, false, TEST_CONSUMER,
+					      NULL, NULL);
+	TEST_ASSERT_RET_OK(rv);
+
+	test_tool_run("gpioget", test_chip_name(1),
+		      "0", "1", "2", "3", "4", "5", "6", "7", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "0 0 1 1 0 1 0 1\n");
+}
+TEST_DEFINE(gpioget_read_all_lines,
+	    "tools: gpioget - read all lines",
+	    0, { 8, 8, 8 });
+
+static void gpioget_read_all_lines_active_low(void)
+{
+	unsigned int offsets[4];
+	int rv, values[4];
+
+	test_tool_run("gpioget", "--active-low", test_chip_name(1),
+		      "0", "1", "2", "3", "4", "5", "6", "7", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "1 1 1 1 1 1 1 1\n");
+
+	offsets[0] = 2;
+	offsets[1] = 3;
+	offsets[2] = 5;
+	offsets[3] = 7;
+
+	values[0] = values[1] = values[2] = values[3] = 1;
+
+	rv = gpiod_ctxless_set_value_multiple(test_chip_name(1), offsets,
+					      values, 4, false, TEST_CONSUMER,
+					      NULL, NULL);
+	TEST_ASSERT_RET_OK(rv);
+
+	test_tool_run("gpioget", "--active-low", test_chip_name(1),
+		      "0", "1", "2", "3", "4", "5", "6", "7", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "1 1 0 0 1 0 1 0\n");
+}
+TEST_DEFINE(gpioget_read_all_lines_active_low,
+	    "tools: gpioget - read all lines (active-low)",
+	    0, { 8, 8, 8 });
+
+static void gpioget_read_some_lines(void)
+{
+	unsigned int offsets[3];
+	int rv, values[3];
+
+	test_tool_run("gpioget", test_chip_name(1),
+		      "0", "1", "4", "6", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "0 0 0 0\n");
+
+	offsets[0] = 1;
+	offsets[1] = 4;
+	offsets[2] = 6;
+
+	values[0] = values[1] = values[2] = 1;
+
+	rv = gpiod_ctxless_set_value_multiple(test_chip_name(1), offsets,
+					      values, 3, false, TEST_CONSUMER,
+					      NULL, NULL);
+	TEST_ASSERT_RET_OK(rv);
+
+	test_tool_run("gpioget", test_chip_name(1),
+			      "0", "1", "4", "6", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "0 1 1 1\n");
+}
+TEST_DEFINE(gpioget_read_some_lines,
+	    "tools: gpioget - read some lines",
+	    0, { 8, 8, 8 });
+
+static void gpioget_no_arguments(void)
+{
+	test_tool_run("gpioget", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "gpiochip must be specified");
+}
+TEST_DEFINE(gpioget_no_arguments,
+	    "tools: gpioget - no arguments",
+	    0, { });
+
+static void gpioget_no_lines_specified(void)
+{
+	test_tool_run("gpioget", test_chip_name(1), (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "at least one GPIO line offset must be specified");
+}
+TEST_DEFINE(gpioget_no_lines_specified,
+	    "tools: gpioget - no lines specified",
+	    0, { 4, 4 });
+
+static void gpioget_too_many_lines_specified(void)
+{
+	test_tool_run("gpioget", test_chip_name(0),
+		      "0", "1", "2", "3", "4", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "error reading GPIO values");
+}
+TEST_DEFINE(gpioget_too_many_lines_specified,
+	    "tools: gpioget - too many lines specified",
+	    0, { 4 });
diff --git a/tests/tests-gpioinfo.c b/tests/tests-gpioinfo.c
new file mode 100644
index 0000000..162896f
--- /dev/null
+++ b/tests/tests-gpioinfo.c
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Test cases for the gpioinfo program. */
+
+#include <stdio.h>
+
+#include "gpiod-test.h"
+
+static void gpioinfo_dump_all_chips(void)
+{
+	test_tool_run("gpioinfo", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stdout(),
+				 test_build_str("%s - 4 lines:",
+						test_chip_name(0)));
+	TEST_ASSERT_STR_CONTAINS(test_tool_stdout(),
+				 test_build_str("%s - 8 lines:",
+						test_chip_name(1)));
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(),
+				"\\s+line\\s+0:\\s+unnamed\\s+unused\\s+input\\s+active-high");
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(),
+				"\\s+line\\s+7:\\s+unnamed\\s+unused\\s+input\\s+active-high");
+}
+TEST_DEFINE(gpioinfo_dump_all_chips,
+	    "tools: gpioinfo - dump all chips",
+	    0, { 4, 8 });
+
+static void gpioinfo_dump_all_chips_one_exported(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(1));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 7);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_input_flags(line, TEST_CONSUMER,
+					    GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW);
+	TEST_ASSERT_RET_OK(rv);
+
+	test_tool_run("gpioinfo", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stdout(),
+				 test_build_str("%s - 4 lines:",
+						test_chip_name(0)));
+	TEST_ASSERT_STR_CONTAINS(test_tool_stdout(),
+				 test_build_str("%s - 8 lines:",
+						test_chip_name(1)));
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(),
+				"\\s+line\\s+0:\\s+unnamed\\s+unused\\s+input\\s+active-high");
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(),
+				"\\s+line\\s+7:\\s+unnamed\\s+\\\"" TEST_CONSUMER "\\\"\\s+input\\s+active-low");
+}
+TEST_DEFINE(gpioinfo_dump_all_chips_one_exported,
+	    "tools: gpioinfo - dump all chips (one line exported)",
+	    0, { 4, 8 });
+
+static void gpioinfo_dump_one_chip(void)
+{
+	test_tool_run("gpioinfo", test_chip_name(1), (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_STR_NOTCONT(test_tool_stdout(),
+				test_build_str("%s - 8 lines:",
+					       test_chip_name(0)));
+	TEST_ASSERT_STR_CONTAINS(test_tool_stdout(),
+				 test_build_str("%s - 4 lines:",
+						test_chip_name(1)));
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(),
+				"\\s+line\\s+0:\\s+unnamed\\s+unused\\s+input\\s+active-high");
+	TEST_ASSERT_REGEX_NOMATCH(test_tool_stdout(),
+				  "\\s+line\\s+7:\\s+unnamed\\s+unused\\s+input\\s+active-high");
+}
+TEST_DEFINE(gpioinfo_dump_one_chip,
+	    "tools: gpioinfo - dump one chip",
+	    0, { 8, 4 });
+
+static void gpioinfo_dump_all_but_one_chip(void)
+{
+	test_tool_run("gpioinfo", test_chip_name(0),
+		      test_chip_name(1), test_chip_name(3), (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_STR_NOTCONT(test_tool_stdout(),
+				test_build_str("%s - 8 lines:",
+					       test_chip_name(2)));
+	TEST_ASSERT_STR_CONTAINS(test_tool_stdout(),
+				 test_build_str("%s - 4 lines:",
+						test_chip_name(0)));
+	TEST_ASSERT_STR_CONTAINS(test_tool_stdout(),
+				 test_build_str("%s - 4 lines:",
+						test_chip_name(1)));
+	TEST_ASSERT_STR_CONTAINS(test_tool_stdout(),
+				 test_build_str("%s - 4 lines:",
+						test_chip_name(3)));
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(),
+				"\\s+line\\s+0:\\s+unnamed\\s+unused\\s+input\\s+active-high");
+	TEST_ASSERT_REGEX_NOMATCH(test_tool_stdout(),
+				  "\\s+line\\s+7:\\s+unnamed\\s+unused\\s+input\\s+active-high");
+}
+TEST_DEFINE(gpioinfo_dump_all_but_one_chip,
+	    "tools: gpioinfo - dump all but one chip",
+	    0, { 4, 4, 8, 4 });
+
+static void gpioinfo_inexistent_chip(void)
+{
+	test_tool_run("gpioinfo", "inexistent", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "looking up chip inexistent");
+}
+TEST_DEFINE(gpioinfo_inexistent_chip,
+	    "tools: gpioinfo - inexistent chip",
+	    0, { 8, 4 });
diff --git a/tests/tests-gpiomon.c b/tests/tests-gpiomon.c
new file mode 100644
index 0000000..fd21391
--- /dev/null
+++ b/tests/tests-gpiomon.c
@@ -0,0 +1,375 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Test cases for the gpiomon program. */
+
+#include <signal.h>
+#include <unistd.h>
+
+#include "gpiod-test.h"
+
+static void gpiomon_single_rising_edge_event(void)
+{
+	test_tool_run("gpiomon", "--rising-edge", "--num-events=1",
+		      test_chip_name(1), "4", (char *)NULL);
+	test_set_event(1, 4, TEST_EVENT_RISING, 200);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(),
+				"event\\:\\s+RISING\\s+EDGE\\s+offset\\:\\s+4\\s+timestamp:\\s+\\[[0-9]+\\.[0-9]+\\]");
+}
+TEST_DEFINE(gpiomon_single_rising_edge_event,
+	    "tools: gpiomon - single rising edge event",
+	    0, { 8, 8 });
+
+static void gpiomon_single_rising_edge_event_active_low(void)
+{
+	test_tool_run("gpiomon", "--rising-edge", "--num-events=1",
+		      "--active-low", test_chip_name(1), "4", (char *)NULL);
+	test_set_event(1, 4, TEST_EVENT_RISING, 200);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(),
+				"event\\:\\s+RISING\\s+EDGE\\s+offset\\:\\s+4\\s+timestamp:\\s+\\[[0-9]+\\.[0-9]+\\]");
+}
+TEST_DEFINE(gpiomon_single_rising_edge_event_active_low,
+	    "tools: gpiomon - single rising edge event (active-low)",
+	    0, { 8, 8 });
+
+static void gpiomon_single_rising_edge_event_silent(void)
+{
+	test_tool_run("gpiomon", "--rising-edge", "--num-events=1",
+		      "--silent", test_chip_name(1), "4", (char *)NULL);
+	test_set_event(1, 4, TEST_EVENT_RISING, 200);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+}
+TEST_DEFINE(gpiomon_single_rising_edge_event_silent,
+	    "tools: gpiomon - single rising edge event (silent mode)",
+	    0, { 8, 8 });
+
+static void gpiomon_four_alternating_events(void)
+{
+	test_tool_run("gpiomon", "--num-events=4",
+		      test_chip_name(1), "4", (char *)NULL);
+	test_set_event(1, 4, TEST_EVENT_ALTERNATING, 100);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(),
+				"event\\:\\s+FALLING\\s+EDGE\\s+offset\\:\\s+4\\s+timestamp:\\s+\\[[0-9]+\\.[0-9]+\\]");
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(),
+				"event\\:\\s+RISING\\s+EDGE\\s+offset\\:\\s+4\\s+timestamp:\\s+\\[[0-9]+\\.[0-9]+\\]");
+}
+TEST_DEFINE(gpiomon_four_alternating_events,
+	    "tools: gpiomon - four alternating events",
+	    0, { 8, 8 });
+
+static void gpiomon_falling_edge_events_sigint(void)
+{
+	test_tool_run("gpiomon", "--falling-edge",
+		      test_chip_name(0), "4", (char *)NULL);
+	test_set_event(0, 4, TEST_EVENT_FALLING, 100);
+	usleep(200000);
+	test_tool_signal(SIGINT);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(),
+				"event\\:\\s+FALLING\\s+EDGE\\s+offset\\:\\s+4\\s+timestamp:\\s+\\[[0-9]+\\.[0-9]+\\]");
+}
+TEST_DEFINE(gpiomon_falling_edge_events_sigint,
+	    "tools: gpiomon - receive falling edge events and kill with SIGINT",
+	    0, { 8, 8 });
+
+static void gpiomon_both_events_sigterm(void)
+{
+	test_tool_run("gpiomon", "--falling-edge", "--rising-edge",
+		      test_chip_name(0), "4", (char *)NULL);
+	test_set_event(0, 4, TEST_EVENT_ALTERNATING, 100);
+	usleep(300000);
+	test_tool_signal(SIGTERM);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(),
+				"event\\:\\s+FALLING\\s+EDGE\\s+offset\\:\\s+4\\s+timestamp:\\s+\\[[0-9]+\\.[0-9]+\\]");
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(),
+				"event\\:\\s+RISING\\s+EDGE\\s+offset\\:\\s+4\\s+timestamp:\\s+\\[[0-9]+\\.[0-9]+\\]");
+}
+TEST_DEFINE(gpiomon_both_events_sigterm,
+	    "tools: gpiomon - receive both types of events and kill with SIGTERM",
+	    0, { 8, 8 });
+
+static void gpiomon_watch_multiple_lines(void)
+{
+	test_tool_run("gpiomon", "--format=%o", test_chip_name(0),
+		      "1", "2", "3", "4", "5", (char *)NULL);
+	test_set_event(0, 2, TEST_EVENT_ALTERNATING, 100);
+	usleep(150000);
+	test_set_event(0, 3, TEST_EVENT_ALTERNATING, 100);
+	usleep(150000);
+	test_set_event(0, 4, TEST_EVENT_ALTERNATING, 100);
+	usleep(150000);
+	test_tool_signal(SIGTERM);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "2\n3\n4\n");
+
+}
+TEST_DEFINE(gpiomon_watch_multiple_lines,
+	    "tools: gpiomon - watch multiple lines",
+	    0, { 8, 8 });
+
+static void gpiomon_watch_multiple_lines_not_in_order(void)
+{
+	test_tool_run("gpiomon", "--format=%o", test_chip_name(0),
+		      "5", "2", "7", "1", "6", (char *)NULL);
+	test_set_event(0, 2, TEST_EVENT_ALTERNATING, 100);
+	usleep(150000);
+	test_set_event(0, 1, TEST_EVENT_ALTERNATING, 100);
+	usleep(150000);
+	test_set_event(0, 6, TEST_EVENT_ALTERNATING, 100);
+	usleep(150000);
+	test_tool_signal(SIGTERM);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "2\n1\n6\n");
+
+}
+TEST_DEFINE(gpiomon_watch_multiple_lines_not_in_order,
+	    "tools: gpiomon - watch multiple lines (offsets not in order)",
+	    0, { 8, 8 });
+
+static void gpiomon_request_the_same_line_twice(void)
+{
+	test_tool_run("gpiomon", test_chip_name(0), "2", "2", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "error waiting for events");
+}
+TEST_DEFINE(gpiomon_request_the_same_line_twice,
+	    "tools: gpiomon - request the same line twice",
+	    0, { 8, 8 });
+
+static void gpiomon_no_arguments(void)
+{
+	test_tool_run("gpiomon", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "gpiochip must be specified");
+}
+TEST_DEFINE(gpiomon_no_arguments,
+	    "tools: gpiomon - no arguments",
+	    0, { });
+
+static void gpiomon_line_not_specified(void)
+{
+	test_tool_run("gpiomon", test_chip_name(1), (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "GPIO line offset must be specified");
+}
+TEST_DEFINE(gpiomon_line_not_specified,
+	    "tools: gpiomon - line not specified",
+	    0, { 4, 4 });
+
+static void gpiomon_line_out_of_range(void)
+{
+	test_tool_run("gpiomon", test_chip_name(0), "4", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "error waiting for events");
+}
+TEST_DEFINE(gpiomon_line_out_of_range,
+	    "tools: gpiomon - line out of range",
+	    0, { 4 });
+
+static void gpiomon_custom_format_event_and_offset(void)
+{
+	test_tool_run("gpiomon", "--num-events=1", "--format=%e %o",
+		      test_chip_name(0), "3", (char *)NULL);
+	test_set_event(0, 3, TEST_EVENT_RISING, 100);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "1 3\n");
+}
+TEST_DEFINE(gpiomon_custom_format_event_and_offset,
+	    "tools: gpiomon - custom output format: event and offset",
+	    0, { 8, 8 });
+
+static void gpiomon_custom_format_event_and_offset_joined(void)
+{
+	test_tool_run("gpiomon", "--num-events=1", "--format=%e%o",
+		      test_chip_name(0), "3", (char *)NULL);
+	test_set_event(0, 3, TEST_EVENT_RISING, 100);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "13\n");
+}
+TEST_DEFINE(gpiomon_custom_format_event_and_offset_joined,
+	    "tools: gpiomon - custom output format: event and offset, joined strings",
+	    0, { 8, 8 });
+
+static void gpiomon_custom_format_timestamp(void)
+{
+	test_tool_run("gpiomon", "--num-events=1", "--format=%e %o %s.%n",
+		      test_chip_name(0), "3", (char *)NULL);
+	test_set_event(0, 3, TEST_EVENT_RISING, 100);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_REGEX_MATCH(test_tool_stdout(), "1 3 [0-9]+\\.[0-9]+");
+}
+TEST_DEFINE(gpiomon_custom_format_timestamp,
+	    "tools: gpiomon - custom output format: timestamp",
+	    0, { 8, 8 });
+
+static void gpiomon_custom_format_double_percent_sign(void)
+{
+	test_tool_run("gpiomon", "--num-events=1", "--format=%%",
+		      test_chip_name(0), "3", (char *)NULL);
+	test_set_event(0, 3, TEST_EVENT_RISING, 100);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "%\n");
+}
+TEST_DEFINE(gpiomon_custom_format_double_percent_sign,
+	    "tools: gpiomon - custom output format: double percent sign",
+	    0, { 8, 8 });
+
+static void gpiomon_custom_format_double_percent_sign_and_spec(void)
+{
+	test_tool_run("gpiomon", "--num-events=1", "--format=%%e",
+		      test_chip_name(0), "3", (char *)NULL);
+	test_set_event(0, 3, TEST_EVENT_RISING, 100);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "%e\n");
+}
+TEST_DEFINE(gpiomon_custom_format_double_percent_sign_and_spec,
+	    "tools: gpiomon - custom output format: double percent sign with specifier",
+	    0, { 8, 8 });
+
+static void gpiomon_custom_format_single_percent_sign(void)
+{
+	test_tool_run("gpiomon", "--num-events=1", "--format=%",
+		      test_chip_name(0), "3", (char *)NULL);
+	test_set_event(0, 3, TEST_EVENT_RISING, 100);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "%\n");
+}
+TEST_DEFINE(gpiomon_custom_format_single_percent_sign,
+	    "tools: gpiomon - custom output format: single percent sign",
+	    0, { 8, 8 });
+
+static void gpiomon_custom_format_single_percent_sign_between_chars(void)
+{
+	test_tool_run("gpiomon", "--num-events=1", "--format=foo % bar",
+		      test_chip_name(0), "3", (char *)NULL);
+	test_set_event(0, 3, TEST_EVENT_RISING, 100);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "foo % bar\n");
+}
+TEST_DEFINE(gpiomon_custom_format_single_percent_sign_between_chars,
+	    "tools: gpiomon - custom output format: single percent sign between other characters",
+	    0, { 8, 8 });
+
+static void gpiomon_custom_format_unknown_specifier(void)
+{
+	test_tool_run("gpiomon", "--num-events=1", "--format=%x",
+		      test_chip_name(0), "3", (char *)NULL);
+	test_set_event(0, 3, TEST_EVENT_RISING, 100);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NOT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_EQ(test_tool_stdout(), "%x\n");
+}
+TEST_DEFINE(gpiomon_custom_format_unknown_specifier,
+	    "tools: gpiomon - custom output format: unknown specifier",
+	    0, { 8, 8 });
diff --git a/tests/tests-gpioset.c b/tests/tests-gpioset.c
new file mode 100644
index 0000000..ab7b514
--- /dev/null
+++ b/tests/tests-gpioset.c
@@ -0,0 +1,385 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Test cases for the gpioset program. */
+
+#include <signal.h>
+#include <unistd.h>
+
+#include "gpiod-test.h"
+
+static void gpioset_set_lines_and_exit(void)
+{
+	unsigned int offsets[8];
+	int rv, values[8];
+
+	test_tool_run("gpioset", test_chip_name(2),
+		      "0=0", "1=0", "2=1", "3=1",
+		      "4=1", "5=1", "6=0", "7=1", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+
+	offsets[0] = 0;
+	offsets[1] = 1;
+	offsets[2] = 2;
+	offsets[3] = 3;
+	offsets[4] = 4;
+	offsets[5] = 5;
+	offsets[6] = 6;
+	offsets[7] = 7;
+
+	rv = gpiod_ctxless_get_value_multiple(test_chip_name(2), offsets,
+					      values, 8, false, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(values[0], 0);
+	TEST_ASSERT_EQ(values[1], 0);
+	TEST_ASSERT_EQ(values[2], 1);
+	TEST_ASSERT_EQ(values[3], 1);
+	TEST_ASSERT_EQ(values[4], 1);
+	TEST_ASSERT_EQ(values[5], 1);
+	TEST_ASSERT_EQ(values[6], 0);
+	TEST_ASSERT_EQ(values[7], 1);
+}
+TEST_DEFINE(gpioset_set_lines_and_exit,
+	    "tools: gpioset - set lines and exit",
+	    0, { 8, 8, 8 });
+
+static void gpioset_set_lines_and_exit_active_low(void)
+{
+	unsigned int offsets[8];
+	int rv, values[8];
+
+	test_tool_run("gpioset", "--active-low", test_chip_name(2),
+		      "0=0", "1=0", "2=1", "3=1",
+		      "4=1", "5=1", "6=0", "7=1", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+
+	offsets[0] = 0;
+	offsets[1] = 1;
+	offsets[2] = 2;
+	offsets[3] = 3;
+	offsets[4] = 4;
+	offsets[5] = 5;
+	offsets[6] = 6;
+	offsets[7] = 7;
+
+	rv = gpiod_ctxless_get_value_multiple(test_chip_name(2), offsets,
+					      values, 8, false, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(values[0], 1);
+	TEST_ASSERT_EQ(values[1], 1);
+	TEST_ASSERT_EQ(values[2], 0);
+	TEST_ASSERT_EQ(values[3], 0);
+	TEST_ASSERT_EQ(values[4], 0);
+	TEST_ASSERT_EQ(values[5], 0);
+	TEST_ASSERT_EQ(values[6], 1);
+	TEST_ASSERT_EQ(values[7], 0);
+}
+TEST_DEFINE(gpioset_set_lines_and_exit_active_low,
+	    "tools: gpioset - set lines and exit (active-low)",
+	    0, { 8, 8, 8 });
+
+static void gpioset_set_lines_and_exit_explicit_mode(void)
+{
+	unsigned int offsets[8];
+	int rv, values[8];
+
+	test_tool_run("gpioset", "--mode=exit", test_chip_name(2),
+		      "0=0", "1=0", "2=1", "3=1",
+		      "4=1", "5=1", "6=0", "7=1", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+
+	offsets[0] = 0;
+	offsets[1] = 1;
+	offsets[2] = 2;
+	offsets[3] = 3;
+	offsets[4] = 4;
+	offsets[5] = 5;
+	offsets[6] = 6;
+	offsets[7] = 7;
+
+	rv = gpiod_ctxless_get_value_multiple(test_chip_name(2), offsets,
+					      values, 8, false, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(values[0], 0);
+	TEST_ASSERT_EQ(values[1], 0);
+	TEST_ASSERT_EQ(values[2], 1);
+	TEST_ASSERT_EQ(values[3], 1);
+	TEST_ASSERT_EQ(values[4], 1);
+	TEST_ASSERT_EQ(values[5], 1);
+	TEST_ASSERT_EQ(values[6], 0);
+	TEST_ASSERT_EQ(values[7], 1);
+}
+TEST_DEFINE(gpioset_set_lines_and_exit_explicit_mode,
+	    "tools: gpioset - set lines and exit (explicit mode argument)",
+	    0, { 8, 8, 8 });
+
+static void gpioset_set_some_lines_and_wait_for_enter(void)
+{
+	unsigned int offsets[5];
+	int rv, values[5];
+
+	test_tool_run("gpioset", "--mode=wait", test_chip_name(2),
+		      "1=0", "2=1", "5=1", "6=0", "7=1", (char *)NULL);
+	test_tool_stdin_write("\n");
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+
+	offsets[0] = 1;
+	offsets[1] = 2;
+	offsets[2] = 5;
+	offsets[3] = 6;
+	offsets[4] = 7;
+
+	rv = gpiod_ctxless_get_value_multiple(test_chip_name(2), offsets,
+					      values, 5, false, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(values[0], 0);
+	TEST_ASSERT_EQ(values[1], 1);
+	TEST_ASSERT_EQ(values[2], 1);
+	TEST_ASSERT_EQ(values[3], 0);
+	TEST_ASSERT_EQ(values[4], 1);
+}
+TEST_DEFINE(gpioset_set_some_lines_and_wait_for_enter,
+	    "tools: gpioset - set some lines and wait for enter",
+	    0, { 8, 8, 8 });
+
+static void gpioset_set_some_lines_and_wait_for_signal(void)
+{
+	static const int signals[] = { SIGTERM, SIGINT };
+
+	unsigned int offsets[5], i;
+	int rv, values[5];
+
+	for (i = 0; i < TEST_ARRAY_SIZE(signals); i++) {
+		test_tool_run("gpioset", "--mode=signal", test_chip_name(2),
+			      "1=0", "2=1", "5=0", "6=0", "7=1", (char *)NULL);
+		usleep(200000);
+		test_tool_signal(signals[i]);
+		test_tool_wait();
+
+		TEST_ASSERT(test_tool_exited());
+		TEST_ASSERT_RET_OK(test_tool_exit_status());
+		TEST_ASSERT_NULL(test_tool_stdout());
+		TEST_ASSERT_NULL(test_tool_stderr());
+
+		offsets[0] = 1;
+		offsets[1] = 2;
+		offsets[2] = 5;
+		offsets[3] = 6;
+		offsets[4] = 7;
+
+		rv = gpiod_ctxless_get_value_multiple(test_chip_name(2),
+						      offsets, values,
+						      5, false, TEST_CONSUMER);
+		TEST_ASSERT_RET_OK(rv);
+
+		TEST_ASSERT_EQ(values[0], 0);
+		TEST_ASSERT_EQ(values[1], 1);
+		TEST_ASSERT_EQ(values[2], 0);
+		TEST_ASSERT_EQ(values[3], 0);
+		TEST_ASSERT_EQ(values[4], 1);
+	}
+}
+TEST_DEFINE(gpioset_set_some_lines_and_wait_for_signal,
+	    "tools: gpioset - set some lines and wait for signal",
+	    0, { 8, 8, 8 });
+
+static void gpioset_set_some_lines_and_wait_time(void)
+{
+	unsigned int offsets[3];
+	int rv, values[3];
+
+	test_tool_run("gpioset", "--mode=time",
+		      "--usec=100000", "--sec=0", test_chip_name(0),
+		      "1=1", "2=0", "5=1", (char *)NULL);
+	usleep(200000);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_RET_OK(test_tool_exit_status());
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NULL(test_tool_stderr());
+
+	offsets[0] = 1;
+	offsets[1] = 2;
+	offsets[2] = 5;
+
+	rv = gpiod_ctxless_get_value_multiple(test_chip_name(0), offsets,
+					      values, 3, false, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(values[0], 1);
+	TEST_ASSERT_EQ(values[1], 0);
+	TEST_ASSERT_EQ(values[2], 1);
+}
+TEST_DEFINE(gpioset_set_some_lines_and_wait_time,
+	    "tools: gpioset - set some lines and wait for specified time",
+	    0, { 8, 8, 8 });
+
+static void gpioset_no_arguments(void)
+{
+	test_tool_run("gpioset", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "gpiochip must be specified");
+}
+TEST_DEFINE(gpioset_no_arguments,
+	    "tools: gpioset - no arguments",
+	    0, { });
+
+static void gpioset_no_lines_specified(void)
+{
+	test_tool_run("gpioset", test_chip_name(1), (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "at least one GPIO line offset to value mapping must be specified");
+}
+TEST_DEFINE(gpioset_no_lines_specified,
+	    "tools: gpioset - no lines specified",
+	    0, { 4, 4 });
+
+static void gpioset_too_many_lines_specified(void)
+{
+	test_tool_run("gpioset", test_chip_name(0),
+		      "0=1", "1=1", "2=1", "3=1", "4=1", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "error setting the GPIO line values");
+}
+TEST_DEFINE(gpioset_too_many_lines_specified,
+	    "tools: gpioset - too many lines specified",
+	    0, { 4 });
+
+static void gpioset_sec_usec_without_time(void)
+{
+	test_tool_run("gpioset", "--mode=exit", "--sec=1",
+		      test_chip_name(0), "0=1", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "can't specify wait time in this mode");
+
+	test_tool_run("gpioset", "--mode=exit", "--usec=100",
+		      test_chip_name(0), "0=1", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "can't specify wait time in this mode");
+}
+TEST_DEFINE(gpioset_sec_usec_without_time,
+	    "tools: gpioset - using --sec/--usec with mode other than 'time'",
+	    0, { 4 });
+
+static void gpioset_invalid_mapping(void)
+{
+	test_tool_run("gpioset", test_chip_name(0), "0=c", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "invalid offset<->value mapping");
+}
+TEST_DEFINE(gpioset_invalid_mapping,
+	    "tools: gpioset - invalid offset<->value mapping",
+	    0, { 4 });
+
+static void gpioset_invalid_value(void)
+{
+	test_tool_run("gpioset", test_chip_name(0), "0=3", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(), "value must be 0 or 1");
+}
+TEST_DEFINE(gpioset_invalid_value,
+	    "tools: gpioset - value different than 0 or 1",
+	    0, { 4 });
+
+static void gpioset_invalid_offset(void)
+{
+	test_tool_run("gpioset", test_chip_name(0),
+		      "4000000000=1", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(), "invalid offset");
+}
+TEST_DEFINE(gpioset_invalid_offset,
+	    "tools: gpioset - invalid offset",
+	    0, { 4 });
+
+static void gpioset_daemonize_in_wrong_mode(void)
+{
+	test_tool_run("gpioset", "--background",
+		      test_chip_name(0), "0=1", (char *)NULL);
+	test_tool_wait();
+
+	TEST_ASSERT(test_tool_exited());
+	TEST_ASSERT_EQ(test_tool_exit_status(), 1);
+	TEST_ASSERT_NULL(test_tool_stdout());
+	TEST_ASSERT_NOT_NULL(test_tool_stderr());
+	TEST_ASSERT_STR_CONTAINS(test_tool_stderr(),
+				 "can't daemonize in this mode");
+}
+TEST_DEFINE(gpioset_daemonize_in_wrong_mode,
+	    "tools: gpioset - daemonize in wrong mode",
+	    0, { 4 });
diff --git a/tests/tests-iter.c b/tests/tests-iter.c
new file mode 100644
index 0000000..f4aff42
--- /dev/null
+++ b/tests/tests-iter.c
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Iterator test cases. */
+
+#include "gpiod-test.h"
+
+static void chip_iter(void)
+{
+	TEST_CLEANUP(test_free_chip_iter) struct gpiod_chip_iter *iter = NULL;
+	struct gpiod_chip *chip;
+	bool A, B, C;
+
+	A = B = C = false;
+
+	iter = gpiod_chip_iter_new();
+	TEST_ASSERT_NOT_NULL(iter);
+
+	gpiod_foreach_chip(iter, chip) {
+		if (strcmp(gpiod_chip_label(chip), "gpio-mockup-A") == 0)
+			A = true;
+		else if (strcmp(gpiod_chip_label(chip), "gpio-mockup-B") == 0)
+			B = true;
+		else if (strcmp(gpiod_chip_label(chip), "gpio-mockup-C") == 0)
+			C = true;
+	}
+
+	TEST_ASSERT(A);
+	TEST_ASSERT(B);
+	TEST_ASSERT(C);
+}
+TEST_DEFINE(chip_iter,
+	    "gpiod_chip_iter - simple loop",
+	    0, { 8, 8, 8 });
+
+static void chip_iter_noclose(void)
+{
+	TEST_CLEANUP(test_free_chip_iter_noclose)
+			struct gpiod_chip_iter *iter = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chipA = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chipB = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chipC = NULL;
+	struct gpiod_chip *chip;
+	bool A, B, C;
+
+	A = B = C = false;
+
+	iter = gpiod_chip_iter_new();
+	TEST_ASSERT_NOT_NULL(iter);
+
+	gpiod_foreach_chip_noclose(iter, chip) {
+		if (strcmp(gpiod_chip_label(chip), "gpio-mockup-A") == 0) {
+			A = true;
+			chipA = chip;
+		} else if (strcmp(gpiod_chip_label(chip),
+				  "gpio-mockup-B") == 0) {
+			B = true;
+			chipB = chip;
+		} else if (strcmp(gpiod_chip_label(chip),
+				  "gpio-mockup-C") == 0) {
+			C = true;
+			chipC = chip;
+		}
+	}
+
+	TEST_ASSERT(A);
+	TEST_ASSERT(B);
+	TEST_ASSERT(C);
+
+	gpiod_chip_iter_free_noclose(iter);
+	iter = NULL;
+
+	/* See if the chips are still open and usable. */
+	TEST_ASSERT_STR_EQ(gpiod_chip_label(chipA), "gpio-mockup-A");
+	TEST_ASSERT_STR_EQ(gpiod_chip_label(chipB), "gpio-mockup-B");
+	TEST_ASSERT_STR_EQ(gpiod_chip_label(chipC), "gpio-mockup-C");
+}
+TEST_DEFINE(chip_iter_noclose,
+	    "gpiod_chip_iter - simple loop, noclose variant",
+	    0, { 8, 8, 8 });
+
+static void chip_iter_break(void)
+{
+	TEST_CLEANUP(test_free_chip_iter) struct gpiod_chip_iter *iter = NULL;
+	struct gpiod_chip *chip;
+	int i = 0;
+
+	iter = gpiod_chip_iter_new();
+	TEST_ASSERT_NOT_NULL(iter);
+
+	gpiod_foreach_chip(iter, chip) {
+		if ((strcmp(gpiod_chip_label(chip), "gpio-mockup-A") == 0) ||
+		    (strcmp(gpiod_chip_label(chip), "gpio-mockup-B") == 0) ||
+		    (strcmp(gpiod_chip_label(chip), "gpio-mockup-C") == 0))
+			i++;
+
+		if (i == 3)
+			break;
+	}
+
+	gpiod_chip_iter_free(iter);
+	iter = NULL;
+
+	TEST_ASSERT_EQ(i, 3);
+}
+TEST_DEFINE(chip_iter_break,
+	    "gpiod_chip_iter - break",
+	    0, { 8, 8, 8, 8, 8 });
+
+static void line_iter(void)
+{
+	TEST_CLEANUP(test_free_line_iter) struct gpiod_line_iter *iter = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line;
+	unsigned int i = 0;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	iter = gpiod_line_iter_new(chip);
+	TEST_ASSERT_NOT_NULL(iter);
+
+	gpiod_foreach_line(iter, line) {
+		TEST_ASSERT_EQ(i, gpiod_line_offset(line));
+		i++;
+	}
+
+	TEST_ASSERT_EQ(8, i);
+}
+TEST_DEFINE(line_iter,
+	    "gpiod_line_iter - simple loop, check offsets",
+	    0, { 8 });
diff --git a/tests/tests-line.c b/tests/tests-line.c
new file mode 100644
index 0000000..32b575a
--- /dev/null
+++ b/tests/tests-line.c
@@ -0,0 +1,721 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* GPIO line test cases. */
+
+#include <errno.h>
+
+#include "gpiod-test.h"
+
+static void line_request_output(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line_0;
+	struct gpiod_line *line_1;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line_0 = gpiod_chip_get_line(chip, 2);
+	line_1 = gpiod_chip_get_line(chip, 5);
+	TEST_ASSERT_NOT_NULL(line_0);
+	TEST_ASSERT_NOT_NULL(line_1);
+
+	rv = gpiod_line_request_output(line_0, TEST_CONSUMER, 0);
+	TEST_ASSERT_RET_OK(rv);
+	rv = gpiod_line_request_output(line_1, TEST_CONSUMER, 1);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(gpiod_line_get_value(line_0), 0);
+	TEST_ASSERT_EQ(gpiod_line_get_value(line_1), 1);
+}
+TEST_DEFINE(line_request_output,
+	    "gpiod_line_request_output() - good",
+	    0, { 8 });
+
+static void line_request_already_requested(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 0);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_input(line, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	rv = gpiod_line_request_input(line, TEST_CONSUMER);
+	TEST_ASSERT_NOTEQ(rv, 0);
+	TEST_ASSERT_ERRNO_IS(EBUSY);
+}
+TEST_DEFINE(line_request_already_requested,
+	    "gpiod_line_request() - already requested",
+	    0, { 8 });
+
+static void line_consumer(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 0);
+	TEST_ASSERT_NOT_NULL(line);
+
+	TEST_ASSERT_NULL(gpiod_line_consumer(line));
+
+	rv = gpiod_line_request_input(line, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT(!gpiod_line_needs_update(line));
+	TEST_ASSERT_STR_EQ(gpiod_line_consumer(line), TEST_CONSUMER);
+}
+TEST_DEFINE(line_consumer,
+	    "gpiod_line_consumer() - good",
+	    0, { 8 });
+
+static void line_consumer_long_string(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 0);
+	TEST_ASSERT_NOT_NULL(line);
+
+	TEST_ASSERT_NULL(gpiod_line_consumer(line));
+
+	rv = gpiod_line_request_input(line,
+				      "consumer string over 32 characters long");
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT(!gpiod_line_needs_update(line));
+	TEST_ASSERT_STR_EQ(gpiod_line_consumer(line),
+			   "consumer string over 32 charact");
+	TEST_ASSERT_EQ(strlen(gpiod_line_consumer(line)), 31);
+}
+TEST_DEFINE(line_consumer_long_string,
+	    "gpiod_line_consumer() - long consumer string",
+	    0, { 8 });
+
+static void line_request_bulk_output(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chipA = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chipB = NULL;
+	struct gpiod_line_bulk bulkB = GPIOD_LINE_BULK_INITIALIZER;
+	struct gpiod_line_bulk bulkA;
+	struct gpiod_line *lineA0;
+	struct gpiod_line *lineA1;
+	struct gpiod_line *lineA2;
+	struct gpiod_line *lineA3;
+	struct gpiod_line *lineB0;
+	struct gpiod_line *lineB1;
+	struct gpiod_line *lineB2;
+	struct gpiod_line *lineB3;
+	int valA[4], valB[4];
+	int rv;
+
+	chipA = gpiod_chip_open(test_chip_path(0));
+	chipB = gpiod_chip_open(test_chip_path(1));
+	TEST_ASSERT_NOT_NULL(chipA);
+	TEST_ASSERT_NOT_NULL(chipB);
+
+	gpiod_line_bulk_init(&bulkA);
+
+	lineA0 = gpiod_chip_get_line(chipA, 0);
+	lineA1 = gpiod_chip_get_line(chipA, 1);
+	lineA2 = gpiod_chip_get_line(chipA, 2);
+	lineA3 = gpiod_chip_get_line(chipA, 3);
+	lineB0 = gpiod_chip_get_line(chipB, 0);
+	lineB1 = gpiod_chip_get_line(chipB, 1);
+	lineB2 = gpiod_chip_get_line(chipB, 2);
+	lineB3 = gpiod_chip_get_line(chipB, 3);
+
+	TEST_ASSERT_NOT_NULL(lineA0);
+	TEST_ASSERT_NOT_NULL(lineA1);
+	TEST_ASSERT_NOT_NULL(lineA2);
+	TEST_ASSERT_NOT_NULL(lineA3);
+	TEST_ASSERT_NOT_NULL(lineB0);
+	TEST_ASSERT_NOT_NULL(lineB1);
+	TEST_ASSERT_NOT_NULL(lineB2);
+	TEST_ASSERT_NOT_NULL(lineB3);
+
+	gpiod_line_bulk_add(&bulkA, lineA0);
+	gpiod_line_bulk_add(&bulkA, lineA1);
+	gpiod_line_bulk_add(&bulkA, lineA2);
+	gpiod_line_bulk_add(&bulkA, lineA3);
+	gpiod_line_bulk_add(&bulkB, lineB0);
+	gpiod_line_bulk_add(&bulkB, lineB1);
+	gpiod_line_bulk_add(&bulkB, lineB2);
+	gpiod_line_bulk_add(&bulkB, lineB3);
+
+	valA[0] = 1;
+	valA[1] = 0;
+	valA[2] = 0;
+	valA[3] = 1;
+	rv = gpiod_line_request_bulk_output(&bulkA, TEST_CONSUMER, valA);
+	TEST_ASSERT_RET_OK(rv);
+
+	valB[0] = 0;
+	valB[1] = 1;
+	valB[2] = 0;
+	valB[3] = 1;
+	rv = gpiod_line_request_bulk_output(&bulkB, TEST_CONSUMER, valB);
+	TEST_ASSERT_RET_OK(rv);
+
+	memset(valA, 0, sizeof(valA));
+	memset(valB, 0, sizeof(valB));
+
+	rv = gpiod_line_get_value_bulk(&bulkA, valA);
+	TEST_ASSERT_RET_OK(rv);
+	TEST_ASSERT_EQ(valA[0], 1);
+	TEST_ASSERT_EQ(valA[1], 0);
+	TEST_ASSERT_EQ(valA[2], 0);
+	TEST_ASSERT_EQ(valA[3], 1);
+
+	rv = gpiod_line_get_value_bulk(&bulkB, valB);
+	TEST_ASSERT_RET_OK(rv);
+	TEST_ASSERT_EQ(valB[0], 0);
+	TEST_ASSERT_EQ(valB[1], 1);
+	TEST_ASSERT_EQ(valB[2], 0);
+	TEST_ASSERT_EQ(valB[3], 1);
+}
+TEST_DEFINE(line_request_bulk_output,
+	    "gpiod_line_request_bulk_output() - good",
+	    0, { 8, 8 });
+
+static void line_request_bulk_different_chips(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chipA = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chipB = NULL;
+	struct gpiod_line_request_config req;
+	struct gpiod_line_bulk bulk;
+	struct gpiod_line *lineA0;
+	struct gpiod_line *lineA1;
+	struct gpiod_line *lineB0;
+	struct gpiod_line *lineB1;
+	int rv;
+
+	chipA = gpiod_chip_open(test_chip_path(0));
+	chipB = gpiod_chip_open(test_chip_path(1));
+	TEST_ASSERT_NOT_NULL(chipA);
+	TEST_ASSERT_NOT_NULL(chipB);
+
+	lineA0 = gpiod_chip_get_line(chipA, 0);
+	lineA1 = gpiod_chip_get_line(chipA, 1);
+	lineB0 = gpiod_chip_get_line(chipB, 0);
+	lineB1 = gpiod_chip_get_line(chipB, 1);
+
+	TEST_ASSERT_NOT_NULL(lineA0);
+	TEST_ASSERT_NOT_NULL(lineA1);
+	TEST_ASSERT_NOT_NULL(lineB0);
+	TEST_ASSERT_NOT_NULL(lineB1);
+
+	gpiod_line_bulk_init(&bulk);
+	gpiod_line_bulk_add(&bulk, lineA0);
+	gpiod_line_bulk_add(&bulk, lineA1);
+	gpiod_line_bulk_add(&bulk, lineB0);
+	gpiod_line_bulk_add(&bulk, lineB1);
+
+	req.consumer = TEST_CONSUMER;
+	req.request_type = GPIOD_LINE_REQUEST_DIRECTION_INPUT;
+	req.flags = GPIOD_LINE_ACTIVE_STATE_HIGH;
+
+	rv = gpiod_line_request_bulk(&bulk, &req, NULL);
+	TEST_ASSERT_NOTEQ(rv, 0);
+	TEST_ASSERT_ERRNO_IS(EINVAL);
+}
+TEST_DEFINE(line_request_bulk_different_chips,
+	    "gpiod_line_request_bulk() - different chips",
+	    0, { 8, 8 });
+
+static void line_request_null_default_vals_for_output(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line_bulk bulk = GPIOD_LINE_BULK_INITIALIZER;
+	struct gpiod_line *line;
+	int rv, vals[3];
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 0);
+	gpiod_line_bulk_add(&bulk, line);
+	line = gpiod_chip_get_line(chip, 1);
+	gpiod_line_bulk_add(&bulk, line);
+	line = gpiod_chip_get_line(chip, 2);
+	gpiod_line_bulk_add(&bulk, line);
+
+	rv = gpiod_line_request_bulk_output(&bulk, TEST_CONSUMER, NULL);
+	TEST_ASSERT_RET_OK(rv);
+
+	gpiod_line_release_bulk(&bulk);
+
+	rv = gpiod_line_request_bulk_input(&bulk, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	memset(vals, 0, sizeof(vals));
+
+	rv = gpiod_line_get_value_bulk(&bulk, vals);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(vals[0], 0);
+	TEST_ASSERT_EQ(vals[1], 0);
+	TEST_ASSERT_EQ(vals[2], 0);
+}
+TEST_DEFINE(line_request_null_default_vals_for_output,
+	    "gpiod_line_request_bulk() - null default vals for output",
+	    0, { 8 });
+
+static void line_set_value(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 2);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_output(line, TEST_CONSUMER, 0);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_RET_OK(gpiod_line_set_value(line, 1));
+	TEST_ASSERT_EQ(gpiod_line_get_value(line), 1);
+	TEST_ASSERT_RET_OK(gpiod_line_set_value(line, 0));
+	TEST_ASSERT_EQ(gpiod_line_get_value(line), 0);
+}
+TEST_DEFINE(line_set_value,
+	    "gpiod_line_set_value() - good",
+	    0, { 8 });
+
+static void line_get_value_different_chips(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chipA = NULL;
+	TEST_CLEANUP_CHIP struct gpiod_chip *chipB = NULL;
+	struct gpiod_line *lineA1, *lineA2, *lineB1, *lineB2;
+	struct gpiod_line_bulk bulkA, bulkB, bulk;
+	int rv, vals[4];
+
+	chipA = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chipA);
+
+	chipB = gpiod_chip_open(test_chip_path(1));
+	TEST_ASSERT_NOT_NULL(chipB);
+
+	lineA1 = gpiod_chip_get_line(chipA, 3);
+	lineA2 = gpiod_chip_get_line(chipA, 4);
+	lineB1 = gpiod_chip_get_line(chipB, 5);
+	lineB2 = gpiod_chip_get_line(chipB, 6);
+	TEST_ASSERT_NOT_NULL(lineA1);
+	TEST_ASSERT_NOT_NULL(lineA2);
+	TEST_ASSERT_NOT_NULL(lineB1);
+	TEST_ASSERT_NOT_NULL(lineB2);
+
+	gpiod_line_bulk_init(&bulkA);
+	gpiod_line_bulk_init(&bulkB);
+	gpiod_line_bulk_init(&bulk);
+
+	gpiod_line_bulk_add(&bulk, lineA1);
+	gpiod_line_bulk_add(&bulk, lineA2);
+	gpiod_line_bulk_add(&bulk, lineB1);
+	gpiod_line_bulk_add(&bulk, lineB2);
+
+	gpiod_line_bulk_add(&bulkA, lineA1);
+	gpiod_line_bulk_add(&bulkA, lineA2);
+	gpiod_line_bulk_add(&bulkB, lineB1);
+	gpiod_line_bulk_add(&bulkB, lineB2);
+	gpiod_line_bulk_add(&bulk, lineA1);
+	gpiod_line_bulk_add(&bulk, lineA2);
+	gpiod_line_bulk_add(&bulk, lineB1);
+	gpiod_line_bulk_add(&bulk, lineB2);
+
+	rv = gpiod_line_request_bulk_input(&bulkA, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	rv = gpiod_line_request_bulk_input(&bulkB, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	rv = gpiod_line_get_value_bulk(&bulk, vals);
+	TEST_ASSERT_EQ(rv, -1);
+	TEST_ASSERT_ERRNO_IS(EINVAL);
+}
+TEST_DEFINE(line_get_value_different_chips,
+	    "gpiod_line_get_value_bulk() - different chips",
+	    0, { 8, 8 });
+
+static void line_get_good(void)
+{
+	TEST_CLEANUP(test_line_close_chip) struct gpiod_line *line = NULL;
+
+	line = gpiod_line_get(test_chip_name(2), 18);
+	TEST_ASSERT_NOT_NULL(line);
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 18);
+}
+TEST_DEFINE(line_get_good,
+	    "gpiod_line_get() - good",
+	    TEST_FLAG_NAMED_LINES, { 16, 16, 32, 16 });
+
+static void line_get_invalid_offset(void)
+{
+	TEST_CLEANUP(test_line_close_chip) struct gpiod_line *line = NULL;
+
+	line = gpiod_line_get(test_chip_name(3), 18);
+	TEST_ASSERT_NULL(line);
+	TEST_ASSERT_ERRNO_IS(EINVAL);
+}
+TEST_DEFINE(line_get_invalid_offset,
+	    "gpiod_line_get() - invalid offset",
+	    TEST_FLAG_NAMED_LINES, { 16, 16, 32, 16 });
+
+static void line_find_good(void)
+{
+	TEST_CLEANUP(test_line_close_chip) struct gpiod_line *line = NULL;
+
+	line = gpiod_line_find("gpio-mockup-C-12");
+	TEST_ASSERT_NOT_NULL(line);
+
+	TEST_ASSERT_EQ(gpiod_line_offset(line), 12);
+}
+TEST_DEFINE(line_find_good,
+	    "gpiod_line_find() - good",
+	    TEST_FLAG_NAMED_LINES, { 16, 16, 32, 16 });
+
+static void line_find_not_found(void)
+{
+	TEST_CLEANUP(test_line_close_chip) struct gpiod_line *line = NULL;
+
+	line = gpiod_line_find("nonexistent");
+	TEST_ASSERT_NULL(line);
+	TEST_ASSERT_ERRNO_IS(ENOENT);
+}
+TEST_DEFINE(line_find_not_found,
+	    "gpiod_line_find() - not found",
+	    TEST_FLAG_NAMED_LINES, { 16, 16, 32, 16 });
+
+static void line_find_unnamed_lines(void)
+{
+	TEST_CLEANUP(test_line_close_chip) struct gpiod_line *line = NULL;
+
+	line = gpiod_line_find("gpio-mockup-C-12");
+	TEST_ASSERT_NULL(line);
+	TEST_ASSERT_ERRNO_IS(ENOENT);
+}
+TEST_DEFINE(line_find_unnamed_lines,
+	    "gpiod_line_find() - unnamed lines",
+	    0, { 16, 16, 32, 16 });
+
+static void line_direction(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 5);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_output(line, TEST_CONSUMER, 0);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(gpiod_line_direction(line), GPIOD_LINE_DIRECTION_OUTPUT);
+
+	gpiod_line_release(line);
+
+	rv = gpiod_line_request_input(line, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(gpiod_line_direction(line), GPIOD_LINE_DIRECTION_INPUT);
+}
+TEST_DEFINE(line_direction,
+	    "gpiod_line_direction() - set & get",
+	    0, { 8 });
+
+static void line_active_state(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 5);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_input(line, TEST_CONSUMER);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(gpiod_line_active_state(line),
+		       GPIOD_LINE_ACTIVE_STATE_HIGH);
+
+	gpiod_line_release(line);
+
+	rv = gpiod_line_request_input_flags(line, TEST_CONSUMER,
+					GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT_EQ(gpiod_line_direction(line), GPIOD_LINE_DIRECTION_INPUT);
+}
+TEST_DEFINE(line_active_state,
+	    "gpiod_line_active_state() - set & get",
+	    0, { 8 });
+
+static void line_misc_flags(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line_request_config config;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 2);
+	TEST_ASSERT_NOT_NULL(line);
+
+	TEST_ASSERT_FALSE(gpiod_line_is_used(line));
+	TEST_ASSERT_FALSE(gpiod_line_is_open_drain(line));
+	TEST_ASSERT_FALSE(gpiod_line_is_open_source(line));
+
+	config.request_type = GPIOD_LINE_REQUEST_DIRECTION_OUTPUT;
+	config.consumer = TEST_CONSUMER;
+	config.flags = GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN;
+
+	rv = gpiod_line_request(line, &config, 0);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT(gpiod_line_is_used(line));
+	TEST_ASSERT(gpiod_line_is_open_drain(line));
+	TEST_ASSERT_FALSE(gpiod_line_is_open_source(line));
+
+	gpiod_line_release(line);
+
+	config.flags = GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE;
+
+	rv = gpiod_line_request(line, &config, 0);
+	TEST_ASSERT_RET_OK(rv);
+
+	TEST_ASSERT(gpiod_line_is_used(line));
+	TEST_ASSERT_FALSE(gpiod_line_is_open_drain(line));
+	TEST_ASSERT(gpiod_line_is_open_source(line));
+}
+TEST_DEFINE(line_misc_flags,
+	    "gpiod_line - misc flags",
+	    0, { 8 });
+
+static void line_open_source_open_drain_input_invalid(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 2);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_input_flags(line, TEST_CONSUMER,
+					GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN);
+	TEST_ASSERT_EQ(rv, -1);
+	TEST_ASSERT_ERRNO_IS(EINVAL);
+
+	rv = gpiod_line_request_input_flags(line, TEST_CONSUMER,
+					GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE);
+	TEST_ASSERT_EQ(rv, -1);
+	TEST_ASSERT_ERRNO_IS(EINVAL);
+}
+TEST_DEFINE(line_open_source_open_drain_input_invalid,
+	    "gpiod_line - open-source & open-drain for input mode (invalid)",
+	    0, { 8 });
+
+static void line_open_source_open_drain_simultaneously(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 2);
+	TEST_ASSERT_NOT_NULL(line);
+
+	rv = gpiod_line_request_output_flags(line, TEST_CONSUMER,
+				GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE |
+				GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN, 1);
+	TEST_ASSERT_EQ(rv, -1);
+	TEST_ASSERT_ERRNO_IS(EINVAL);
+}
+TEST_DEFINE(line_open_source_open_drain_simultaneously,
+	    "gpiod_line - open-source & open-drain flags simultaneously",
+	    0, { 8 });
+
+/* Verify that the reference counting of the line fd handle works correctly. */
+static void line_release_one_use_another(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line_bulk bulk;
+	struct gpiod_line *line1;
+	struct gpiod_line *line2;
+	int rv, vals[2];
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line1 = gpiod_chip_get_line(chip, 2);
+	TEST_ASSERT_NOT_NULL(line1);
+	line2 = gpiod_chip_get_line(chip, 3);
+	TEST_ASSERT_NOT_NULL(line2);
+
+	gpiod_line_bulk_init(&bulk);
+	gpiod_line_bulk_add(&bulk, line1);
+	gpiod_line_bulk_add(&bulk, line2);
+
+	vals[0] = vals[1] = 1;
+
+	rv = gpiod_line_request_bulk_output(&bulk, TEST_CONSUMER, vals);
+	TEST_ASSERT_RET_OK(rv);
+
+	gpiod_line_release(line1);
+
+	rv = gpiod_line_get_value(line1);
+	TEST_ASSERT_EQ(rv, -1);
+	TEST_ASSERT_ERRNO_IS(EPERM);
+
+	rv = gpiod_line_get_value(line2);
+	TEST_ASSERT_EQ(rv, 1);
+}
+TEST_DEFINE(line_release_one_use_another,
+	    "gpiod_line - request two, release one, use the other one",
+	    0, { 8 });
+
+static void line_null_consumer(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line_request_config config;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 2);
+	TEST_ASSERT_NOT_NULL(line);
+
+	config.request_type = GPIOD_LINE_REQUEST_DIRECTION_INPUT;
+	config.consumer = NULL;
+	config.flags = 0;
+
+	rv = gpiod_line_request(line, &config, 0);
+	TEST_ASSERT_RET_OK(rv);
+	TEST_ASSERT_STR_EQ(gpiod_line_consumer(line), "?");
+
+	gpiod_line_release(line);
+
+	/*
+	 * Internally we use different structures for event requests, so we
+	 * need to test that explicitly too.
+	 */
+	config.request_type = GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES;
+
+	rv = gpiod_line_request(line, &config, 0);
+	TEST_ASSERT_RET_OK(rv);
+	TEST_ASSERT_STR_EQ(gpiod_line_consumer(line), "?");
+}
+TEST_DEFINE(line_null_consumer,
+	    "line request - NULL consumer string",
+	    0, { 8 });
+
+static void line_empty_consumer(void)
+{
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line_request_config config;
+	struct gpiod_line *line;
+	int rv;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	line = gpiod_chip_get_line(chip, 2);
+	TEST_ASSERT_NOT_NULL(line);
+
+	config.request_type = GPIOD_LINE_REQUEST_DIRECTION_INPUT;
+	config.consumer = "";
+	config.flags = 0;
+
+	rv = gpiod_line_request(line, &config, 0);
+	TEST_ASSERT_RET_OK(rv);
+	TEST_ASSERT_STR_EQ(gpiod_line_consumer(line), "?");
+
+	gpiod_line_release(line);
+
+	/*
+	 * Internally we use different structures for event requests, so we
+	 * need to test that explicitly too.
+	 */
+	config.request_type = GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES;
+
+	rv = gpiod_line_request(line, &config, 0);
+	TEST_ASSERT_RET_OK(rv);
+	TEST_ASSERT_STR_EQ(gpiod_line_consumer(line), "?");
+}
+TEST_DEFINE(line_empty_consumer,
+	    "line request - empty consumer string",
+	    0, { 8 });
+
+static void line_bulk_foreach(void)
+{
+	static const char *const line_names[] = { "gpio-mockup-A-0",
+						  "gpio-mockup-A-1",
+						  "gpio-mockup-A-2",
+						  "gpio-mockup-A-3" };
+
+	TEST_CLEANUP_CHIP struct gpiod_chip *chip = NULL;
+	struct gpiod_line_bulk bulk = GPIOD_LINE_BULK_INITIALIZER;
+	struct gpiod_line *line, **lineptr;
+	int i;
+
+	chip = gpiod_chip_open(test_chip_path(0));
+	TEST_ASSERT_NOT_NULL(chip);
+
+	for (i = 0; i < 4; i++) {
+		line = gpiod_chip_get_line(chip, i);
+		TEST_ASSERT_NOT_NULL(line);
+
+		gpiod_line_bulk_add(&bulk, line);
+	}
+
+	i = 0;
+	gpiod_line_bulk_foreach_line(&bulk, line, lineptr)
+		TEST_ASSERT_STR_EQ(gpiod_line_name(line), line_names[i++]);
+
+	i = 0;
+	gpiod_line_bulk_foreach_line(&bulk, line, lineptr) {
+		TEST_ASSERT_STR_EQ(gpiod_line_name(line), line_names[i++]);
+		if (i == 2)
+			break;
+	}
+}
+TEST_DEFINE(line_bulk_foreach,
+	    "line bulk - iterate over all lines",
+	    TEST_FLAG_NAMED_LINES, { 8 });
diff --git a/tests/tests-misc.c b/tests/tests-misc.c
new file mode 100644
index 0000000..99a80a7
--- /dev/null
+++ b/tests/tests-misc.c
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Misc test cases. */
+
+#include <errno.h>
+
+#include "gpiod-test.h"
+
+static void version_string(void)
+{
+	/* Check that gpiod_version_string() returns an actual string. */
+	TEST_ASSERT_NOT_NULL(gpiod_version_string());
+	TEST_ASSERT(strlen(gpiod_version_string()) > 0);
+	TEST_ASSERT_REGEX_MATCH(gpiod_version_string(),
+				"^[0-9]+\\.[0-9]+[0-9a-zA-Z\\.]*$");
+}
+TEST_DEFINE(version_string,
+	    "gpiod_version_string()",
+	    0, { });
diff --git a/tools/Makefile.am b/tools/Makefile.am
new file mode 100644
index 0000000..4198dba
--- /dev/null
+++ b/tools/Makefile.am
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+#
+
+AM_CFLAGS = -I$(top_srcdir)/include/ -include $(top_builddir)/config.h
+AM_CFLAGS += -Wall -Wextra -g
+
+noinst_LTLIBRARIES = libtools-common.la
+libtools_common_la_SOURCES = tools-common.c tools-common.h
+
+LDADD = libtools-common.la $(top_builddir)/lib/libgpiod.la
+
+bin_PROGRAMS = gpiodetect gpioinfo gpioget gpioset gpiomon gpiofind
+
+gpiodetect_SOURCES = gpiodetect.c
+
+gpioinfo_SOURCES = gpioinfo.c
+
+gpioget_SOURCES = gpioget.c
+
+gpioset_SOURCES = gpioset.c
+
+gpiomon_SOURCES = gpiomon.c
+
+gpiofind_SOURCES = gpiofind.c
diff --git a/tools/gpiodetect.c b/tools/gpiodetect.c
new file mode 100644
index 0000000..aa86568
--- /dev/null
+++ b/tools/gpiodetect.c
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#include <getopt.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "tools-common.h"
+
+static const struct option longopts[] = {
+	{ "help",	no_argument,	NULL,	'h' },
+	{ "version",	no_argument,	NULL,	'v' },
+	{ GETOPT_NULL_LONGOPT },
+};
+
+static const char *const shortopts = "+hv";
+
+static void print_help(void)
+{
+	printf("Usage: %s [OPTIONS]\n", get_progname());
+	printf("List all GPIO chips, print their labels and number of GPIO lines.\n");
+	printf("\n");
+	printf("Options:\n");
+	printf("  -h, --help:\t\tdisplay this message and exit\n");
+	printf("  -v, --version:\tdisplay the version and exit\n");
+}
+
+int main(int argc, char **argv)
+{
+	struct gpiod_chip_iter *iter;
+	struct gpiod_chip *chip;
+	int optc, opti;
+
+	for (;;) {
+		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+		if (optc < 0)
+			break;
+
+		switch (optc) {
+		case 'h':
+			print_help();
+			return EXIT_SUCCESS;
+		case 'v':
+			print_version();
+			return EXIT_SUCCESS;
+		case '?':
+			die("try %s --help", get_progname());
+		default:
+			abort();
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc > 0)
+		die("unrecognized argument: %s", argv[0]);
+
+	iter = gpiod_chip_iter_new();
+	if (!iter)
+		die_perror("unable to access GPIO chips");
+
+	gpiod_foreach_chip(iter, chip) {
+		printf("%s [%s] (%u lines)\n",
+		       gpiod_chip_name(chip),
+		       gpiod_chip_label(chip),
+		       gpiod_chip_num_lines(chip));
+	}
+
+	gpiod_chip_iter_free(iter);
+
+	return EXIT_SUCCESS;
+}
diff --git a/tools/gpiofind.c b/tools/gpiofind.c
new file mode 100644
index 0000000..35cc491
--- /dev/null
+++ b/tools/gpiofind.c
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#include <getopt.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "tools-common.h"
+
+static const struct option longopts[] = {
+	{ "help",	no_argument,	NULL,	'h' },
+	{ "version",	no_argument,	NULL,	'v' },
+	{ GETOPT_NULL_LONGOPT },
+};
+
+static const char *const shortopts = "+hv";
+
+static void print_help(void)
+{
+	printf("Usage: %s [OPTIONS] <name>\n", get_progname());
+	printf("Find a GPIO line by name. The output of this command can be used as input for gpioget/set.\n");
+	printf("\n");
+	printf("Options:\n");
+	printf("  -h, --help:\t\tdisplay this message and exit\n");
+	printf("  -v, --version:\tdisplay the version and exit\n");
+}
+
+int main(int argc, char **argv)
+{
+	unsigned int offset;
+	int optc, opti, rv;
+	char chip[32];
+
+	for (;;) {
+		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+		if (optc < 0)
+			break;
+
+		switch (optc) {
+		case 'h':
+			print_help();
+			return EXIT_SUCCESS;
+		case 'v':
+			print_version();
+			return EXIT_SUCCESS;
+		case '?':
+			die("try %s --help", get_progname());
+		default:
+			abort();
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc != 1)
+		die("exactly one GPIO line name must be specified");
+
+	rv = gpiod_ctxless_find_line(argv[0], chip, sizeof(chip), &offset);
+	if (rv < 0)
+		die_perror("error performing the line lookup");
+	else if (rv == 0)
+		return EXIT_FAILURE;
+
+	printf("%s %u\n", chip, offset);
+
+	return EXIT_SUCCESS;
+}
diff --git a/tools/gpioget.c b/tools/gpioget.c
new file mode 100644
index 0000000..196ebeb
--- /dev/null
+++ b/tools/gpioget.c
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#include <getopt.h>
+#include <gpiod.h>
+#include <limits.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "tools-common.h"
+
+static const struct option longopts[] = {
+	{ "help",	no_argument,	NULL,	'h' },
+	{ "version",	no_argument,	NULL,	'v' },
+	{ "active-low",	no_argument,	NULL,	'l' },
+	{ GETOPT_NULL_LONGOPT },
+};
+
+static const char *const shortopts = "+hvl";
+
+static void print_help(void)
+{
+	printf("Usage: %s [OPTIONS] <chip name/number> <offset 1> <offset 2> ...\n",
+	       get_progname());
+	printf("Read line value(s) from a GPIO chip\n");
+	printf("\n");
+	printf("Options:\n");
+	printf("  -h, --help:\t\tdisplay this message and exit\n");
+	printf("  -v, --version:\tdisplay the version and exit\n");
+	printf("  -l, --active-low:\tset the line active state to low\n");
+}
+
+int main(int argc, char **argv)
+{
+	unsigned int *offsets, i, num_lines;
+	int *values, optc, opti, rv;
+	bool active_low = false;
+	char *device, *end;
+
+	for (;;) {
+		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+		if (optc < 0)
+			break;
+
+		switch (optc) {
+		case 'h':
+			print_help();
+			return EXIT_SUCCESS;
+		case 'v':
+			print_version();
+			return EXIT_SUCCESS;
+		case 'l':
+			active_low = true;
+			break;
+		case '?':
+			die("try %s --help", get_progname());
+		default:
+			abort();
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc < 1)
+		die("gpiochip must be specified");
+
+	if (argc < 2)
+		die("at least one GPIO line offset must be specified");
+
+	device = argv[0];
+	num_lines = argc - 1;
+
+	values = malloc(sizeof(*values) * num_lines);
+	offsets = malloc(sizeof(*offsets) * num_lines);
+	if (!values || !offsets)
+		die("out of memory");
+
+	for (i = 0; i < num_lines; i++) {
+		offsets[i] = strtoul(argv[i + 1], &end, 10);
+		if (*end != '\0' || offsets[i] > INT_MAX)
+			die("invalid GPIO offset: %s", argv[i + 1]);
+	}
+
+	rv = gpiod_ctxless_get_value_multiple(device, offsets, values,
+					      num_lines, active_low,
+					      "gpioget");
+	if (rv < 0)
+		die_perror("error reading GPIO values");
+
+	for (i = 0; i < num_lines; i++) {
+		printf("%d", values[i]);
+		if (i != num_lines - 1)
+			printf(" ");
+	}
+	printf("\n");
+
+	free(values);
+	free(offsets);
+
+	return EXIT_SUCCESS;
+}
diff --git a/tools/gpioinfo.c b/tools/gpioinfo.c
new file mode 100644
index 0000000..bb17262
--- /dev/null
+++ b/tools/gpioinfo.c
@@ -0,0 +1,203 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#include <errno.h>
+#include <getopt.h>
+#include <gpiod.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "tools-common.h"
+
+typedef bool (*is_set_func)(struct gpiod_line *);
+
+struct flag {
+	const char *name;
+	is_set_func is_set;
+};
+
+static const struct flag flags[] = {
+	{
+		.name = "used",
+		.is_set = gpiod_line_is_used,
+	},
+	{
+		.name = "open-drain",
+		.is_set = gpiod_line_is_open_drain,
+	},
+	{
+		.name = "open-source",
+		.is_set = gpiod_line_is_open_source,
+	},
+};
+
+static const struct option longopts[] = {
+	{ "help",	no_argument,	NULL,	'h' },
+	{ "version",	no_argument,	NULL,	'v' },
+	{ GETOPT_NULL_LONGOPT },
+};
+
+static const char *const shortopts = "+hv";
+
+static void print_help(void)
+{
+	printf("Usage: %s [OPTIONS] <gpiochip1> ...\n", get_progname());
+	printf("Print information about all lines of the specified GPIO chip(s) (or all gpiochips if none are specified).\n");
+	printf("\n");
+	printf("Options:\n");
+	printf("  -h, --help:\t\tdisplay this message and exit\n");
+	printf("  -v, --version:\tdisplay the version and exit\n");
+}
+
+static PRINTF(3, 4) void prinfo(bool *of,
+				unsigned int prlen, const char *fmt, ...)
+{
+	char *buf, *buffmt = NULL;
+	size_t len;
+	va_list va;
+	int rv;
+
+	va_start(va, fmt);
+	rv = vasprintf(&buf, fmt, va);
+	va_end(va);
+	if (rv < 0)
+		die("vasprintf: %s\n", strerror(errno));
+
+	len = strlen(buf) - 1;
+
+	if (len >= prlen || *of) {
+		*of = true;
+		printf("%s", buf);
+	} else {
+		rv = asprintf(&buffmt, "%%%us", prlen);
+		if (rv < 0)
+			die("asprintf: %s\n", strerror(errno));
+
+		printf(buffmt, buf);
+	}
+
+	free(buf);
+	if (fmt)
+		free(buffmt);
+}
+
+static void list_lines(struct gpiod_chip *chip)
+{
+	struct gpiod_line_iter *iter;
+	int direction, active_state;
+	const char *name, *consumer;
+	struct gpiod_line *line;
+	unsigned int i, offset;
+	bool flag_printed, of;
+
+	iter = gpiod_line_iter_new(chip);
+	if (!iter)
+		die_perror("error creating line iterator");
+
+	printf("%s - %u lines:\n",
+	       gpiod_chip_name(chip), gpiod_chip_num_lines(chip));
+
+	gpiod_foreach_line(iter, line) {
+		offset = gpiod_line_offset(line);
+		name = gpiod_line_name(line);
+		consumer = gpiod_line_consumer(line);
+		direction = gpiod_line_direction(line);
+		active_state = gpiod_line_active_state(line);
+
+		of = false;
+
+		printf("\tline ");
+		prinfo(&of, 3, "%u", offset);
+		printf(": ");
+
+		name ? prinfo(&of, 12, "\"%s\"", name)
+		     : prinfo(&of, 12, "unnamed");
+		printf(" ");
+
+		consumer ? prinfo(&of, 12, "\"%s\"", consumer)
+			 : prinfo(&of, 12, "unused");
+		printf(" ");
+
+		prinfo(&of, 8, "%s ", direction == GPIOD_LINE_DIRECTION_INPUT
+							? "input" : "output");
+		prinfo(&of, 13, "%s ",
+		       active_state == GPIOD_LINE_ACTIVE_STATE_LOW
+							? "active-low"
+							: "active-high");
+
+		flag_printed = false;
+		for (i = 0; i < ARRAY_SIZE(flags); i++) {
+			if (flags[i].is_set(line)) {
+				if (flag_printed)
+					printf(" ");
+				else
+					printf("[");
+				printf("%s", flags[i].name);
+				flag_printed = true;
+			}
+		}
+		if (flag_printed)
+			printf("]");
+
+		printf("\n");
+	}
+
+	gpiod_line_iter_free(iter);
+}
+
+int main(int argc, char **argv)
+{
+	struct gpiod_chip_iter *chip_iter;
+	struct gpiod_chip *chip;
+	int i, optc, opti;
+
+	for (;;) {
+		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+		if (optc < 0)
+			break;
+
+		switch (optc) {
+		case 'h':
+			print_help();
+			return EXIT_SUCCESS;
+		case 'v':
+			print_version();
+			return EXIT_SUCCESS;
+		case '?':
+			die("try %s --help", get_progname());
+		default:
+			abort();
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc == 0) {
+		chip_iter = gpiod_chip_iter_new();
+		if (!chip_iter)
+			die_perror("error accessing GPIO chips");
+
+		gpiod_foreach_chip(chip_iter, chip)
+			list_lines(chip);
+
+		gpiod_chip_iter_free(chip_iter);
+	} else {
+		for (i = 0; i < argc; i++) {
+			chip = gpiod_chip_open_lookup(argv[i]);
+			if (!chip)
+				die_perror("looking up chip %s", argv[i]);
+
+			list_lines(chip);
+
+			gpiod_chip_close(chip);
+		}
+	}
+
+	return EXIT_SUCCESS;
+}
diff --git a/tools/gpiomon.c b/tools/gpiomon.c
new file mode 100644
index 0000000..9a1843b
--- /dev/null
+++ b/tools/gpiomon.c
@@ -0,0 +1,332 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#include <errno.h>
+#include <getopt.h>
+#include <gpiod.h>
+#include <limits.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/signalfd.h>
+#include <unistd.h>
+
+#include "tools-common.h"
+
+static const struct option longopts[] = {
+	{ "help",		no_argument,		NULL,	'h' },
+	{ "version",		no_argument,		NULL,	'v' },
+	{ "active-low",		no_argument,		NULL,	'l' },
+	{ "num-events",		required_argument,	NULL,	'n' },
+	{ "silent",		no_argument,		NULL,	's' },
+	{ "rising-edge",	no_argument,		NULL,	'r' },
+	{ "falling-edge",	no_argument,		NULL,	'f' },
+	{ "line-buffered",	no_argument,		NULL,	'b' },
+	{ "format",		required_argument,	NULL,	'F' },
+	{ GETOPT_NULL_LONGOPT },
+};
+
+static const char *const shortopts = "+hvln:srfbF:";
+
+static void print_help(void)
+{
+	printf("Usage: %s [OPTIONS] <chip name/number> <offset 1> <offset 2> ...\n",
+	       get_progname());
+	printf("Wait for events on GPIO lines and print them to standard output\n");
+	printf("\n");
+	printf("Options:\n");
+	printf("  -h, --help:\t\tdisplay this message and exit\n");
+	printf("  -v, --version:\tdisplay the version and exit\n");
+	printf("  -l, --active-low:\tset the line active state to low\n");
+	printf("  -n, --num-events=NUM:\texit after processing NUM events\n");
+	printf("  -s, --silent:\t\tdon't print event info\n");
+	printf("  -r, --rising-edge:\tonly process rising edge events\n");
+	printf("  -f, --falling-edge:\tonly process falling edge events\n");
+	printf("  -b, --line-buffered:\tset standard output as line buffered\n");
+	printf("  -F, --format=FMT\tspecify custom output format\n");
+	printf("\n");
+	printf("Format specifiers:\n");
+	printf("  %%o:  GPIO line offset\n");
+	printf("  %%e:  event type (0 - falling edge, 1 rising edge)\n");
+	printf("  %%s:  seconds part of the event timestamp\n");
+	printf("  %%n:  nanoseconds part of the event timestamp\n");
+}
+
+struct mon_ctx {
+	unsigned int offset;
+	unsigned int events_wanted;
+	unsigned int events_done;
+
+	bool silent;
+	char *fmt;
+
+	int sigfd;
+};
+
+static void event_print_custom(unsigned int offset,
+			       const struct timespec *ts,
+			       int event_type,
+			       struct mon_ctx *ctx)
+{
+	char *prev, *curr, fmt;
+
+	for (prev = curr = ctx->fmt;;) {
+		curr = strchr(curr, '%');
+		if (!curr) {
+			fputs(prev, stdout);
+			break;
+		}
+
+		if (prev != curr)
+			fwrite(prev, curr - prev, 1, stdout);
+
+		fmt = *(curr + 1);
+
+		switch (fmt) {
+		case 'o':
+			printf("%u", offset);
+			break;
+		case 'e':
+			if (event_type == GPIOD_CTXLESS_EVENT_CB_RISING_EDGE)
+				fputc('1', stdout);
+			else
+				fputc('0', stdout);
+			break;
+		case 's':
+			printf("%ld", ts->tv_sec);
+			break;
+		case 'n':
+			printf("%ld", ts->tv_nsec);
+			break;
+		case '%':
+			fputc('%', stdout);
+			break;
+		case '\0':
+			fputc('%', stdout);
+			goto end;
+		default:
+			fwrite(curr, 2, 1, stdout);
+			break;
+		}
+
+		curr += 2;
+		prev = curr;
+	}
+
+end:
+	fputc('\n', stdout);
+}
+
+static void event_print_human_readable(unsigned int offset,
+				       const struct timespec *ts,
+				       int event_type)
+{
+	char *evname;
+
+	if (event_type == GPIOD_CTXLESS_EVENT_CB_RISING_EDGE)
+		evname = " RISING EDGE";
+	else
+		evname = "FALLING EDGE";
+
+	printf("event: %s offset: %u timestamp: [%8ld.%09ld]\n",
+	       evname, offset, ts->tv_sec, ts->tv_nsec);
+}
+
+static int poll_callback(unsigned int num_lines,
+			 struct gpiod_ctxless_event_poll_fd *fds,
+			 const struct timespec *timeout, void *data)
+{
+	struct pollfd pfds[GPIOD_LINE_BULK_MAX_LINES + 1];
+	struct mon_ctx *ctx = data;
+	int cnt, ts, rv;
+	unsigned int i;
+
+	for (i = 0; i < num_lines; i++) {
+		pfds[i].fd = fds[i].fd;
+		pfds[i].events = POLLIN | POLLPRI;
+	}
+
+	pfds[i].fd = ctx->sigfd;
+	pfds[i].events = POLLIN | POLLPRI;
+
+	ts = timeout->tv_sec * 1000 + timeout->tv_nsec / 1000000;
+
+	cnt = poll(pfds, num_lines + 1, ts);
+	if (cnt < 0)
+		return GPIOD_CTXLESS_EVENT_POLL_RET_ERR;
+	else if (cnt == 0)
+		return GPIOD_CTXLESS_EVENT_POLL_RET_TIMEOUT;
+
+	rv = cnt;
+	for (i = 0; i < num_lines; i++) {
+		if (pfds[i].revents) {
+			fds[i].event = true;
+			if (!--cnt)
+				return rv;
+		}
+	}
+
+	/*
+	 * If we're here, then there's a signal pending. No need to read it,
+	 * we know we should quit now.
+	 */
+	close(ctx->sigfd);
+
+	return GPIOD_CTXLESS_EVENT_POLL_RET_STOP;
+}
+
+static void handle_event(struct mon_ctx *ctx, int event_type,
+			 unsigned int line_offset,
+			 const struct timespec *timestamp)
+{
+	if (!ctx->silent) {
+		if (ctx->fmt)
+			event_print_custom(line_offset, timestamp,
+					   event_type, ctx);
+		else
+			event_print_human_readable(line_offset,
+						   timestamp, event_type);
+	}
+
+	ctx->events_done++;
+}
+
+static int event_callback(int event_type, unsigned int line_offset,
+			  const struct timespec *timestamp, void *data)
+{
+	struct mon_ctx *ctx = data;
+
+	switch (event_type) {
+	case GPIOD_CTXLESS_EVENT_CB_RISING_EDGE:
+	case GPIOD_CTXLESS_EVENT_CB_FALLING_EDGE:
+		handle_event(ctx, event_type, line_offset, timestamp);
+		break;
+	default:
+		/*
+		 * REVISIT: This happening would indicate a problem in the
+		 * library.
+		 */
+		return GPIOD_CTXLESS_EVENT_CB_RET_OK;
+	}
+
+	if (ctx->events_wanted && ctx->events_done >= ctx->events_wanted)
+		return GPIOD_CTXLESS_EVENT_CB_RET_STOP;
+
+	return GPIOD_CTXLESS_EVENT_CB_RET_OK;
+}
+
+static int make_signalfd(void)
+{
+	sigset_t sigmask;
+	int sigfd, rv;
+
+	sigemptyset(&sigmask);
+	sigaddset(&sigmask, SIGTERM);
+	sigaddset(&sigmask, SIGINT);
+
+	rv = sigprocmask(SIG_BLOCK, &sigmask, NULL);
+	if (rv < 0)
+		die("error masking signals: %s", strerror(errno));
+
+	sigfd = signalfd(-1, &sigmask, 0);
+	if (sigfd < 0)
+		die("error creating signalfd: %s", strerror(errno));
+
+	return sigfd;
+}
+
+int main(int argc, char **argv)
+{
+	unsigned int offsets[GPIOD_LINE_BULK_MAX_LINES], num_lines = 0, offset;
+	bool active_low = false, watch_rising = false, watch_falling = false;
+	struct timespec timeout = { 10, 0 };
+	int optc, opti, rv, i, event_type;
+	struct mon_ctx ctx;
+	char *end;
+
+	memset(&ctx, 0, sizeof(ctx));
+
+	for (;;) {
+		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+		if (optc < 0)
+			break;
+
+		switch (optc) {
+		case 'h':
+			print_help();
+			return EXIT_SUCCESS;
+		case 'v':
+			print_version();
+			return EXIT_SUCCESS;
+		case 'l':
+			active_low = true;
+			break;
+		case 'n':
+			ctx.events_wanted = strtoul(optarg, &end, 10);
+			if (*end != '\0')
+				die("invalid number: %s", optarg);
+			break;
+		case 's':
+			ctx.silent = true;
+			break;
+		case 'r':
+			watch_rising = true;
+			break;
+		case 'f':
+			watch_falling = true;
+			break;
+		case 'b':
+			setlinebuf(stdout);
+			break;
+		case 'F':
+			ctx.fmt = optarg;
+			break;
+		case '?':
+			die("try %s --help", get_progname());
+		default:
+			abort();
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (watch_rising && !watch_falling)
+		event_type = GPIOD_CTXLESS_EVENT_RISING_EDGE;
+	else if (watch_falling && !watch_rising)
+		event_type = GPIOD_CTXLESS_EVENT_FALLING_EDGE;
+	else
+		event_type = GPIOD_CTXLESS_EVENT_BOTH_EDGES;
+
+	if (argc < 1)
+		die("gpiochip must be specified");
+
+	if (argc < 2)
+		die("at least one GPIO line offset must be specified");
+
+	for (i = 1; i < argc; i++) {
+		offset = strtoul(argv[i], &end, 10);
+		if (*end != '\0' || offset > INT_MAX)
+			die("invalid GPIO offset: %s", argv[i]);
+
+		offsets[i - 1] = offset;
+		num_lines++;
+	}
+
+	ctx.sigfd = make_signalfd();
+
+	rv = gpiod_ctxless_event_monitor_multiple(argv[0], event_type,
+						  offsets, num_lines,
+						  active_low, "gpiomon",
+						  &timeout, poll_callback,
+						  event_callback, &ctx);
+	if (rv)
+		die_perror("error waiting for events");
+
+	return EXIT_SUCCESS;
+}
diff --git a/tools/gpioset.c b/tools/gpioset.c
new file mode 100644
index 0000000..d9977a7
--- /dev/null
+++ b/tools/gpioset.c
@@ -0,0 +1,281 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <getopt.h>
+#include <limits.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/select.h>
+#include <sys/signalfd.h>
+#include <unistd.h>
+
+#include "tools-common.h"
+
+static const struct option longopts[] = {
+	{ "help",		no_argument,		NULL,	'h' },
+	{ "version",		no_argument,		NULL,	'v' },
+	{ "active-low",		no_argument,		NULL,	'l' },
+	{ "mode",		required_argument,	NULL,	'm' },
+	{ "sec",		required_argument,	NULL,	's' },
+	{ "usec",		required_argument,	NULL,	'u' },
+	{ "background",		no_argument,		NULL,	'b' },
+	{ GETOPT_NULL_LONGOPT },
+};
+
+static const char *const shortopts = "+hvlm:s:u:b";
+
+static void print_help(void)
+{
+	printf("Usage: %s [OPTIONS] <chip name/number> <offset1>=<value1> <offset2>=<value2> ...\n",
+	       get_progname());
+	printf("Set GPIO line values of a GPIO chip and maintain the state until the process exits\n");
+	printf("\n");
+	printf("Options:\n");
+	printf("  -h, --help:\t\tdisplay this message and exit\n");
+	printf("  -v, --version:\tdisplay the version and exit\n");
+	printf("  -l, --active-low:\tset the line active state to low\n");
+	printf("  -m, --mode=[exit|wait|time|signal] (defaults to 'exit'):\n");
+	printf("		tell the program what to do after setting values\n");
+	printf("  -s, --sec=SEC:\tspecify the number of seconds to wait (only valid for --mode=time)\n");
+	printf("  -u, --usec=USEC:\tspecify the number of microseconds to wait (only valid for --mode=time)\n");
+	printf("  -b, --background:\tafter setting values: detach from the controlling terminal\n");
+	printf("\n");
+	printf("Modes:\n");
+	printf("  exit:\t\tset values and exit immediately\n");
+	printf("  wait:\t\tset values and wait for user to press ENTER\n");
+	printf("  time:\t\tset values and sleep for a specified amount of time\n");
+	printf("  signal:\tset values and wait for SIGINT or SIGTERM\n");
+	printf("\n");
+	printf("Note: the state of a GPIO line controlled over the character device reverts to default\n");
+	printf("when the last process referencing the file descriptor representing the device file exits.\n");
+	printf("This means that it's wrong to run gpioset, have it exit and expect the line to continue\n");
+	printf("being driven high or low. It may happen if given pin is floating but it must be interpreted\n");
+	printf("as undefined behavior.\n");
+}
+
+struct callback_data {
+	/* Replace with a union once we have more modes using callback data. */
+	struct timeval tv;
+	bool daemonize;
+};
+
+static void maybe_daemonize(bool daemonize)
+{
+	int rv;
+
+	if (daemonize) {
+		rv = daemon(0, 0);
+		if (rv < 0)
+			die("unable to daemonize: %s", strerror(errno));
+	}
+}
+
+static void wait_enter(void *data GPIOD_UNUSED)
+{
+	getchar();
+}
+
+static void wait_time(void *data)
+{
+	struct callback_data *cbdata = data;
+
+	maybe_daemonize(cbdata->daemonize);
+	select(0, NULL, NULL, NULL, &cbdata->tv);
+}
+
+static void wait_signal(void *data)
+{
+	struct callback_data *cbdata = data;
+	struct pollfd pfd;
+	sigset_t sigmask;
+	int sigfd, rv;
+
+	sigemptyset(&sigmask);
+	sigaddset(&sigmask, SIGTERM);
+	sigaddset(&sigmask, SIGINT);
+
+	rv = sigprocmask(SIG_BLOCK, &sigmask, NULL);
+	if (rv < 0)
+		die("error blocking signals: %s", strerror(errno));
+
+	sigfd = signalfd(-1, &sigmask, 0);
+	if (sigfd < 0)
+		die("error creating signalfd: %s", strerror(errno));
+
+	memset(&pfd, 0, sizeof(pfd));
+	pfd.fd = sigfd;
+	pfd.events = POLLIN | POLLPRI;
+
+	maybe_daemonize(cbdata->daemonize);
+
+	for (;;) {
+		rv = poll(&pfd, 1, 1000 /* one second */);
+		if (rv < 0)
+			die("error polling for signals: %s", strerror(errno));
+		else if (rv > 0)
+			break;
+	}
+
+	/*
+	 * Don't bother reading siginfo - it's enough to know that we
+	 * received any signal.
+	 */
+	close(sigfd);
+}
+
+enum {
+	MODE_EXIT = 0,
+	MODE_WAIT,
+	MODE_TIME,
+	MODE_SIGNAL,
+};
+
+struct mode_mapping {
+	int id;
+	const char *name;
+	gpiod_ctxless_set_value_cb callback;
+};
+
+static const struct mode_mapping modes[] = {
+	[MODE_EXIT] = {
+		.id		= MODE_EXIT,
+		.name		= "exit",
+		.callback	= NULL,
+	},
+	[MODE_WAIT] = {
+		.id		= MODE_WAIT,
+		.name		= "wait",
+		.callback	= wait_enter,
+	},
+	[MODE_TIME] = {
+		.id		= MODE_TIME,
+		.name		= "time",
+		.callback	= wait_time,
+	},
+	[MODE_SIGNAL] = {
+		.id		= MODE_SIGNAL,
+		.name		= "signal",
+		.callback	= wait_signal,
+	},
+};
+
+static const struct mode_mapping *parse_mode(const char *mode)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(modes); i++)
+		if (strcmp(mode, modes[i].name) == 0)
+			return &modes[i];
+
+	return NULL;
+}
+
+int main(int argc, char **argv)
+{
+	const struct mode_mapping *mode = &modes[MODE_EXIT];
+	unsigned int *offsets, num_lines, i;
+	int *values, rv, optc, opti;
+	struct callback_data cbdata;
+	bool active_low = false;
+	char *device, *end;
+
+	memset(&cbdata, 0, sizeof(cbdata));
+
+	for (;;) {
+		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+		if (optc < 0)
+			break;
+
+		switch (optc) {
+		case 'h':
+			print_help();
+			return EXIT_SUCCESS;
+		case 'v':
+			print_version();
+			return EXIT_SUCCESS;
+		case 'l':
+			active_low = true;
+			break;
+		case 'm':
+			mode = parse_mode(optarg);
+			if (!mode)
+				die("invalid mode: %s", optarg);
+			break;
+		case 's':
+			cbdata.tv.tv_sec = strtoul(optarg, &end, 10);
+			if (*end != '\0')
+				die("invalid time value in seconds: %s", optarg);
+			break;
+		case 'u':
+			cbdata.tv.tv_usec = strtoul(optarg, &end, 10);
+			if (*end != '\0')
+				die("invalid time value in microseconds: %s",
+				    optarg);
+			break;
+		case 'b':
+			cbdata.daemonize = true;
+			break;
+		case '?':
+			die("try %s --help", get_progname());
+		default:
+			abort();
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (mode->id != MODE_TIME && (cbdata.tv.tv_sec || cbdata.tv.tv_usec))
+		die("can't specify wait time in this mode");
+
+	if (mode->id != MODE_SIGNAL &&
+	    mode->id != MODE_TIME &&
+	    cbdata.daemonize)
+		die("can't daemonize in this mode");
+
+	if (argc < 1)
+		die("gpiochip must be specified");
+
+	if (argc < 2)
+		die("at least one GPIO line offset to value mapping must be specified");
+
+	device = argv[0];
+
+	num_lines = argc - 1;
+
+	offsets = malloc(sizeof(*offsets) * num_lines);
+	values = malloc(sizeof(*values) * num_lines);
+	if (!values || !offsets)
+		die("out of memory");
+
+	for (i = 0; i < num_lines; i++) {
+		rv = sscanf(argv[i + 1], "%u=%d", &offsets[i], &values[i]);
+		if (rv != 2)
+			die("invalid offset<->value mapping: %s", argv[i + 1]);
+
+		if (values[i] != 0 && values[i] != 1)
+			die("value must be 0 or 1: %s", argv[i + 1]);
+
+		if (offsets[i] > INT_MAX)
+			die("invalid offset: %s", argv[i + 1]);
+	}
+
+	rv = gpiod_ctxless_set_value_multiple(device, offsets, values,
+					      num_lines, active_low, "gpioset",
+					      mode->callback, &cbdata);
+	if (rv < 0)
+		die_perror("error setting the GPIO line values");
+
+	free(offsets);
+	free(values);
+
+	return EXIT_SUCCESS;
+}
diff --git a/tools/tools-common.c b/tools/tools-common.c
new file mode 100644
index 0000000..a6e38e5
--- /dev/null
+++ b/tools/tools-common.c
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+/* Common code for GPIO tools. */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <libgen.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "tools-common.h"
+
+const char *get_progname(void)
+{
+	return program_invocation_name;
+}
+
+void die(const char *fmt, ...)
+{
+	va_list va;
+
+	va_start(va, fmt);
+	fprintf(stderr, "%s: ", program_invocation_name);
+	vfprintf(stderr, fmt, va);
+	fprintf(stderr, "\n");
+	va_end(va);
+
+	exit(EXIT_FAILURE);
+}
+
+void die_perror(const char *fmt, ...)
+{
+	va_list va;
+
+	va_start(va, fmt);
+	fprintf(stderr, "%s: ", program_invocation_name);
+	vfprintf(stderr, fmt, va);
+	fprintf(stderr, ": %s\n", strerror(errno));
+	va_end(va);
+
+	exit(EXIT_FAILURE);
+}
+
+void print_version(void)
+{
+	printf("%s (libgpiod) v%s\n",
+	       program_invocation_short_name, gpiod_version_string());
+	printf("Copyright (C) 2017-2018 Bartosz Golaszewski\n");
+	printf("License: LGPLv2.1\n");
+	printf("This is free software: you are free to change and redistribute it.\n");
+	printf("There is NO WARRANTY, to the extent permitted by law.\n");
+}
diff --git a/tools/tools-common.h b/tools/tools-common.h
new file mode 100644
index 0000000..ace3b46
--- /dev/null
+++ b/tools/tools-common.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
+ */
+
+#ifndef __GPIOD_TOOLS_COMMON_H__
+#define __GPIOD_TOOLS_COMMON_H__
+
+/*
+ * Various helpers for the GPIO tools.
+ *
+ * NOTE: This is not a stable interface - it's only to avoid duplicating
+ * common code.
+ */
+
+#define NORETURN		__attribute__((noreturn))
+#define PRINTF(fmt, arg)	__attribute__((format(printf, fmt, arg)))
+#define ARRAY_SIZE(x)		(sizeof(x) / sizeof(*(x)))
+
+#define GETOPT_NULL_LONGOPT	NULL, 0, NULL, 0
+
+const char *get_progname(void);
+void die(const char *fmt, ...) NORETURN PRINTF(1, 2);
+void die_perror(const char *fmt, ...) NORETURN PRINTF(1, 2);
+void print_version(void);
+
+#endif /* __GPIOD_TOOLS_COMMON_H__ */