changeset 171:e27c5c667a5a

Create full new types for return code and flags, separate end to end. This plumbs the ReturnCode and RawFlags types through the places where we call into or are called from PAM. Also adds Sun documentation to the project.
author Paul Fisher <paul@pfish.zone>
date Fri, 25 Jul 2025 20:52:14 -0400
parents f052e2417195
children 6727cbe56f4a
files Cargo.toml build.rs src/_doc.rs src/constants.rs src/handle.rs src/lib.rs src/libpam/conversation.rs src/libpam/handle.rs src/libpam/mod.rs src/libpam/module.rs src/logging.rs src/module.rs testharness/src/bin/testharness.rs testharness/src/lib.rs
diffstat 14 files changed, 619 insertions(+), 444 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.toml	Wed Jul 16 18:45:20 2025 -0400
+++ b/Cargo.toml	Fri Jul 25 20:52:14 2025 -0400
@@ -21,17 +21,22 @@
 rust-version.workspace = true
 
 [features]
-default = ["link", "basic-ext"]
+default = ["link"]
 
 # Enable this to actually link against your system's PAM library.
 #
 # This will fail if you have extensions enabled that are not compatible
 # with your system's PAM.
-link = ["libpam-sys"]
+link = ["dep:libpam-sys"]
 
+# Extensions to PAM that are shared by Linux-PAM and OpenPAM
+# (i.e., most PAM installations).
 basic-ext = []
+# Extensions to PAM that are supported by Linux-PAM.
 linux-pam-ext = []
+# Extensions to PAM that are supported by OpenPAM.
 openpam-ext = []
+# Extensions to PAM that are supported by Sun's PAM.
 sun-ext = []
 
 # This feature exists only for testing.
@@ -43,7 +48,6 @@
 num_enum = "0.7.3"
 libpam-sys = { optional = true, path = "libpam-sys" }
 libpam-sys-helpers = { path = "libpam-sys/libpam-sys-helpers" }
-libpam-sys-consts = { path = "libpam-sys/libpam-sys-consts" }
 
 [build-dependencies]
 libpam-sys-consts = { path = "libpam-sys/libpam-sys-consts" }
--- a/build.rs	Wed Jul 16 18:45:20 2025 -0400
+++ b/build.rs	Fri Jul 25 20:52:14 2025 -0400
@@ -1,5 +1,3 @@
-use libpam_sys_consts::pam_impl;
-
 fn main() {
-    pam_impl::enable_pam_impl_cfg()
+    libpam_sys_consts::pam_impl::enable_pam_impl_cfg()
 }
--- a/src/_doc.rs	Wed Jul 16 18:45:20 2025 -0400
+++ b/src/_doc.rs	Fri Jul 25 20:52:14 2025 -0400
@@ -31,7 +31,7 @@
         )
     };
     ($func:ident: _std$(, $rest:ident)*) => {
-        $crate::_doc::linklist!($func: man7, manbsd, xsso$(, $rest)*)
+        $crate::_doc::linklist!($func: man7, manbsd, mansun, xsso$(, $rest)*)
     };
     ($func:ident: man7$(, $rest:ident)*) => {
         concat!(
@@ -45,6 +45,12 @@
             $crate::_doc::linklist!($func: $($rest),*)
         )
     };
+    ($func:ident: mansun$(, $rest:ident)*) => {
+        concat!(
+            "- [Illumos manpage for `", stringify!($func), "`][mansun]\n",
+            $crate::_doc::linklist!($func: $($rest),*)
+        )
+    };
     ($func:ident: xsso$(, $rest:ident)*) => {
         concat!(
             "- [X/SSO spec for `", stringify!($func), "`][xsso]",
@@ -131,6 +137,40 @@
     };
 }
 
+/// Generates a Markdown link reference to the SmartOS man pages.
+///
+/// # Examples
+///
+/// ```ignore
+/// # use nonstick::_doc::mansun;
+/// // Both of these formulations create a link named `manbsd`.
+/// #[doc = mansun!(3pam fn_name)]
+/// #[doc = mansun!(5 "a.out" "synopsis")]
+/// // This one creates a link named `link_name`.
+/// #[doc = mansun!(link_name: 1 alias "examples")]
+/// # fn do_whatever() {}
+/// ```
+macro_rules! mansun {
+    ($n:literal $func:ident $($anchor:literal)?) => {
+        $crate::_doc::mansun!(mansun: [$n ""] $func $($anchor)?)
+    };
+    ([$n:literal $sect:literal] $func:ident $($anchor:literal)?) => {
+        $crate::_doc::mansun!(mansun: [$n $sect] $func $($anchor)?)
+    };
+    ($name:ident: $n:literal $func:ident $($anchor:literal)?) => {
+        $crate::_doc::mansun!($name: [$n ""] $func $($anchor)?)
+    };
+    ($name:ident: [$n:literal $sect:literal] $func:ident $($anchor:literal)?) => {
+        $crate::_doc::mansun!($name: [$n $sect] (stringify!($func)) $($anchor)?)
+    };
+    ($name:ident: [$n:literal $sect:literal] ($func:expr) $($anchor:literal)?) => {
+        concat!("[", stringify!($name), "]: ",
+            "https://smartos.org/man/", $n, $sect, "/", $func,
+            $("#", $anchor)?
+        )
+    };
+}
+
 /// Generates a Markdown link reference to the X/SSO specification.
 ///
 /// # Examples
@@ -175,8 +215,9 @@
         concat!(
             $crate::_doc::man7!($n $func), "\n",
             $crate::_doc::manbsd!($n $func), "\n",
+            $crate::_doc::mansun!([$n "pam"] $func), "\n",
             $crate::_doc::xsso!($func))
     };
 }
 
-pub(crate) use {guide, linklist, man7, manbsd, stdlinks, xsso};
+pub(crate) use {guide, linklist, man7, manbsd, mansun, stdlinks, xsso};
--- a/src/constants.rs	Wed Jul 16 18:45:20 2025 -0400
+++ b/src/constants.rs	Fri Jul 25 20:52:14 2025 -0400
@@ -1,157 +1,109 @@
 //! Constants and enum values from the PAM library.
 
-use crate::_doc::{linklist, man7, manbsd, xsso};
+use crate::_doc::{linklist, man7, manbsd, mansun, xsso};
 use bitflags::bitflags;
-use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::error::Error;
 use std::ffi::c_int;
 use std::fmt;
 use std::fmt::{Display, Formatter};
 use std::result::Result as StdResult;
 
-#[cfg(features = "link")]
-use libpam_sys_consts::constants as pam_constants;
-
-
-/// The union of constants available in all versions of PAM.
-/// The values here are fictitious and should not be used.
-#[cfg(not(features = "link"))]
-mod pam_constants {
-    pub const PAM_SUCCESS: i32 = 0;
-
-    /// Generates a sequence of values.
-    macro_rules! c_enum {
-        ($first:ident = $value:expr, $($rest:ident,)*) => {
-            c_enum!(($value) $first, $($rest,)*);
-        };
-        (($value:expr) $first:ident, $($rest:ident,)*) => {
-            pub const $first: i32 = $value;
-            c_enum!(($value+1) $($rest,)*);
-        };
-        (($value:expr)) => {};
-    }
-
-    // Since all these values are fictitious, we can start them wherever.
-    // All the items.
-    c_enum!(
-        PAM_SERVICE = 64,
-        PAM_USER,
-        PAM_TTY,
-        PAM_RHOST,
-        PAM_CONV,
-        PAM_AUTHTOK,
-        PAM_OLDAUTHTOK,
-        PAM_RUSER,
-        PAM_USER_PROMPT,
-        // Linux-only items.
-        PAM_FAIL_DELAY,
-        PAM_XDISPLAY,
-        PAM_XAUTHDATA,
-        PAM_AUTHTOK_TYPE,
-        // OpenPAM-only items.
-        PAM_REPOSITORY,
-        PAM_AUTHTOK_PROMPT,
-        PAM_OLDAUTHTOK_PROMPT,
-        PAM_HOST,
-        // Sun-only items.
-        PAM_RESOURCE,
-        PAM_AUSER,
-    );
-
-    // Prompt types.
-    c_enum!(
-        PAM_PROMPT_ECHO_OFF = 96,
-        PAM_PROMPT_ECHO_ON,
-        PAM_ERROR_MSG,
-        PAM_TEXT_INFO,
-        PAM_RADIO_TYPE,
-        PAM_BINARY_PROMPT,
-    );
+macro_rules! wrapper {
+    (
+        $(#[$m:meta])*
+        $viz:vis $name:ident($wraps:ty);
+    ) => {
+        $(#[$m])*
+        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
+        #[repr(transparent)]
+        $viz struct $name(i32);
 
-    // Errors.
-    c_enum!(
-        PAM_OPEN_ERR = 128,
-        PAM_SYMBOL_ERR,
-        PAM_SERVICE_ERR,
-        PAM_SYSTEM_ERR,
-        PAM_BUF_ERR,
-        PAM_PERM_DENIED,
-        PAM_AUTH_ERR,
-        PAM_CRED_INSUFFICIENT,
-        PAM_AUTHINFO_UNAVAIL,
-        PAM_USER_UNKNOWN,
-        PAM_MAXTRIES,
-        PAM_NEW_AUTHTOK_REQD,
-        PAM_ACCT_EXPIRED,
-        PAM_SESSION_ERR,
-        PAM_CRED_UNAVAIL,
-        PAM_CRED_EXPIRED,
-        PAM_CRED_ERR,
-        PAM_NO_MODULE_DATA,
-        PAM_CONV_ERR,
-        PAM_AUTHTOK_ERR,
-        PAM_AUTHTOK_RECOVERY_ERR,
-        PAM_AUTHTOK_LOCK_BUSY,
-        PAM_AUTHTOK_DISABLE_AGING,
-        PAM_TRY_AGAIN,
-        PAM_IGNORE,
-        PAM_ABORT,
-        PAM_AUTHTOK_EXPIRED,
-        PAM_MODULE_UNKNOWN,
-        PAM_BAD_ITEM,
-        PAM_CONV_AGAIN,
-        PAM_INCOMPLETE,
-        // OpenPAM-only errors.
-        PAM_DOMAIN_UNKNOWN,
-        PAM_BAD_HANDLE,
-        PAM_BAD_FEATURE,
-        PAM_BAD_CONSTANT,
-    );
-
-    macro_rules! flag_enum {
-        ($first:ident = $value:expr, $($rest:ident,)*) => {
-            flag_enum!(($value) $first, $($rest,)*);
-        };
-        (($value:expr) $first:ident, $($rest:ident,)*) => {
-            pub const $first: i32 = $value;
-            flag_enum!(($value*2) $($rest,)*);
-        };
-        (($value:expr)) => {};
+        impl From<i32> for $name {
+            fn from(value: i32) -> Self {
+                Self(value)
+            }
+        }
+        impl From<$name> for i32 {
+            fn from(value: $name) -> Self {
+                value.0
+            }
+        }
     }
-
-    flag_enum!(
-        PAM_SILENT = 256,
-        PAM_DISALLOW_NULL_AUTHTOK,
-        PAM_ESTABLISH_CRED,
-        PAM_DELETE_CRED,
-        PAM_REINITIALIZE_CRED,
-        PAM_REFRESH_CRED,
-
-        PAM_CHANGE_EXPIRED_AUTHTOK,
-
-        PAM_PRELIM_CHECK,
-        PAM_UPDATE_AUTHTOK,
-        PAM_DATA_REPLACE,
-        PAM_DATA_SILENT,
-    );
 }
 
-/// Creates a bitflags! macro, with an extra SILENT element.
+wrapper! {
+    /// Type of the flags that PAM passes to us (or that we pass to PAM).
+    pub RawFlags(c_int);
+}
+wrapper! {
+    /// The error code that we return to PAM.
+    pub ReturnCode(c_int);
+}
+
+impl ReturnCode {
+    /// A successful return.
+    pub const SUCCESS: Self = Self(0);
+}
+
 macro_rules! pam_flags {
     (
         $(#[$m:meta])*
         $name:ident {
-            $($inner:tt)*
+            $(
+                $(#[$m_ident:ident $($m_arg:tt)*])*
+                const $item_name:ident = (link = $value_value:expr, else = $other_value:expr);
+            )*
         }
     ) => {
         bitflags! {
+            #[derive(Clone, Copy, Debug, Default, PartialEq)]
             $(#[$m])*
-            #[derive(Clone, Copy, Debug, Default, PartialEq)]
-            #[repr(transparent)]
-            pub struct $name: c_int {
-                /// The module should not generate any messages.
-                const SILENT = pam_constants::PAM_SILENT;
-                $($inner)*
+            pub struct $name: u16 {
+                $(
+                    $(#[$m_ident $($m_arg)*])*
+                    const $item_name = $other_value;
+                )*
+            }
+        }
+
+        #[cfg(feature = "link")]
+        impl From<RawFlags> for $name {
+            #[allow(unused_doc_comments)]
+            fn from(value: RawFlags) -> Self {
+                eprintln!(concat!(stringify!($name), " FROM RAW FLAGS"));
+                let value: c_int = value.into();
+                let result = Self::empty();
+                $(
+                    $(#[$m_ident $($m_arg)*])*
+                    let result = result | if value & $value_value == 0 {
+                        eprintln!(concat!("checked against ", stringify!($value_value)));
+                        Self::empty()
+                    } else {
+                        eprintln!(concat!("checked against ", stringify!($value_value), " success"));
+                        Self::$item_name
+                    };
+                )*
+                result
+            }
+        }
+
+        #[cfg(feature = "link")]
+        impl From<$name> for RawFlags {
+            #[allow(unused_doc_comments)]
+            fn from(value: $name) -> Self {
+                eprintln!(concat!("RAW FLAGS FROM ", stringify!($name)));
+                let result = 0;
+                $(
+                    $(#[$m_ident $($m_arg)*])*
+                    let result = result | if value.contains($name::$item_name) {
+                        eprintln!(concat!("checked against ", stringify!($item_name), " success"));
+                        $value_value
+                    } else {
+                        eprintln!(concat!("checked against ", stringify!($item_name)));
+                        0
+                    };
+                )*
+                Self(result)
             }
         }
     }
@@ -160,30 +112,39 @@
 pam_flags! {
     /// Flags for authentication and account management.
     AuthnFlags {
+        /// The PAM module should not generate any messages.
+        const SILENT = (link = libpam_sys::PAM_SILENT, else = 0x8000);
+
         /// The module should return [AuthError](ErrorCode::AuthError)
         /// if the user has an empty authentication token, rather than
         /// allowing them to log in.
-        const DISALLOW_NULL_AUTHTOK = pam_constants::PAM_DISALLOW_NULL_AUTHTOK;
+        const DISALLOW_NULL_AUTHTOK = (link = libpam_sys::PAM_DISALLOW_NULL_AUTHTOK, else = 0b1);
     }
 }
 
 pam_flags! {
     /// Flags for changing the authentication token.
     AuthtokFlags {
+        /// The PAM module should not generate any messages.
+        const SILENT = (link = libpam_sys::PAM_SILENT, else = 0x8000);
+
         /// Indicates that the user's authentication token should
         /// only be changed if it is expired. If not passed,
         /// the authentication token should be changed unconditionally.
-        const CHANGE_EXPIRED_AUTHTOK = pam_constants::PAM_CHANGE_EXPIRED_AUTHTOK;
+        const CHANGE_EXPIRED_AUTHTOK = (link = libpam_sys::PAM_CHANGE_EXPIRED_AUTHTOK, else = 0b10);
 
         /// Don't check if the password is any good (Sun only).
         #[cfg(pam_impl = "Sun")]
-        const NO_AUTHTOK_CHECK = pam_constants::PAM_NO_AUTHTOK_CHECK;
+        const NO_AUTHTOK_CHECK = (link = libpam_sys::PAM_NO_AUTHTOK_CHECK, else = 0b100);
     }
 }
 
 pam_flags! {
     /// Common flag(s) shared by all PAM actions.
-    BaseFlags {}
+    BaseFlags {
+        /// The PAM module should not generate any messages.
+        const SILENT = (link = libpam_sys::PAM_SILENT, else = 0x8000);
+    }
 }
 
 #[cfg(feature = "openpam-ext")]
@@ -197,29 +158,53 @@
         $name:ident {
             $(
                 $(#[$item_m:meta])*
-                $item_name:ident = $item_value:expr,
+                $item_name:ident = $item_value:path,
             )*
         }
     ) => {
         $(#[$m])*
-        #[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)]
-        #[repr(i32)]
+        #[derive(Clone, Copy, Debug, PartialEq)]
         pub enum $name {
             $(
                 $(#[$item_m])*
-                $item_name = $item_value,
+                $item_name,
             )*
         }
 
+        #[cfg(feature = "link")]
+        impl TryFrom<RawFlags> for $name {
+            type Error = ErrorCode;
+            fn try_from(value: RawFlags) -> Result<$name> {
+                match value.0 {
+                    $(
+                        $item_value => Ok(Self::$item_name),
+                    )*
+                    _ => Err(BAD_CONST),
+                }
+            }
+        }
+
+        #[cfg(feature = "link")]
+        impl From<$name> for RawFlags {
+            fn from(value: $name) -> Self {
+                match value {
+                    $(
+                        $name::$item_name => $item_value.into(),
+                    )*
+                }
+            }
+        }
+
+        #[cfg(feature = "link")]
         impl $name {
             const ALL_VALUES: i32 = 0 $( | $item_value)*;
 
-            fn split(value: i32) -> Result<(Option<Self>, i32)> {
-                let me = value & Self::ALL_VALUES;
-                let them = value & !Self::ALL_VALUES;
-                let me = match me {
-                    0 => None,
-                    n => Some(Self::try_from(n).map_err(|_| BAD_CONST)?),
+            fn split(value: RawFlags) -> Result<(Option<Self>, RawFlags)> {
+                let me = value.0 & Self::ALL_VALUES;
+                let them = (value.0 & !Self::ALL_VALUES).into();
+                let me = match RawFlags(me) {
+                    RawFlags(0) => None,
+                    other => Some(Self::try_from(other).map_err(|_| BAD_CONST)?),
                 };
                 Ok((me, them))
             }
@@ -231,21 +216,21 @@
     /// The credential management action that should take place.
     CredAction {
         /// Set the user's credentials from this module. Default if unspecified.
-        Establish = pam_constants::PAM_ESTABLISH_CRED,
+        Establish = libpam_sys::PAM_ESTABLISH_CRED,
         /// Revoke the user's credentials established by this module.
-        Delete = pam_constants::PAM_DELETE_CRED,
+        Delete = libpam_sys::PAM_DELETE_CRED,
         /// Fully reinitialize the user's credentials from this module.
-        Reinitialize = pam_constants::PAM_REINITIALIZE_CRED,
+        Reinitialize = libpam_sys::PAM_REINITIALIZE_CRED,
         /// Extend the lifetime of the user's credentials from this module.
-        Refresh = pam_constants::PAM_REFRESH_CRED,
+        Refresh = libpam_sys::PAM_REFRESH_CRED,
     }
 }
 
+#[cfg(feature = "link")]
 impl CredAction {
     /// Separates this enum from the remaining [`BaseFlags`].
-    pub fn extract(value: i32) -> Result<(Self, BaseFlags)> {
-        Self::split(value)
-            .map(|(act, rest)| (act.unwrap_or_default(), BaseFlags::from_bits_retain(rest)))
+    pub(crate) fn extract(value: RawFlags) -> Result<(Self, BaseFlags)> {
+        Self::split(value).map(|(act, rest)| (act.unwrap_or_default(), BaseFlags::from(rest)))
     }
 }
 
@@ -257,148 +242,200 @@
 
 flag_enum! {
     AuthtokAction {
-        /// This is a preliminary call to check if we're ready to change passwords
-        /// and that the new password is acceptable.
-        PreliminaryCheck = pam_constants::PAM_PRELIM_CHECK,
-        /// You should actually update the password.
-        Update = pam_constants::PAM_UPDATE_AUTHTOK,
+        /// On this call, just validate that the password is acceptable
+        /// and that you have all the resources you need to change it.
+        ///
+        /// This corresponds to the constant `PAM_PRELIM_CHECK`.
+        Validate = libpam_sys::PAM_PRELIM_CHECK,
+        /// Actually perform the update.
+        ///
+        /// This corresponds to the constant `PAM_UPDATE_AUTHTOK`.
+        Update = libpam_sys::PAM_UPDATE_AUTHTOK,
     }
 }
 
+#[cfg(feature = "link")]
 impl AuthtokAction {
     /// Separates this enum from the remaining [`AuthtokFlags`].
-    pub fn extract(value: i32) -> Result<(Self, AuthtokFlags)> {
+    pub(crate) fn extract(value: RawFlags) -> Result<(Self, AuthtokFlags)> {
         match Self::split(value)? {
-            (Some(act), rest) => Ok((act, AuthtokFlags::from_bits_retain(rest))),
+            (Some(act), rest) => Ok((act, AuthtokFlags::from(rest))),
             (None, _) => Err(BAD_CONST),
         }
     }
 }
 
-/// The PAM error return codes.
-///
-/// These are returned by most PAM functions if an error of some kind occurs.
-///
-/// Instead of being an error code, success is represented by an Ok [`Result`].
-///
-/// # References
-///
-#[doc = linklist!(pam: man7, manbsd)]
-/// - [X/SSO error code specification][xsso]
-///
-#[doc = man7!(3 pam "RETURN_VALUES")]
-#[doc = manbsd!(3 pam "RETURN%20VALUES")]
-#[doc = xsso!("chap5.htm#tagcjh_06_02")]
-#[allow(non_camel_case_types, dead_code)]
-#[derive(Copy, Clone, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)]
-#[non_exhaustive] // C might give us anything!
-#[repr(i32)]
-pub enum ErrorCode {
-    OpenError = pam_constants::PAM_OPEN_ERR,
-    SymbolError = pam_constants::PAM_SYMBOL_ERR,
-    ServiceError = pam_constants::PAM_SERVICE_ERR,
-    SystemError = pam_constants::PAM_SYSTEM_ERR,
-    BufferError = pam_constants::PAM_BUF_ERR,
-    PermissionDenied = pam_constants::PAM_PERM_DENIED,
-    AuthenticationError = pam_constants::PAM_AUTH_ERR,
-    CredentialsInsufficient = pam_constants::PAM_CRED_INSUFFICIENT,
-    AuthInfoUnavailable = pam_constants::PAM_AUTHINFO_UNAVAIL,
-    UserUnknown = pam_constants::PAM_USER_UNKNOWN,
-    MaxTries = pam_constants::PAM_MAXTRIES,
-    NewAuthTokRequired = pam_constants::PAM_NEW_AUTHTOK_REQD,
-    AccountExpired = pam_constants::PAM_ACCT_EXPIRED,
-    SessionError = pam_constants::PAM_SESSION_ERR,
-    CredentialsUnavailable = pam_constants::PAM_CRED_UNAVAIL,
-    CredentialsExpired = pam_constants::PAM_CRED_EXPIRED,
-    CredentialsError = pam_constants::PAM_CRED_ERR,
-    NoModuleData = pam_constants::PAM_NO_MODULE_DATA,
-    ConversationError = pam_constants::PAM_CONV_ERR,
-    AuthTokError = pam_constants::PAM_AUTHTOK_ERR,
-    AuthTokRecoveryError = pam_constants::PAM_AUTHTOK_RECOVERY_ERR,
-    AuthTokLockBusy = pam_constants::PAM_AUTHTOK_LOCK_BUSY,
-    AuthTokDisableAging = pam_constants::PAM_AUTHTOK_DISABLE_AGING,
-    TryAgain = pam_constants::PAM_TRY_AGAIN,
-    Ignore = pam_constants::PAM_IGNORE,
-    Abort = pam_constants::PAM_ABORT,
-    AuthTokExpired = pam_constants::PAM_AUTHTOK_EXPIRED,
-    #[cfg(feature = "basic-ext")]
-    ModuleUnknown = pam_constants::PAM_MODULE_UNKNOWN,
-    #[cfg(feature = "basic-ext")]
-    BadItem = pam_constants::PAM_BAD_ITEM,
-    #[cfg(feature = "linux-pam-ext")]
-    ConversationAgain = pam_constants::PAM_CONV_AGAIN,
-    #[cfg(feature = "linux-pam-ext")]
-    Incomplete = pam_constants::PAM_INCOMPLETE,
-    #[cfg(feature = "openpam-ext")]
-    DomainUnknown = pam_constants::PAM_DOMAIN_UNKNOWN,
-    #[cfg(feature = "openpam-ext")]
-    BadHandle = pam_constants::PAM_BAD_HANDLE,
-    #[cfg(feature = "openpam-ext")]
-    BadFeature = pam_constants::PAM_BAD_FEATURE,
-    #[cfg(feature = "openpam-ext")]
-    BadConstant = pam_constants::PAM_BAD_CONSTANT,
+/// Constructs an enum which has the values if it's linked
+macro_rules! linky_enum {
+    (
+        $(#[$om:meta])*
+        pub enum $name:ident($wrap:ty) {
+            $(
+                $(#[$im:meta])*
+                $key:ident = $value:path,
+            )*
+        }
+    ) => {
+        $(#[$om])*
+        #[derive(Copy, Clone, Debug, PartialEq, Eq)]
+        pub enum $name {
+            $(
+                $(#[$im])*
+                $key,
+            )*
+        }
+
+        #[cfg(feature = "link")]
+        impl TryFrom<$wrap> for $name {
+            type Error = ErrorCode;
+            fn try_from(value: $wrap) -> Result<Self> {
+                match value.into() {
+                    $(
+                        $(#[$im])*
+                        $value => Ok(Self::$key),
+                    )*
+                    _ => Err(BAD_CONST),
+                }
+            }
+        }
+
+        #[cfg(feature = "link")]
+        impl From<$name> for $wrap {
+            fn from(value: $name) -> Self {
+                match value {
+                    $(
+                        $(#[$im])*
+                        $name::$key => $value.into(),
+                    )*
+                }
+            }
+        }
+    }
+}
+
+linky_enum! {
+    /// The PAM error return codes.
+    ///
+    /// These are returned by most PAM functions if an error of some kind occurs.
+    ///
+    /// Instead of being an error code, success is represented by an Ok [`Result`].
+    ///
+    /// **Do not depend upon the numerical value of these error codes,
+    /// or the enum's representation type.
+    /// The available codes and their values will vary depending upon
+    /// PAM implementation.**
+    ///
+    /// # References
+    ///
+    #[doc = linklist!(pam: man7, manbsd, mansun)]
+    /// - [X/SSO error code specification][xsso]
+    ///
+    #[doc = man7!(3 pam "RETURN_VALUES")]
+    #[doc = manbsd!(3 pam "RETURN%20VALUES")]
+    #[doc = mansun!([3 "pam"] pam "return-values")]
+    #[doc = xsso!("chap5.htm#tagcjh_06_02")]
+    #[allow(non_camel_case_types, dead_code)]
+    #[non_exhaustive] // Different PAMs have different error code sets.
+    pub enum ErrorCode(ReturnCode) {
+        OpenError = libpam_sys::PAM_OPEN_ERR,
+        SymbolError = libpam_sys::PAM_SYMBOL_ERR,
+        ServiceError = libpam_sys::PAM_SERVICE_ERR,
+        SystemError = libpam_sys::PAM_SYSTEM_ERR,
+        BufferError = libpam_sys::PAM_BUF_ERR,
+        PermissionDenied = libpam_sys::PAM_PERM_DENIED,
+        AuthenticationError = libpam_sys::PAM_AUTH_ERR,
+        CredentialsInsufficient = libpam_sys::PAM_CRED_INSUFFICIENT,
+        AuthInfoUnavailable = libpam_sys::PAM_AUTHINFO_UNAVAIL,
+        UserUnknown = libpam_sys::PAM_USER_UNKNOWN,
+        MaxTries = libpam_sys::PAM_MAXTRIES,
+        NewAuthTokRequired = libpam_sys::PAM_NEW_AUTHTOK_REQD,
+        AccountExpired = libpam_sys::PAM_ACCT_EXPIRED,
+        SessionError = libpam_sys::PAM_SESSION_ERR,
+        CredentialsUnavailable = libpam_sys::PAM_CRED_UNAVAIL,
+        CredentialsExpired = libpam_sys::PAM_CRED_EXPIRED,
+        CredentialsError = libpam_sys::PAM_CRED_ERR,
+        NoModuleData = libpam_sys::PAM_NO_MODULE_DATA,
+        ConversationError = libpam_sys::PAM_CONV_ERR,
+        AuthTokError = libpam_sys::PAM_AUTHTOK_ERR,
+        AuthTokRecoveryError = libpam_sys::PAM_AUTHTOK_RECOVERY_ERR,
+        AuthTokLockBusy = libpam_sys::PAM_AUTHTOK_LOCK_BUSY,
+        AuthTokDisableAging = libpam_sys::PAM_AUTHTOK_DISABLE_AGING,
+        TryAgain = libpam_sys::PAM_TRY_AGAIN,
+        Ignore = libpam_sys::PAM_IGNORE,
+        Abort = libpam_sys::PAM_ABORT,
+        AuthTokExpired = libpam_sys::PAM_AUTHTOK_EXPIRED,
+        #[cfg(feature = "basic-ext")]
+        ModuleUnknown = libpam_sys::PAM_MODULE_UNKNOWN,
+        #[cfg(feature = "basic-ext")]
+        BadItem = libpam_sys::PAM_BAD_ITEM,
+        #[cfg(feature = "linux-pam-ext")]
+        ConversationAgain = libpam_sys::PAM_CONV_AGAIN,
+        #[cfg(feature = "linux-pam-ext")]
+        Incomplete = libpam_sys::PAM_INCOMPLETE,
+        #[cfg(feature = "openpam-ext")]
+        DomainUnknown = libpam_sys::PAM_DOMAIN_UNKNOWN,
+        #[cfg(feature = "openpam-ext")]
+        BadHandle = libpam_sys::PAM_BAD_HANDLE,
+        #[cfg(feature = "openpam-ext")]
+        BadFeature = libpam_sys::PAM_BAD_FEATURE,
+        #[cfg(feature = "openpam-ext")]
+        BadConstant = libpam_sys::PAM_BAD_CONSTANT,
+    }
 }
 
 /// A PAM-specific Result type with an [ErrorCode] error.
 pub type Result<T> = StdResult<T, ErrorCode>;
 
 impl Display for ErrorCode {
+    #[cfg(all(
+        feature = "link",
+        any(pam_impl = "LinuxPam", pam_impl = "OpenPam", pam_impl = "Sun")
+    ))]
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-        match strerror((*self).into()) {
-            Some(err) => f.write_str(err),
-            None => self.fmt_internal(f),
+        use std::ffi::CStr;
+        use std::ptr;
+        // SAFETY: PAM impls don't care about the PAM handle and always return
+        // static strings.
+        let got = unsafe { libpam_sys::pam_strerror(ptr::null(), *self as c_int) };
+        if got.is_null() {
+            // This shouldn't happen.
+            write!(f, "PAM error: {self:?} ({:?})", *self as c_int)
+        } else {
+            // SAFETY: We just got this back from PAM and we checked if it's null.
+            f.write_str(&unsafe { CStr::from_ptr(got) }.to_string_lossy())
         }
     }
+    #[cfg(not(all(
+        feature = "link",
+        any(pam_impl = "LinuxPam", pam_impl = "OpenPam", pam_impl = "Sun")
+    )))]
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        fmt::Debug::fmt(self, f)
+    }
 }
 
 impl Error for ErrorCode {}
 
+#[cfg(feature = "link")]
 impl ErrorCode {
-    /// Converts this [Result] into a C-compatible result code.
-    pub fn result_to_c<T>(value: Result<T>) -> c_int {
-        match value {
-            Ok(_) => 0, // PAM_SUCCESS
-            Err(otherwise) => otherwise.into(),
+    pub(crate) fn result_from(ret: c_int) -> Result<()> {
+        match ret {
+            0 => Ok(()),
+            value => Err(ReturnCode(value).try_into().unwrap_or(BAD_CONST)),
         }
     }
-
-    /// Converts a C result code into a [Result], with success as Ok.
-    /// Invalid values are returned as a [Self::SystemError].
-    pub fn result_from(value: c_int) -> Result<()> {
-        match value {
-            0 => Ok(()),
-            value => Err(value.try_into().unwrap_or(Self::SystemError)),
-        }
-    }
-
-    /// A basic Display implementation for if we don't link against PAM.
-    fn fmt_internal(self, f: &mut Formatter<'_>) -> fmt::Result {
-        let n: c_int = self.into();
-        write!(f, "PAM error: {self:?} ({n})")
-    }
 }
 
-/// Gets a string version of an error message.
-#[cfg(feature = "link")]
-pub fn strerror(code: c_int) -> Option<&'static str> {
-    use std::ffi::CStr;
-    use std::ptr;
-    // SAFETY: PAM impls don't care about the PAM handle and always return
-    // static strings.
-    let strerror = unsafe { libpam_sys::pam_strerror(ptr::null(), code as c_int) };
-    // SAFETY: We just got this back from PAM and we checked if it's null.
-    (!strerror.is_null())
-        .then(|| unsafe { CStr::from_ptr(strerror) }.to_str().ok())
-        .flatten()
+impl<T> From<Result<T>> for ReturnCode {
+    fn from(value: Result<T>) -> Self {
+        match value {
+            Ok(_) => ReturnCode::SUCCESS,
+            Err(otherwise) => otherwise.into()
+        }
+    }
 }
 
-/// Dummy implementation of strerror so that it always returns None.
-#[cfg(not(feature = "link"))]
-pub fn strerror(_: c_int) -> Option<&'static str> {
-    None
-}
-
-#[cfg(test)]
+#[cfg(all(test, feature = "link"))]
 mod tests {
     use super::*;
 
@@ -406,35 +443,72 @@
     fn test_enums() {
         assert_eq!(Ok(()), ErrorCode::result_from(0));
         assert_eq!(
-            pam_constants::PAM_SESSION_ERR,
-            ErrorCode::result_to_c::<()>(Err(ErrorCode::SessionError))
+            ReturnCode(libpam_sys::PAM_SESSION_ERR),
+            Result::<()>::Err(ErrorCode::SessionError).into()
+        );
+        assert_eq!(
+            Result::<()>::Err(ErrorCode::Abort),
+            ErrorCode::result_from(libpam_sys::PAM_ABORT)
+        );
+        assert_eq!(Err(BAD_CONST), ErrorCode::result_from(423));
+    }
+
+    #[test]
+    fn test_flags() {
+        assert_eq!(
+            AuthtokFlags::CHANGE_EXPIRED_AUTHTOK | AuthtokFlags::SILENT,
+            AuthtokFlags::from(RawFlags(
+                libpam_sys::PAM_SILENT | libpam_sys::PAM_CHANGE_EXPIRED_AUTHTOK
+            ))
+        );
+        assert_eq!(
+            RawFlags(libpam_sys::PAM_DISALLOW_NULL_AUTHTOK),
+            AuthnFlags::DISALLOW_NULL_AUTHTOK.into()
         );
         assert_eq!(
-            Err(ErrorCode::Abort),
-            ErrorCode::result_from(pam_constants::PAM_ABORT)
+            RawFlags(libpam_sys::PAM_SILENT | libpam_sys::PAM_CHANGE_EXPIRED_AUTHTOK),
+            (AuthtokFlags::SILENT | AuthtokFlags::CHANGE_EXPIRED_AUTHTOK).into()
+        );
+    }
+
+    #[test]
+    #[cfg(pam_impl = "Sun")]
+    fn test_flags_sun() {
+        assert_eq!(
+            AuthtokFlags::NO_AUTHTOK_CHECK,
+            AuthtokFlags::from(RawFlags(libpam_sys::PAM_NO_AUTHTOK_CHECK))
         );
-        assert_eq!(Err(ErrorCode::SystemError), ErrorCode::result_from(423));
+        assert_eq!(
+            RawFlags(
+                libpam_sys::PAM_SILENT
+                    | libpam_sys::PAM_CHANGE_EXPIRED_AUTHTOK
+                    | libpam_sys::PAM_NO_AUTHTOK_CHECK
+            ),
+            (AuthtokFlags::SILENT
+                | AuthtokFlags::CHANGE_EXPIRED_AUTHTOK
+                | AuthtokFlags::NO_AUTHTOK_CHECK)
+                .into()
+        );
     }
 
     #[test]
     fn test_flag_enums() {
-        AuthtokAction::extract(-1).expect_err("too many set");
-        AuthtokAction::extract(0).expect_err("too few set");
+        AuthtokAction::extract((-1).into()).expect_err("too many set");
+        AuthtokAction::extract(0.into()).expect_err("too few set");
         assert_eq!(
-            Ok((
-                AuthtokAction::Update,
-                AuthtokFlags::from_bits_retain(0x7f000000)
-            )),
-            AuthtokAction::extract(0x7f000000 | pam_constants::PAM_UPDATE_AUTHTOK)
+            Ok((AuthtokAction::Update, AuthtokFlags::SILENT,)),
+            AuthtokAction::extract(
+                (libpam_sys::PAM_SILENT | libpam_sys::PAM_UPDATE_AUTHTOK).into()
+            )
         );
-        CredAction::extract(0xffff).expect_err("too many set");
+        CredAction::extract(0xffff.into()).expect_err("too many set");
         assert_eq!(
             Ok((CredAction::Establish, BaseFlags::empty())),
-            CredAction::extract(0)
+            CredAction::extract(0.into())
         );
         assert_eq!(
-            Ok((CredAction::Delete, BaseFlags::from_bits_retain(0x55000000))),
-            CredAction::extract(0x55000000 | pam_constants::PAM_DELETE_CRED)
+            Ok((CredAction::Delete, BaseFlags::SILENT)),
+            CredAction::extract((libpam_sys::PAM_SILENT | libpam_sys::PAM_DELETE_CRED).into())
         );
     }
 }
--- a/src/handle.rs	Wed Jul 16 18:45:20 2025 -0400
+++ b/src/handle.rs	Fri Jul 25 20:52:14 2025 -0400
@@ -182,6 +182,7 @@
     /// ```
     #[doc = man7!(3 pam_get_authtok)]
     #[doc = manbsd!(3 pam_get_authtok)]
+    /// [pam_authtok_get]: https://smartos.org/man/7/pam_authtok_get
     fn authtok(&mut self, prompt: Option<&OsStr>) -> Result<OsString>;
 
     /// Retrieves the user's old authentication token when changing passwords.
--- a/src/lib.rs	Wed Jul 16 18:45:20 2025 -0400
+++ b/src/lib.rs	Fri Jul 25 20:52:14 2025 -0400
@@ -22,8 +22,30 @@
 //!
 //! [module-guide]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/Linux-PAM_MWG.html
 
-// Temporary until everything is fully wired up.
-#![allow(dead_code)]
+#[cfg(feature = "link")]
+mod _compat_checker {
+    macro_rules! feature_check {
+        ($feature:literal, pam_impl = ($($pimpl:literal),*)) => {
+            #[cfg(all(feature = $feature, not(any($(pam_impl = $pimpl),*))))]
+            compile_error!(
+                concat!(
+                    "The feature '", $feature, "' is only available ",
+                    "with these PAM implementations:\n",
+                    $("- ", $pimpl, "\n"),*,
+                    "The current PAM implementation is:\n\n",
+                    "  ", libpam_sys::pam_impl_name!(), "\n\n",
+                    "Set the 'LIBPAMSYS_IMPL' environment variable to one of ",
+                    "the above PAM implementation names to build ",
+                    "for that implementation of PAM."
+                )
+            );
+        }
+    }
+    feature_check!("linux-pam-ext", pam_impl = ("LinuxPam"));
+    feature_check!("basic-ext", pam_impl = ("LinuxPam", "OpenPam"));
+    feature_check!("openpam-ext", pam_impl = ("OpenPam"));
+    feature_check!("sun-ext", pam_impl = ("Sun"));
+}
 
 pub mod constants;
 pub mod conv;
@@ -39,8 +61,11 @@
 pub mod logging;
 
 #[cfg(feature = "link")]
+#[doc(hidden)]
+pub use crate::libpam::ModuleExporter;
+#[cfg(feature = "link")]
 #[doc(inline)]
-pub use crate::libpam::{LibPamHandle, LibPamTransaction};
+pub use crate::libpam::{LibPamHandle, LibPamTransaction, TransactionBuilder};
 #[doc(inline)]
 pub use crate::{
     constants::{
--- a/src/libpam/conversation.rs	Wed Jul 16 18:45:20 2025 -0400
+++ b/src/libpam/conversation.rs	Fri Jul 25 20:52:14 2025 -0400
@@ -11,6 +11,7 @@
 use std::iter;
 use std::ptr::NonNull;
 use std::result::Result as StdResult;
+use crate::constants::ReturnCode;
 
 /// The type used by PAM to call back into a conversation.
 ///
@@ -64,7 +65,7 @@
             *answers_ptr = owned.into_ptr();
             Ok(())
         };
-        ErrorCode::result_to_c(internal())
+        ReturnCode::from(internal()).into()
     }
 }
 
--- a/src/libpam/handle.rs	Wed Jul 16 18:45:20 2025 -0400
+++ b/src/libpam/handle.rs	Fri Jul 25 20:52:14 2025 -0400
@@ -1,6 +1,6 @@
 use super::conversation::{OwnedConversation, PamConv};
 use crate::_doc::{guide, linklist, man7, stdlinks};
-use crate::constants::{ErrorCode, Result};
+use crate::constants::{ErrorCode, RawFlags, Result, ReturnCode};
 use crate::conv::Exchange;
 use crate::environ::EnvironMapMut;
 use crate::handle::PamShared;
@@ -10,7 +10,6 @@
 use crate::libpam::{items, memory};
 use crate::logging::{Level, Location, Logger};
 use crate::{AuthnFlags, AuthtokFlags, Conversation, EnvironMap, ModuleClient, Transaction};
-use libpam_sys_consts::constants;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
 use std::any::TypeId;
 use std::cell::Cell;
@@ -146,7 +145,8 @@
 
     /// Internal "end" function, which binary-ORs the status with `or_with`.
     fn end_internal(&mut self, or_with: i32) {
-        let result = ErrorCode::result_to_c(self.last_return.get()) | or_with;
+        let last: i32 = ReturnCode::from(self.last_return.get()).into();
+        let result = last | or_with;
         unsafe { libpam_sys::pam_end(self.handle.cast(), result) };
     }
 }
@@ -154,8 +154,9 @@
 macro_rules! wrap {
     (fn $name:ident($ftype:ident) { $pam_func:ident }) => {
         fn $name(&mut self, flags: $ftype) -> Result<()> {
+            let flags: RawFlags = flags.into();
             ErrorCode::result_from(unsafe {
-                libpam_sys::$pam_func((self as *mut Self).cast(), flags.bits())
+                libpam_sys::$pam_func((self as *mut Self).cast(), flags.into())
             })
         }
     };
@@ -265,7 +266,8 @@
     #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
     #[doc = stdlinks!(3 pam_end)]
     pub fn end(&mut self, result: Result<()>) {
-        unsafe { libpam_sys::pam_end(self.inner_mut(), ErrorCode::result_to_c(result)) };
+        let code: ReturnCode = result.into();
+        unsafe { libpam_sys::pam_end(self.inner_mut(), code.into()) };
     }
 
     #[cfg_attr(
@@ -290,7 +292,7 @@
     #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
     #[doc = stdlinks!(3 pam_end)]
     pub fn end_silent(&mut self, result: Result<()>) {
-        let result = ErrorCode::result_to_c(result);
+        let result: i32 = ReturnCode::from(result).into();
         #[cfg(pam_impl = "LinuxPam")]
         let result = result | libpam_sys::PAM_DATA_SILENT;
         unsafe {
@@ -517,33 +519,33 @@
 #[non_exhaustive] // because C could give us anything!
 pub enum ItemType {
     /// The PAM service name.
-    Service = constants::PAM_SERVICE,
+    Service = libpam_sys::PAM_SERVICE,
     /// The user's login name.
-    User = constants::PAM_USER,
+    User = libpam_sys::PAM_USER,
     /// The TTY name.
-    Tty = constants::PAM_TTY,
+    Tty = libpam_sys::PAM_TTY,
     /// The remote host (if applicable).
-    RemoteHost = constants::PAM_RHOST,
+    RemoteHost = libpam_sys::PAM_RHOST,
     /// The conversation struct (not a CStr-based item).
-    Conversation = constants::PAM_CONV,
+    Conversation = libpam_sys::PAM_CONV,
     /// The authentication token (password).
-    AuthTok = constants::PAM_AUTHTOK,
+    AuthTok = libpam_sys::PAM_AUTHTOK,
     /// The old authentication token (when changing passwords).
-    OldAuthTok = constants::PAM_OLDAUTHTOK,
+    OldAuthTok = libpam_sys::PAM_OLDAUTHTOK,
     /// The remote user's name.
-    RemoteUser = constants::PAM_RUSER,
+    RemoteUser = libpam_sys::PAM_RUSER,
     /// The prompt shown when requesting a username.
-    UserPrompt = constants::PAM_USER_PROMPT,
+    UserPrompt = libpam_sys::PAM_USER_PROMPT,
     #[cfg(feature = "linux-pam-ext")]
     /// App-supplied function to override failure delays.
-    FailDelay = constants::PAM_FAIL_DELAY,
+    FailDelay = libpam_sys::PAM_FAIL_DELAY,
     #[cfg(feature = "linux-pam-ext")]
     /// X display name.
-    XDisplay = constants::PAM_XDISPLAY,
+    XDisplay = libpam_sys::PAM_XDISPLAY,
     #[cfg(feature = "linux-pam-ext")]
     /// X server authentication data.
-    XAuthData = constants::PAM_XAUTHDATA,
+    XAuthData = libpam_sys::PAM_XAUTHDATA,
     #[cfg(feature = "linux-pam-ext")]
     /// The type of `pam_get_authtok`.
-    AuthTokType = constants::PAM_AUTHTOK_TYPE,
+    AuthTokType = libpam_sys::PAM_AUTHTOK_TYPE,
 }
--- a/src/libpam/mod.rs	Wed Jul 16 18:45:20 2025 -0400
+++ b/src/libpam/mod.rs	Fri Jul 25 20:52:14 2025 -0400
@@ -17,3 +17,4 @@
 
 #[doc(inline)]
 pub use handle::{LibPamHandle, LibPamTransaction, TransactionBuilder};
+pub use module::ModuleExporter;
--- a/src/libpam/module.rs	Wed Jul 16 18:45:20 2025 -0400
+++ b/src/libpam/module.rs	Fri Jul 25 20:52:14 2025 -0400
@@ -1,7 +1,13 @@
+use crate::constants::{ErrorCode, RawFlags, Result};
+use crate::libpam::handle::LibPamHandle;
+use crate::module::PamModule;
+use crate::{AuthnFlags, AuthtokAction, BaseFlags, CredAction};
+use std::ffi::{c_char, c_int, c_void, CStr};
+
 /// Generates the dynamic library entry points for a PAM module
 ///
 /// Calling `pam_hooks!(SomeType)` on a type that implements
-/// [`PamModule`](crate::PamModule) will generate the exported
+/// [`PamModule`] will generate the exported
 /// `extern "C"` functions that PAM uses to call into your module.
 ///
 /// ## Examples:
@@ -41,115 +47,132 @@
 macro_rules! pam_hooks {
     ($ident:ident) => {
         mod _pam_hooks_scope {
-            use std::ffi::{c_char, c_int, c_void, CStr};
-            use $crate::{
-                AuthnFlags, AuthtokAction, BaseFlags, CredAction, ErrorCode, LibPamHandle,
-                PamModule,
-            };
+            use std::ffi::{c_char, c_int, c_void};
+            use $crate::ModuleExporter;
+            use $crate::constants::{RawFlags, ReturnCode};
 
-            macro_rules! handle {
-                ($pamh:ident) => {
-                    match unsafe { $pamh.cast::<LibPamHandle>().as_mut() } {
-                        Some(handle) => handle,
-                        None => return ErrorCode::Ignore.into(),
+            macro_rules! export {
+                ($func:ident) => {
+                    #[no_mangle]
+                    unsafe extern "C" fn $func(
+                        pamh: *mut c_void,
+                        flags: RawFlags,
+                        argc: c_int,
+                        argv: *const *const c_char,
+                    ) -> c_int {
+                        let ret: ReturnCode = ModuleExporter::$func::<super::$ident>(
+                            pamh, flags, argc, argv
+                        ).into();
+                        ret.into()
                     }
                 };
             }
 
-            #[no_mangle]
-            extern "C" fn pam_sm_acct_mgmt(
-                pamh: *mut c_void,
-                flags: AuthnFlags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let handle = handle!(pamh);
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::account_management(handle, args, flags))
-            }
-
-            #[no_mangle]
-            extern "C" fn pam_sm_authenticate(
-                pamh: *mut c_void,
-                flags: AuthnFlags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let handle = handle!(pamh);
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::authenticate(handle, args, flags))
-            }
-
-            #[no_mangle]
-            extern "C" fn pam_sm_chauthtok(
-                pamh: *mut c_void,
-                flags: c_int,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let handle = handle!(pamh);
-                let (action, flags) = match AuthtokAction::extract(flags) {
-                    Ok(val) => val,
-                    Err(e) => return e.into(),
-                };
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::change_authtok(handle, args, action, flags))
-            }
-
-            #[no_mangle]
-            extern "C" fn pam_sm_close_session(
-                pamh: *mut c_void,
-                flags: BaseFlags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let handle = handle!(pamh);
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::close_session(handle, args, flags))
-            }
-
-            #[no_mangle]
-            extern "C" fn pam_sm_open_session(
-                pamh: *mut c_void,
-                flags: BaseFlags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let handle = handle!(pamh);
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::open_session(handle, args, flags))
-            }
-
-            #[no_mangle]
-            extern "C" fn pam_sm_setcred(
-                pamh: *mut c_void,
-                flags: c_int,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let handle = handle!(pamh);
-                let (action, flags) = match CredAction::extract(flags) {
-                    Ok(val) => val,
-                    Err(e) => return e.into(),
-                };
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::set_credentials(handle, args, action, flags))
-            }
-
-            /// Turns `argc`/`argv` into a [Vec] of [CStr]s.
-            ///
-            /// # Safety
-            ///
-            /// We use this only with arguments we get from `libpam`, which we kind of have to trust.
-            fn extract_argv<'a>(argc: c_int, argv: *const *const c_char) -> Vec<&'a CStr> {
-                (0..argc)
-                    .map(|o| unsafe { CStr::from_ptr(*argv.offset(o as isize) as *const c_char) })
-                    .collect()
-            }
+            export!(pam_sm_acct_mgmt);
+            export!(pam_sm_authenticate);
+            export!(pam_sm_chauthtok);
+            export!(pam_sm_close_session);
+            export!(pam_sm_open_session);
+            export!(pam_sm_setcred);
         }
     };
 }
 
+#[doc(hidden)]
+pub struct ModuleExporter;
+
+// All of the below are only intended to be called directly from C.
+#[allow(clippy::missing_safety_doc)]
+impl ModuleExporter {
+    pub unsafe fn pam_sm_acct_mgmt<M: PamModule<LibPamHandle>>(
+        pamh: *mut c_void,
+        flags: RawFlags,
+        argc: c_int,
+        argv: *const *const c_char,
+    ) -> Result<()> {
+        let handle = wrap(pamh)?;
+        let args = extract_argv(argc, argv);
+        M::account_management(handle, args, AuthnFlags::from(flags))
+    }
+
+    pub unsafe fn pam_sm_authenticate<M: PamModule<LibPamHandle>>(
+        pamh: *mut c_void,
+        flags: RawFlags,
+        argc: c_int,
+        argv: *const *const c_char,
+    ) -> Result<()> {
+        let handle = wrap(pamh)?;
+        let args = extract_argv(argc, argv);
+        M::authenticate(handle, args, AuthnFlags::from(flags))
+    }
+
+    pub unsafe fn pam_sm_chauthtok<M: PamModule<LibPamHandle>>(
+        pamh: *mut c_void,
+        flags: RawFlags,
+        argc: c_int,
+        argv: *const *const c_char,
+    ) -> Result<()> {
+        let handle = wrap(pamh)?;
+        let (action, flags) = AuthtokAction::extract(flags)?;
+        let args = extract_argv(argc, argv);
+        M::change_authtok(handle, args, action, flags)
+    }
+
+    pub unsafe fn pam_sm_close_session<M: PamModule<LibPamHandle>>(
+        pamh: *mut c_void,
+        flags: RawFlags,
+        argc: c_int,
+        argv: *const *const c_char,
+    ) -> Result<()> {
+        let handle = wrap(pamh)?;
+        let args = extract_argv(argc, argv);
+        M::close_session(handle, args, BaseFlags::from(flags))
+    }
+
+    pub unsafe fn pam_sm_open_session<M: PamModule<LibPamHandle>>(
+        pamh: *mut c_void,
+        flags: RawFlags,
+        argc: c_int,
+        argv: *const *const c_char,
+    ) -> Result<()> {
+        let handle = wrap(pamh)?;
+        let args = extract_argv(argc, argv);
+        M::open_session(handle, args, BaseFlags::from(flags))
+    }
+
+    pub unsafe fn pam_sm_setcred<M: PamModule<LibPamHandle>>(
+        pamh: *mut c_void,
+        flags: RawFlags,
+        argc: c_int,
+        argv: *const *const c_char,
+    ) -> Result<()> {
+        let handle = wrap(pamh)?;
+        let (action, flags) = CredAction::extract(flags)?;
+        let args = extract_argv(argc, argv);
+        M::set_credentials(handle, args, action, flags)
+    }
+}
+
+/// Turns `argc`/`argv` into a [Vec] of [CStr]s.
+///
+/// # Safety
+///
+/// We use this only with arguments we get from `libpam`, which we kind of have to trust.
+unsafe fn extract_argv<'a>(argc: c_int, argv: *const *const c_char) -> Vec<&'a CStr> {
+    (0..argc)
+        .map(|o| unsafe { CStr::from_ptr(*argv.offset(o as isize) as *const c_char) })
+        .collect()
+}
+
+/// Wraps the pointer in a PAM handle, or returns an error if it's null.
+///
+/// # Safety
+///
+/// It's up to you to pass a valid handle.
+unsafe fn wrap<'a>(handle: *mut c_void) -> Result<&'a mut LibPamHandle> {
+    handle.cast::<LibPamHandle>().as_mut().ok_or(ErrorCode::SystemError)
+}
+
 #[cfg(test)]
 mod tests {
     // Compile-time test that the `pam_hooks` macro compiles.
--- a/src/logging.rs	Wed Jul 16 18:45:20 2025 -0400
+++ b/src/logging.rs	Fri Jul 25 20:52:14 2025 -0400
@@ -1,10 +1,8 @@
 //! PAM logging variables and macros.
 //!
 //! PAM implementations usually include the ability to log to syslog in a way
-//! that is associated with the log entry itself. This module defines the enums
-//! and macros for logging.
-//!
-//! For more details, see [`PamShared::log`](crate::PamShared::log).
+//! that is associated with the log entry itself. This module defines
+//! the interface we use for logging.
 //!
 //! We can't use [the `log` crate](https://docs.rs/log) because that requires
 //! that any `Log` implementors be `Sync` and `Send`, and a PAM handle
--- a/src/module.rs	Wed Jul 16 18:45:20 2025 -0400
+++ b/src/module.rs	Fri Jul 25 20:52:14 2025 -0400
@@ -100,19 +100,11 @@
     /// that information to the application. It should only be called after
     /// authentication but before a session is established.
     ///
+    /// The module should perform the specified `action`.
+    ///
     /// See [the Module Writer's Guide entry for `pam_sm_setcred`][mwg]
     /// for more information.
     ///
-    /// # Valid flags
-    ///
-    /// This function may be called with the following flags set:
-    ///
-    /// - [`Flags::SILENT`]
-    /// - [`Flags::ESTABLISH_CREDENTIALS`]: Initialize credentials for the user.
-    /// - [`Flags::DELETE_CREDENTIALS`]: Delete the credentials associated with this module.
-    /// - [`Flags::REINITIALIZE_CREDENTIALS`]: Re-initialize credentials for this user.
-    /// - [`Flags::REFRESH_CREDENTIALS`]: Extend the lifetime of the user's credentials.
-    ///
     /// # Returns
     ///
     /// If credentials were set successfully, return `Ok(())`.
@@ -139,11 +131,18 @@
     /// Called to set or reset the user's authentication token.
     ///
     /// PAM calls this function twice in succession.
-    ///  1. The first time, [`Flags::PRELIMINARY_CHECK`] will be set.
+    ///  1. The first time, the `action` will be
+    ///     [`AuthtokAction::Validate`].
     ///     If the new token is acceptable, return success;
     ///     if not, return [`ErrorCode::TryAgain`] to re-prompt the user.
-    ///  2. After the preliminary check succeeds, [`Flags::UPDATE_AUTHTOK`]
-    ///     will be set. On this call, actually update the stored auth token.
+    ///  2. After the preliminary check succeeds, you will be called again
+    ///     with the same password and [`AuthtokAction::Update`].
+    ///     When this happens, actually change the authentication token.
+    ///
+    /// The new authentication token will be available in
+    /// [`authtok`](ModuleClient::authtok),
+    /// and the previous authentication token will be available in
+    /// [`old_authtok`](ModuleClient::old_authtok).
     ///
     /// See [the Module Writer's Guide entry for `pam_sm_chauthtok`][mwg]
     /// for more information.
--- a/testharness/src/bin/testharness.rs	Wed Jul 16 18:45:20 2025 -0400
+++ b/testharness/src/bin/testharness.rs	Fri Jul 25 20:52:14 2025 -0400
@@ -22,7 +22,7 @@
     wrong_username: bool,
     wrong_password: bool,
     changing_password: Cell<bool>,
-    change_prompt_count: Cell<u8>,
+    change_prompt_count: Cell<usize>,
 }
 
 impl Conversation for &TestHarness {
@@ -42,19 +42,27 @@
                 }
                 Exchange::MaskedPrompt(p) => {
                     let answer = if self.changing_password.get() {
-                        let prompts = self.change_prompt_count.get();
-                        eprintln!("CHANGING PASSWORD PROMPT {prompts}");
+                        let prompt_count = self.change_prompt_count.get();
+                        eprintln!("CHANGING PASSWORD PROMPT {prompt_count}");
                         eprintln!("-> {p:?}");
-                        self.change_prompt_count.set(prompts + 1);
-                        match prompts {
-                            0 => "old token!",
-                            1 => "mistake",
-                            2 => "mismatch",
-                            3 => "old token!",
-                            4 => "acceptable",
-                            5 => "acceptable",
-                            _ => panic!("unexpected number of prompts!"),
-                        }
+                        self.change_prompt_count.set(prompt_count + 1);
+                        // When changing passwords after logging in, Sun PAM
+                        // uses the existing authtok that was just entered as
+                        // the old_authtok. Other PAMs prompt the user to enter
+                        // their existing password again.
+                        let responses: &[&str] = if cfg!(pam_impl = "Sun") {
+                            &["mistake", "mismatch", "acceptable", "acceptable"]
+                        } else {
+                            &[
+                                "old token!",
+                                "mistake",
+                                "mismatch",
+                                "old token!",
+                                "acceptable",
+                                "acceptable",
+                            ]
+                        };
+                        responses[prompt_count]
                     } else if self.wrong_password {
                         "bogus"
                     } else {
--- a/testharness/src/lib.rs	Wed Jul 16 18:45:20 2025 -0400
+++ b/testharness/src/lib.rs	Fri Jul 25 20:52:14 2025 -0400
@@ -87,7 +87,7 @@
         _flags: AuthtokFlags,
     ) -> nonstick::Result<()> {
         match action {
-            AuthtokAction::PreliminaryCheck => {
+            AuthtokAction::Validate => {
                 if handle.old_authtok(None)?.as_bytes() != b"old token!" {
                     return Err(ErrorCode::AuthenticationError);
                 }