diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,6 +1,6 @@
 -- This file is part of Hoppy.
 --
--- Copyright 2015-2021 Bryan Gardiner <bog@khumba.net>
+-- Copyright 2015-2024 Bryan Gardiner <bog@khumba.net>
 --
 -- This program is free software: you can redistribute it and/or modify
 -- it under the terms of the GNU Affero General Public License as published by
diff --git a/hoppy-docs.cabal b/hoppy-docs.cabal
--- a/hoppy-docs.cabal
+++ b/hoppy-docs.cabal
@@ -1,12 +1,12 @@
 name: hoppy-docs
-version: 0.8.0
+version: 0.9.0
 synopsis: C++ FFI generator - Documentation
-homepage: http://khumba.net/projects/hoppy
+homepage: https://khumba.net/projects/hoppy
 license: AGPL-3
 license-file: LICENSE
 author: Bryan Gardiner <bog@khumba.net>
 maintainer: Bryan Gardiner <bog@khumba.net>
-copyright: Copyright 2015-2021 Bryan Gardiner
+copyright: Copyright 2015-2024 Bryan Gardiner
 category: Foreign
 build-type: Simple
 cabal-version: 1.24
@@ -19,10 +19,10 @@
   exposed-modules:
       Foreign.Hoppy.Documentation.UsersGuide
   build-depends:
-      base >=4.7 && <5
+      base >=4.10 && <5
     , haskell-src >=1.0 && <1.1
-    , hoppy-generator >=0.8 && <0.9
-    , hoppy-runtime >=0.8 && <0.9
+    , hoppy-generator >=0.9 && <0.10
+    , hoppy-runtime >=0.9 && <0.10
   hs-source-dirs: src
   ghc-options: -W -fwarn-incomplete-patterns -fwarn-unused-do-bind
   default-language: Haskell2010
diff --git a/src/Foreign/Hoppy/Documentation/UsersGuide.hs b/src/Foreign/Hoppy/Documentation/UsersGuide.hs
--- a/src/Foreign/Hoppy/Documentation/UsersGuide.hs
+++ b/src/Foreign/Hoppy/Documentation/UsersGuide.hs
@@ -1,6 +1,6 @@
 -- This file is part of Hoppy.
 --
--- Copyright 2015-2021 Bryan Gardiner <bog@khumba.net>
+-- Copyright 2015-2024 Bryan Gardiner <bog@khumba.net>
 --
 -- This program is free software: you can redistribute it and/or modify
 -- it under the terms of the GNU Affero General Public License as published by
@@ -28,6 +28,9 @@
   -- ** Project setup
   -- $getting-started-project-setup
 
+  -- ** Alternate project structures
+  -- $getting-started-alternate-project-structures
+
   -- ** A first binding
   -- $getting-started-a-first-binding
 
@@ -129,21 +132,16 @@
 code generators, some runtime support for Haskell bindings, and interface
 definitions for the C++ standard library.
 
-Bindings using Hoppy have three Cabal packages:
-
-- A Haskell generator program (in @\/myproject-generator@) that knows the
-interface definition and generates code for the next two parts.
-
-- A C++ library (in @\/myproject-cpp@) that gets compiled into a shared object
-containing the C++ half of the bindings.
+Bindings using Hoppy can be structured many different ways, but the recommended
+approach is to use two Cabal packages:
 
-- A Haskell library (in @\/myproject@) that links against the C++ library and
-exposes the bindings.
+- A \"generator\" library that contains all of the binding definitions for your
+  project, and links to @hoppy-generator@ to be able to do code generation.
 
-The path names are suggested subdirectories of a project, and are used in this
-document, but are not required.  Despite the second being called a C++ library
-and containing C++ code, all three of these are Haskell packages, built using
-Cabal.
+- The actual binding library that includes the C++ and Haskell code generated by
+  the previous package, as well as any other wrapper logic you want, and does
+  the actual linking to C++.  This package calls the generator library early in
+  the build process, via a custom @Setup.hs@.
 
 -}
 {- $getting-started
@@ -154,39 +152,85 @@
 {- $getting-started-project-setup
 
 To set up a new Hoppy project, it's recommended to start with the template
-project in the @example\/@ directory.  You can find this in the Hoppy source
-repository:
+project in the @examples\/two-package-example\/@ directory.  You can find this
+in the Hoppy source repository:
 
-<https://gitlab.com/khumba/hoppy/tree/master/example>
+<https://git.sr.ht/~khumba/hoppy/tree/master/item/examples/two-package-example>
 
-This is a minimal project that defines a C++ function to reverse a @std::string@,
-exposes that to Haskell via a library, and provides a demo program that uses the
-library.  The @example\/install.sh@ script simply compiles and installs the
-generator, C++, and Haskell packages in turn.
+This is a minimal project that defines a C++ function to reverse a
+@std::string@, exposes that to Haskell via a library, and provides a demo
+program that uses the library.  The C++ standard library is linked to, but you
+can link to any C++ library you need this way.
 
-The generator package specifies the C++ interface to be exposed, using the
-functions and data types described in the rest of this section.
+The bundled example program reads lines from standard input, reverses the
+character in each one, and writes the result back to standard output.  Calling
+it like so produces the following output:
 
-The C++ package is mostly empty, primarily containing a @Setup.hs@ file that
-invokes Hoppy build hooks, and any C++ code of our own that we want to bind to,
-beyond what's provided by third-party libraries.  For this example, we add a
-@reverse()@ function and make use of the C++ standard library.  If we were
-writing a binding for an existing third-party library, we might not need to
-provide any C++ code ourselves here.  When building this package, Hoppy
-generates some C++ code and then relies on a Makefile we provide for linking it
-together with any code we provided (see @example\/example-cpp\/Makefile@).  If
-you are relying on a system library, you can link to it in the Makefile.
+> (echo one; echo two; echo three) | cabal v2-run hoppy-two-package-example
+>
+> eno
+> owt
+> eerht
 
-The Haskell package is even more empty than the C++ one.  It contains a similar
-@Setup.hs@ to invoke Hoppy.  Nothing else is included in the package's library,
-although you are free to add your own Haskell modules.  The executable ties
-everything together by calling the C++ code.  It reverses the characters of each
-input line it sees.
+There are two Cabal packages in this example.  The first, called
+@hoppy-two-package-example-generator@, specifies the C++ interface to be
+exposed, using the functions and data types described in the rest of this
+section, from the @hoppy-generator@ package.  All this package does is expose a
+top-level @Interface@ object to be used by the next package.
 
-To publish this project, one would upload all three packages to Hackage.  (But
-make sure to rename it first!)
+The second package, called @hoppy-two-package-example@, is the actual bindings.
+Consumers of your bindings will depend on this package (and @hoppy-runtime@
+which contains commonly-needed support functions) and ignore the generator
+package.
 
+There are a few parts to the binding package.  The first are the C++ files
+@example\/cpp\/utils.{cpp,hpp}@ which contain the @reverse()@ function that we
+are wrapping.  This demonstrates how to provide additional C++ code in your
+bindings, although this is not required if you have no logic to add.
+
+The @hoppy-two-package-example.cabal@ file for this project uses @build-type:
+Custom@ with a nondefault @Setup.hs@.  Our @Setup.hs@ invokes the generator at
+configure time, to generate C++ and Haskell glue code that can talk to each
+other.  To do this, we need the @custom-setup@ block to declare that our
+@Setup.hs@ needs our generator, along with @hoppy-generator@ and
+@hoppy-runtime@.  The generator will produce two modules, which are declared in
+the library's @exposed-modules@ and @autogen-modules@ lists.  The generator also
+produces some C++ files that we need to include under @cxx-sources@ (albeit
+indirectly).  In the @Setup.hs@ itself, rather than using the standard @main =
+defaultMain@, we call into the Hoppy generator, passing in the interface
+definition, plus some directory names so that Hoppy knows where to find files.
+
+To publish this project, one would upload both packages to Hackage.  Make sure
+to rename them first!
+
 -}
+{- $getting-started-alternate-project-structures
+
+It is also possible to structure your bindings as one, or three, packages.  One
+package can be achieved by writing the Hoppy interface definitions directly in
+the @Setup.hs@ of the binding package.  The downside to this is that the entire
+interface must be specified in one file, but this may be suitable for small
+APIs.
+
+Three packages can be used if the C++ building infrastructure provided by
+@cxx-sources@ isn't enough, and you need to use a more general build system
+(make, CMake, etc.) and have full control over the build process.  Here, the
+binding package is split into two separate C++ and Haskell packages.  The C++
+binding package uses @Setup.hs@ to generate C++ code and run the desired C++
+build process, and install the resulting shared object, but contains no Haskell
+modules.  The Haskell binding package then contains the generated Haskell code,
+but only links to the shared objects from the C++ binding package.
+
+Using three packages is the traditional way to use Hoppy and worked before Cabal
+added @cxx-sources@.  It's the method used by Qtah, which needs significant
+custom build logic, but also demonstrates what can be done, for example dynamic
+version detection of Qt, and further wrapping of all generated modules to make
+identifiers shorter, add typeclass instances, etc.
+
+Examples containing one, two, and three packages are present in the @examples/@
+directory of the Hoppy repository.
+
+-}
 {- $getting-started-a-first-binding
 
 A complete C++ API is specified using Haskell data structures in
@@ -350,14 +394,25 @@
 -}
 {- $getting-started-types
 
-Let's take a break from @std::string@ for a moment and talk about how we
-represent data types in Hoppy.
+Let's take a break from @std::string@ and talk about how we represent data types
+in Hoppy.
 
 All C++ types are represented with the 'Type' data type, values of which are in
 the "Foreign.Hoppy.Generator.Types" module.  This includes primitive numeric
 types, object types, function types, pointers and references, @void@, the const
-qualifier, etc.
+qualifier, etc.  'Type's tell Hoppy how to perform conversions when Haskell code
+calls C++ code or vice versa.
 
+The association between C++ types and 'Type's is not necessarily one-to-one.
+For a single C++ type, it is perfectly okay to use 'Type's that specify
+different conversions in different situations.  A simple example would be two
+different functions that return pointers to objects of a particular class, but
+one function transfers ownership to the caller, so using 'toGcT' would make
+sense.  A more complicated example would be a function that returns an object
+pointer, where in one situation you want to pass the pointer back to Haskell
+directly, and in another situation you want the object converted to a Haskell
+record.
+
 A Hoppy 'Type' value has a corresponding C++ type, and also what we refer to as
 a C type, and possibly a Haskell type.  The C++ type is, of course, whatever C++
 type the 'Type' structure represents.  The Haskell type is the Haskell data type
@@ -812,7 +867,7 @@
 And a method binding:
 
 @
-'makeFn' "reverse" Nothing 'Nonpure' ['objT' c_string] $ 'objT' c_string
+'makeFn' \"reverse\" Nothing 'Nonpure' ['objT' c_string] $ 'objT' c_string
 @
 
 We get the following Haskell function and instance:
@@ -1008,7 +1063,7 @@
 allocate our G instance on the heap, and create a second class F that holds a
 @shared_ptr\<G>@ and whose @operator()@ calls through to G.
 
-This way, the existance of the F and G objects are invisible to the foreign
+This way, the existence of the F and G objects are invisible to the foreign
 language, and (for now) passing these callbacks back to the foreign language is
 not supported.
 
@@ -1073,9 +1128,9 @@
 alignment :: 'CppEnum'
 alignment =
   'makeAutoEnum' ('ident' \"Alignment\") Nothing 'EnumUnscoped'
-  [ "LeftAlign"
-  , "CenterAlign"
-  , "RightAlign"
+  [ \"LeftAlign\"
+  , \"CenterAlign\"
+  , \"RightAlign\"
   ]
 @
 
@@ -1363,15 +1418,17 @@
 Catching a wildcard (i.e. @catch (...)@) is supported, but no information is
 available about the caught value.
 
-Implementation-wise, an in-flight C++ exception in Haskell always owns the
-object (which is on the heap).  An exception coming from C++ into Haskell (it's
-a heap temporary) will be given to the garbage collector.  Hence, for ease of
-use, caught exceptions should always be garbage-collected.  Also, when throwing
-from Haskell, throwing will always take ownership of the object.  If 'throwCpp'
-gets a non-GCed object, then it will be given to the garbage collector; and then
-the exception will be thrown as a Haskell exception.  If the exception
-propagates out to a callback and back into C++, then a temporary non-GCed copy
-will be passed over the gateway, and rethrown as a value object on the C++ side.
+Implementation-wise, in-flight C++ exceptions in Haskell are always owned by the
+throwing machinery, and the exception object always lives on the C++ heap.  An
+exception being thrown from C++ into Haskell (a heap temporary) will be given to
+the garbage collector, for ease of use, so do not delete it manually.
+
+In the other direction, when throwing a C++ exception from Haskell, throwing
+again takes ownership of the object.  If 'throwCpp' gets a non-GCed object, then
+it will be given to the garbage collector; and then the exception will be thrown
+as a Haskell exception.  If the exception propagates out to a callback and back
+into C++, then a temporary non-GCed copy will be passed over the gateway, and
+rethrown as a value object on the C++ side.
 
 In the above strategy, when throwing an exception from Haskell that propagates
 to C++, it is wasteful to make the thrown object GCed, just to have to create a
