A lot of functionality can be implemented in high-level user-space libraries, but at some point we must interact with the system and its kernel through syscalls. In the GHC Haskell ecosystem, we use the wrappers provided by the system C library instead of directly calling into the kernel, which doesn’t take a lot of effort thanks to GHC’s excellent Foreign Function Interface support. Exposing the
int openat(int dirfd, const char *pathname, int flags)
function from libc to Haskell code is as simple as
foreign import capi safe "fcntl.h openat" c_openat :: CInt -> CString -> CInt -> IO CInt
Now Haskell code can call
c_openat to open a file.
The above code is, however, a bit too low-level for most practical purposes:
openatfails, it returns
errnois set. Checking whether the return value of the
-1is not very Haskell’esque: this is where we expect an exception to be thrown.
CInttype is very generic. Instead, we’d like to use a type specific to file-descriptors, so a corresponding
closeaction will take such type as an argument, not any arbitrary
CStringargument, which is a
Ptr CChar. In high-level code, this is not a type we regularly work with. An API using a less clumsy path type would be welcome.
openFdAt :: Maybe Fd -> FilePath -> OpenMode -> OpenFileFlags -> IO Fd
Foreign.C.Error.throwErrnoIfMinus1 :: (Eq a, Num a) => String -> IO a -> IO a
and takes a
String) as argument, internally turning this into a
CString for the lifetime of the call to
Foreign.C.String.withCString :: String -> (CString -> IO a) -> IO a
Some problems with
unix package has been serving the Haskell ecosystem well for many years
(and, without a doubt, for many years to come). However, it’s not without its
FilePathtype (as used in, e.g., the
System.Posix.IOmodule) being a
Stringcomes with performance implications. Hence, the
System.Posix.IOmodule was cloned into
System.Posix.IO.ByteStringand reworked to use
ByteStringvalues as paths. This duplication requires certain code changes to be applied in multiple modules.
ByteStringare suitable types for paths, because of encoding issues. Hence, new clones of applicable modules were created now using
System.Posix.IO.PosixString), further increasing code-duplication.
unixlibrary exposes functions that are not necessarily available on all supported platforms, e.g., the WASM/WASI platform lacks some. Availability of library functions is checked using a
configurescript at build time, but when some library function is not found, the corresponding binding in
unixis implemented, unconditionally, as
ioError (ioeSetLocation unsupportedOperation "...")
This breaks the “If it compiles, it works” mantra, since any call to the function will most definitely not work.
unixprovides somewhat-high-level access the system functions, though one can argue the wrappers are, sometimes, too high-level, and lower-level interfaces are not made available. As an example, the
openAtcall above takes an
OpenFileFlagsargument which is a structure whose fields are mapped to bits in the
openat. However, if one want to use a flag that’s not available in
O_PATHon a Linux system), this is not possible. This forced me to implement another set of bindings for
landlock-hs: not a lot of effort, but duplication nonetheless.
- Most system functions are effectful (that’s why they exist in the first
place), so a big part of the
unixAPI lives in
IO. When working with monad stacks layered on top of
IO, this requires a lot of
To experiment with alternative implementation strategies to expose system
functions to Haskell code, I created the
package. Actually, two packages:
xinu-ffi, which exposes FFI bindings to library functions, using a
configurescript to detect system capabilities. If a library function is not found at
xinu-ffiwill not provide a binding to it. Hence, the library API can depend on the build environment. However, a
xinu-ffi.hheader file is installed so dependent package can detect availability of functions using the
xinu, and a couple of internal libraries, which expose higher-level APIs to a developer.
xinu library, similar to
unix, supports multiple path types. However,
unix, this is not implemented by copying the modules. Instead, it
which brings ML module functor-like functionality to Haskell. Basically,
Backpack allows us to write code, abstracted over a module for which we only
provide the signature (i.e., the types and functions it must expose). Then, at
build time, we can specify one or more implementations of this signature and
create instanciations of the abstract module. Hence, without any code
both instanciations of
System.Xinu.Path.ByteString). The latter are
two modules implementing a rather simple signature:
signature System.Xinu.Path ( Path , toString , withPath ) where import Control.Monad.Catch (MonadMask) import Control.Monad.IO.Class (MonadIO) import Foreign.C.String (CString) data Path -- Execute an action, passing the given Path as a CString. withPath :: (MonadIO m, MonadMask m) => Path -> (CString -> m a) -> m a -- Used for error reporting. toString :: Path -> String
xinu will expose a different API depending on
library function availability of the build environment (based on findings of
configure script). Similar to
xinu-ffi, it provides a
header file, so dependents who care about availability of functions (e.g., to
provide different implementations based on what’s available) can use
xinu functions aren’t
IO (though they’re
SPECIALIZEd for it).
Instead, it relies on the
MonadIO type-class to
where necessary. Furthermore, errors are reported and (where applicable) safely
handled using the
MonadMask type-classes from the
exceptions package. This
xinu functions to be used within arbitrary monad stacks (assuming an
MonadMask is present).
System.Xinu.IO.FFI.c_openat :: Int32 -> CString -> Int32 -> Word32 -> IO Int32
xinu exposes, among others,
System.Xinu.IO.FilePath.openat :: (MonadIO m, MonadMask m) => Maybe Int32 -> FilePath -> Int32 -> Maybe Word32 -> m Int32
System.Xinu.IO.ByteString.openat :: (MonadIO m, MonadMask m) => Maybe Int32 -> ByteString -> Int32 -> Maybe Word32 -> m Int32
Of course, the intent is to use higher-level types (like
Fd) instead. This
should be fairly simple to do, especially using
since indeed, we expect (or rather, require) such
Fd to be equal to an
(at least on this platform).
Learnings and Questions
It’s a bit too soon to know where this experiment could be heading. However, some early experiences:
- Backpack doesn’t work when a package has
Configure, which is a bummer: it forces
xinu-ffito be a separate package, which increases maintenance burden. If it could be a (public) internal library, this would make things quite a bit easier.
- A multi-package repository (using
cabal.project) and Backpack seems to trigger a dependency resolution issue in Cabal, causing
cabal install --dependencies-only allto fail.
stackdoesn’t support Backpack. This is a known issue but limits adoption of Backpack in the ecosystem, which is a shame, since ML-style module functors are a great way to reduce code duplication without incurring any runtime performance overhead.
- Haddock doesn’t like
Mixins, internal libraries and
Reexported-Modules. Basically, no decent API documentation of
xinucan be generated. There are several related issues filed upstream.
- Given the new
OsPathfamily of types, does it make sense to keep providing
ByteStringand other implementations?
- More tests to ensure exception handling is working as desired are needed. How
is this code different from the
xinu is in no way meant to be as extensive as
unix, it’s always
interesting to explore different approaches to a given problem, especially when
new functionality can be leveraged which wasn’t available when the original
solution was coded.
I’d love to hear your feedback. Would a library like
xinu be of any use to
you? What’s missing? Head to the repository’s