moonlight-triangulation-1.3.0.0: bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Native.hs
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
-- | Resource-safe dynamic boundary to the Rust crates under comparison.
module Moonlight.Triangulation.Bench.DelaunayCompare.Native
( NativeApi
, NativeFixture
, GeneratedCoordinates
, withNativeApi
, generateCoordinates
, generatedCoordinatePairs
, prepareNativeFixture
, runNativeFixture
, releaseNativeFixture
) where
import Control.Exception (IOException, bracket, displayException, try)
import Control.Monad (filterM)
import Control.Monad.Trans.Except (ExceptT (ExceptT), runExceptT)
import Data.Bifunctor (first)
import Data.Int (Int32)
import Data.List (unfoldr)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NonEmpty
import Moonlight.Triangulation.Bench.DelaunayCompare.Domain
import Foreign.C.Types (CDouble, CInt (..), CSize (..))
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr (FunPtr, Ptr, nullPtr)
import Foreign.Storable (peek, poke)
import qualified Data.Vector as Vector
import qualified Data.Vector.Storable as StorableVector
import qualified Data.Vector.Storable.Mutable as MutableStorableVector
import System.Directory (doesFileExist, makeAbsolute)
import System.Environment (lookupEnv)
import System.Exit (ExitCode (..))
import System.FilePath ((</>), takeDirectory)
import System.Info (os)
import System.Posix.DynamicLinker
( DL
, RTLDFlags (RTLD_LOCAL, RTLD_NOW)
, dlclose
, dlopen
, dlsym
)
import System.Process
( CreateProcess (cwd)
, proc
, readCreateProcessWithExitCode
)
type GenerateFunction = CInt -> CSize -> Ptr CDouble -> CSize -> IO CInt
type PrepareFunction = CInt -> Ptr CDouble -> CSize -> Ptr (Ptr ()) -> IO CInt
type RunFunction = Ptr () -> Ptr CSize -> Ptr CSize -> IO CInt
type ReleaseFunction = Ptr () -> IO ()
foreign import ccall safe "dynamic"
bindGenerateFunction :: FunPtr GenerateFunction -> GenerateFunction
foreign import ccall safe "dynamic"
bindPrepareFunction :: FunPtr PrepareFunction -> PrepareFunction
foreign import ccall safe "dynamic"
bindRunFunction :: FunPtr RunFunction -> RunFunction
foreign import ccall unsafe "dynamic"
bindReleaseFunction :: FunPtr ReleaseFunction -> ReleaseFunction
data NativeApi = NativeApi
{ nativeGenerate :: !GenerateFunction
, nativePrepare :: !PrepareFunction
, nativeRun :: !RunFunction
, nativeRelease :: !ReleaseFunction
}
newtype NativeFixture = NativeFixture (Ptr ())
data GeneratedCoordinates = GeneratedCoordinates
{ generatedPointCount :: !PointCount
, generatedCoordinateValues :: !(StorableVector.Vector CDouble)
}
withNativeApi
:: (NativeApi -> IO (Either CompareObstruction result))
-> IO (Either CompareObstruction result)
withNativeApi action = do
manifestResult <- resolveRustManifest
case manifestResult of
Left obstruction -> pure (Left obstruction)
Right manifest -> do
libraryResult <- buildRustLibrary manifest
case libraryResult of
Left obstruction -> pure (Left obstruction)
Right libraryPath -> withLoadedNativeApi libraryPath action
resolveRustManifest :: IO (Either CompareObstruction FilePath)
resolveRustManifest = do
configured <- lookupEnv "MOONLIGHT_DELAUNAY_COMPARE_RUST_MANIFEST"
let candidates =
case configured of
Just path -> path :| []
Nothing ->
"bench/delaunay-compare/rust/Cargo.toml"
:| [ "foundation/moonlight-triangulation/bench/delaunay-compare/rust/Cargo.toml"
, "compiler/foundation/moonlight-triangulation/bench/delaunay-compare/rust/Cargo.toml"
]
existing <- filterM doesFileExist (NonEmpty.toList candidates)
case existing of
manifest : _ -> Right <$> makeAbsolute manifest
[] -> pure (Left (RustManifestNotFound candidates))
buildRustLibrary :: FilePath -> IO (Either CompareObstruction FilePath)
buildRustLibrary manifest = do
libraryNameResult <- dynamicLibraryName
case libraryNameResult of
Left obstruction -> pure (Left obstruction)
Right libraryName -> do
let rustRoot = takeDirectory manifest
targetDirectory = rustRoot </> "target"
command =
( proc
"cargo"
[ "build"
, "--quiet"
, "--release"
, "--lib"
, "--manifest-path"
, manifest
, "--target-dir"
, targetDirectory
]
)
{ cwd = Just rustRoot
}
invocation <- try @IOException (readCreateProcessWithExitCode command "")
pure $
case invocation of
Left exception ->
Left (CargoInvocationFailed manifest (displayException exception))
Right (ExitFailure exitCode, standardOutput, standardError) ->
Left
( CargoBuildFailed
manifest
exitCode
(standardError <> standardOutput)
)
Right (ExitSuccess, _, _) ->
Right (targetDirectory </> "release" </> libraryName)
dynamicLibraryName :: IO (Either CompareObstruction FilePath)
dynamicLibraryName =
pure $
case os of
"darwin" -> Right "libmoonlight_delaunay_compare_referents.dylib"
"linux" -> Right "libmoonlight_delaunay_compare_referents.so"
host -> Left (UnsupportedDynamicLibraryHost host)
withLoadedNativeApi
:: FilePath
-> (NativeApi -> IO (Either CompareObstruction result))
-> IO (Either CompareObstruction result)
withLoadedNativeApi libraryPath action = do
opened <- try @IOException (dlopen libraryPath [RTLD_NOW, RTLD_LOCAL])
case opened of
Left exception ->
pure (Left (DynamicLibraryOpenFailed libraryPath (displayException exception)))
Right handle ->
bracket
(pure handle)
dlclose
( \loadedHandle -> do
apiResult <- loadNativeApi libraryPath loadedHandle
case apiResult of
Left obstruction -> pure (Left obstruction)
Right api -> action api
)
loadNativeApi :: FilePath -> DL -> IO (Either CompareObstruction NativeApi)
loadNativeApi libraryPath handle =
runExceptT $
NativeApi
<$> loadSymbol libraryPath handle "delaunay_compare_generate" bindGenerateFunction
<*> loadSymbol libraryPath handle "delaunay_compare_prepare" bindPrepareFunction
<*> loadSymbol libraryPath handle "delaunay_compare_run" bindRunFunction
<*> loadSymbol libraryPath handle "delaunay_compare_release" bindReleaseFunction
loadSymbol
:: FilePath
-> DL
-> String
-> (FunPtr function -> boundFunction)
-> ExceptT CompareObstruction IO boundFunction
loadSymbol libraryPath handle symbol bind =
ExceptT $ do
loaded <- try @IOException (dlsym handle symbol)
pure $
first
(DynamicSymbolLoadFailed libraryPath symbol . displayException)
(bind <$> loaded)
generateCoordinates
:: NativeApi
-> FixtureSpec
-> IO (Either NativeStatus GeneratedCoordinates)
generateCoordinates api fixture = do
let pointCount = fixturePointCount fixture
coordinateCount = 2 * pointCountValue pointCount
mutableCoordinates <- MutableStorableVector.new coordinateCount
statusCode <-
MutableStorableVector.unsafeWith mutableCoordinates $ \coordinatePointer ->
nativeGenerate api
(distributionTag (fixturePointDistribution fixture))
(fromIntegral (pointCountValue pointCount))
coordinatePointer
(fromIntegral coordinateCount)
case decodeNativeStatus statusCode of
Left status -> pure (Left status)
Right () -> do
coordinates <- StorableVector.unsafeFreeze mutableCoordinates
pure (Right (GeneratedCoordinates pointCount coordinates))
generatedCoordinatePairs :: GeneratedCoordinates -> Vector.Vector (Double, Double)
generatedCoordinatePairs generated =
Vector.fromList
( unfoldr
takeCoordinatePair
(realToFrac <$> StorableVector.toList (generatedCoordinateValues generated))
)
where
takeCoordinatePair :: [Double] -> Maybe ((Double, Double), [Double])
takeCoordinatePair = \case
x : y : remaining -> Just ((x, y), remaining)
_ -> Nothing
prepareNativeFixture
:: NativeApi
-> NativeImplementation
-> GeneratedCoordinates
-> IO (Either NativePreparationFailure NativeFixture)
prepareNativeFixture api implementation coordinates =
alloca $ \preparedOutput -> do
poke preparedOutput nullPtr
statusCode <-
StorableVector.unsafeWith (generatedCoordinateValues coordinates) $ \coordinatePointer ->
nativePrepare api
(implementationTag implementation)
coordinatePointer
(fromIntegral (pointCountValue (generatedPointCount coordinates)))
preparedOutput
case decodeNativeStatus statusCode of
Left status -> pure (Left (NativePreparationStatus status))
Right () -> do
prepared <- peek preparedOutput
pure $
if prepared == nullPtr
then Left NativePreparationReturnedNull
else Right (NativeFixture prepared)
runNativeFixture
:: NativeApi
-> NativeFixture
-> IO (Either NativeRunFailure DelaunaySummary)
runNativeFixture api (NativeFixture prepared) =
alloca $ \vertexCountOutput ->
alloca $ \triangleCountOutput -> do
statusCode <- nativeRun api prepared vertexCountOutput triangleCountOutput
case decodeNativeStatus statusCode of
Left status -> pure (Left (NativeRunStatus status))
Right () -> do
vertexCount <- peek vertexCountOutput
triangleCount <- peek triangleCountOutput
pure $ DelaunaySummary <$> cSizeToInt vertexCount <*> cSizeToInt triangleCount
releaseNativeFixture :: NativeApi -> NativeFixture -> IO ()
releaseNativeFixture api (NativeFixture prepared) = nativeRelease api prepared
decodeNativeStatus :: CInt -> Either NativeStatus ()
decodeNativeStatus statusCode =
case fromIntegral statusCode :: Int32 of
0 -> Right ()
1 -> Left NativeNullPointer
2 -> Left NativeUnknownTag
3 -> Left NativeCoordinateCountMismatch
4 -> Left NativeDistributionConstructionFailed
5 -> Left NativeTriangulationFailed
6 -> Left NativePanicked
unknown -> Left (NativeUnknownStatus (fromIntegral unknown))
cSizeToInt :: CSize -> Either NativeRunFailure Int
cSizeToInt value =
let integerValue = toInteger value
in if integerValue > toInteger (maxBound :: Int)
then Left (NativeCountExceedsHaskellInt integerValue)
else Right (fromInteger integerValue)
distributionTag :: PointDistribution -> CInt
distributionTag = \case
LocalInsertion -> 0
Uniform -> 1
implementationTag :: NativeImplementation -> CInt
implementationTag = \case
NativeSpade -> 0
NativeSpadeHierarchy -> 1
NativeCdt -> 2
NativeDelaunator -> 3