Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hard-code global tables' contents instead of calculating them at run-time #131

Draft
wants to merge 43 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ac17dd1
Remove uses of unsafePerformIO
exarkun Jan 13, 2023
9ffd704
Add an aggressive property test for encoding sanely
exarkun Jan 13, 2023
989f90f
Merge remote-tracking branch 'origin/master' into remove-unsafePerformIO
exarkun Jan 18, 2023
e452854
Adapt to switch to quickcheck-instances
exarkun Jan 18, 2023
eb47bfc
Adapt to enFEC and deFEC being in IO
exarkun Jan 18, 2023
ec5e1dd
let fourmolu do its thing
exarkun Jan 18, 2023
768fe4e
bump version to reflect incompatible change
exarkun Jan 18, 2023
390bfad
prefer where over let
exarkun Sep 13, 2023
b515dbf
remove redundant parens
exarkun Sep 13, 2023
3820caa
prefer where over let
exarkun Sep 13, 2023
1fa8a58
more frequent failure reproduction
exarkun Sep 13, 2023
40beb48
Initialise tables instead of calculating them at run time
adrianmay Mar 17, 2025
2108bbc
Bump version, add changelog entry
adrianmay Mar 17, 2025
fc4d19d
Merge remote-tracking branch 'origin/master' into inittables
adrianmay Mar 17, 2025
dfefc04
Update a comment about fec_init
adrianmay Mar 17, 2025
4ce97ec
Fix typo
adrianmay Mar 17, 2025
e51db10
Merge from master
adrianmay Mar 17, 2025
31bbbaa
Merge from master
adrianmay Mar 17, 2025
28cc766
Merge from master
adrianmay Mar 17, 2025
3527617
Merge commit '14fc79b' into slowmerge
adrianmay Mar 17, 2025
923f11a
Merge commit '6de0374' into slowmerge
adrianmay Mar 17, 2025
a23f2a7
Fix errors and warnings
adrianmay Mar 17, 2025
882c5c9
Bump major version
adrianmay Mar 17, 2025
29bffee
Bump major version
adrianmay Mar 17, 2025
e81cc13
Completely remove fec_init and Haskell's initialize
adrianmay Mar 18, 2025
519b64b
Use a better lock
adrianmay Mar 18, 2025
05da318
Merge better lock from remove-unsafePerformIO to init-tables
adrianmay Mar 18, 2025
bf0e50d
Reduce cabal version contraints to hackage rules
adrianmay Mar 18, 2025
a27931c
Merge branch 'remove-unsafePerformIO' into init-tables
adrianmay Mar 18, 2025
2e9f5ac
Oops: reinstate uninitialised check
adrianmay Mar 18, 2025
1e650f8
Merge from remove-unsafePerformIO
adrianmay Mar 18, 2025
977fa5e
Move code that's purely for table generation to write.c
adrianmay Mar 18, 2025
096aaf4
Typo
adrianmay Mar 18, 2025
7c8629a
Merge remote-tracking branch 'tahoe/127.pypi-trusted-publishing' into…
adrianmay Mar 19, 2025
fde70c3
Merge branch 'remove-unsafePerformIO' into init-tables
adrianmay Mar 19, 2025
8a4d067
Add workflow-dispatch trigger so I can manually start CI
adrianmay Mar 19, 2025
25ee0d9
Typo
adrianmay Mar 19, 2025
dd99b5f
Another typo
adrianmay Mar 19, 2025
31507fb
Merge branch 'master' into remove-unsafePerformIO
adrianmay Mar 20, 2025
fde8efd
Merge branch 'remove-unsafePerformIO' into init-tables
adrianmay Mar 20, 2025
0e782f7
Undoing workflow_dispatch cos it didn't do anything
adrianmay Mar 20, 2025
c4781a5
Merge branch 'remove-unsafePerformIO' into init-tables
adrianmay Mar 20, 2025
2fd7156
Removing the irrelevant distraction of py310
adrianmay Mar 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog for fec

## 2.0.0 (2025-03-17)

`fec_init` completely removed because the global tables are initialized data, i.e., the values in the tables are listed in C.

## 1.0 0 (2025-03-17)

* Haskell wrapper returns IO throughout to avoid unsafePerformIO

## 0.2.0 (2023-10-06)

* Application code must now execute the `Codec.FEC.initialize` action at least
20 changes: 8 additions & 12 deletions benchmark-zfec/Main.hs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module Main where

import Codec.FEC (FECParams (paramK, paramN), decode, encode, fec, initialize)
import Codec.FEC (FECParams (paramK, paramN), decode, encode, fec)
import Control.Monad (replicateM)
import Criterion.Main (Benchmark, bench, bgroup, defaultMain, env, nf)
import Criterion.Main (Benchmark, bench, bgroup, defaultMain, env, nfAppIO)
import Data.Bifunctor (bimap)
import qualified Data.ByteString as B
import Data.List (unfoldr)
@@ -13,15 +13,11 @@ main =
defaultMain
-- Run against some somewhat arbitrarily chosen configurations. Notably,
-- though, 94/100 matches the numbers recorded in the readme.
[ env (setupFEC 2 3) makeFECBenchmarks
, env (setupFEC 16 31) makeFECBenchmarks
, env (setupFEC 94 100) makeFECBenchmarks
[ env (fec 2 3) makeFECBenchmarks
, env (fec 16 31) makeFECBenchmarks
, env (fec 94 100) makeFECBenchmarks
]
where
setupFEC :: Int -> Int -> IO FECParams
setupFEC k n = do
initialize
pure (fec k n)

makeFECBenchmarks = fecGroup [10 ^ 6]

@@ -53,20 +49,20 @@ main =
-- result is serialize use all of the bytes (eg, to write them to a
-- file or send them over the network) so they will certainly all be
-- used.
nf (uncurry encode) (params, blocks)
nfAppIO (uncurry encode) (params, blocks)

benchmarkPrimaryDecode params blocks =
bench ("decode [0..] blockSize=" <> showWithUnit (B.length $ head blocks)) $
-- normal form here for the same reason as in benchmarkEncode.
-- assign block numbers to use only primary blocks
nf (uncurry decode) (params, (zip [0 ..] blocks))
nfAppIO (uncurry decode) (params, (zip [0 ..] blocks))

benchmarkSecondaryDecode params blocks =
bench ("decode [" <> show n <> "..] blockSize=" <> showWithUnit (B.length $ head blocks)) $
-- normal form here for the same reason as in benchmarkEncode.
-- assign block numbers to use as many non-primary blocks as
-- possible
nf (uncurry decode) (params, (zip [n ..] blocks))
nfAppIO (uncurry decode) (params, (zip [n ..] blocks))
where
n = paramN params - paramK params

32 changes: 16 additions & 16 deletions fec.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: 3.0
name: fec
version: 0.2.0
version: 2.0.0
license: GPL-2.0-or-later
license-file: README.rst
author: Adam Langley <agl@imperialviolet.org>
@@ -31,10 +31,10 @@ extra-doc-files: ChangeLog.md

library
build-depends:
, base >=4.9 && <5
, bytestring >=0.10 && <0.13
, deepseq >=1.4 && <1.6
, extra >=1.7 && <1.8
, base <5
, bytestring <0.13
, deepseq <1.7
, extra <2

exposed-modules: Codec.FEC
default-language: Haskell2010
@@ -49,11 +49,11 @@ executable benchmark-zfec
main-is: Main.hs
ghc-options: -threaded
build-depends:
, base >=4.9 && <5
, bytestring >=0.10 && <0.13
, criterion >=1.1 && <1.7
, base <5
, bytestring <0.13
, criterion <1.7
, fec
, random >=1.1 && <1.3
, random <2

hs-source-dirs: benchmark-zfec
default-language: Haskell2010
@@ -65,13 +65,13 @@ test-suite tests
hs-source-dirs: haskell/test
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
build-depends:
, base >=4.9 && <5
, bytestring >=0.10 && <0.13
, data-serializer >=0.3 && <0.4
, base <5
, bytestring <0.13
, data-serializer <1
, fec
, hspec >=2.7 && <2.12
, QuickCheck >=2.14 && <2.15
, quickcheck-instances >=0.3 && <0.4
, random >=1.1 && <1.3
, hspec <3
, QuickCheck <2.16
, quickcheck-instances <1
, random <2

default-language: Haskell2010
243 changes: 79 additions & 164 deletions haskell/Codec/FEC.hs
Original file line number Diff line number Diff line change
@@ -21,7 +21,6 @@
-}
module Codec.FEC (
FECParams (paramK, paramN),
initialize,
fec,
encode,
decode,
@@ -33,27 +32,20 @@ module Codec.FEC (
deFEC,
) where

import Control.Concurrent.Extra (Lock, newLock, withLock)
import Control.DeepSeq (NFData (rnf))
import Control.Exception (Exception, throwIO)
import Data.Bits (xor)
import qualified Data.ByteString as B
import qualified Data.ByteString.Unsafe as BU
import Data.List (nub, partition, sortBy, (\\))
import Data.Word (Word8)
import Foreign.C.Types (CSize (..), CUInt (..))
import Foreign.ForeignPtr (
ForeignPtr,
newForeignPtr,
withForeignPtr,
)
import Foreign.ForeignPtr ( ForeignPtr, newForeignPtr, withForeignPtr,)
import Foreign.Marshal.Alloc (allocaBytes)
import Foreign.Marshal.Array (advancePtr, withArray)
import Foreign.Ptr (FunPtr, Ptr, castPtr, nullPtr)
import Foreign.Ptr (FunPtr, Ptr, castPtr)
import Foreign.Storable (poke, sizeOf)
import GHC.Generics (Generic)
import System.IO (IOMode (..), withFile)
import System.IO.Unsafe (unsafePerformIO)

data CFEC
data FECParams = FECParams
@@ -76,8 +68,6 @@ instance NFData FECParams where
instance Show FECParams where
show (FECParams _ k n) = "FEC (" ++ show k ++ ", " ++ show n ++ ")"

foreign import ccall unsafe "fec_init"
_init :: IO ()
foreign import ccall unsafe "fec_new"
_new ::
-- | k
@@ -122,46 +112,18 @@ isValidConfig k n
| n > 255 = False
| otherwise = True

{- | The underlying library signaled that it has not been properly initialized
yet. Use @initialize@ to initialize it.
-}
data Uninitialized = Uninitialized deriving (Ord, Eq, Show)

instance Exception Uninitialized

-- A lock to ensure at most one thread attempts to initialize the underlying
-- library at a time. Multiple initializations are harmless but concurrent
-- initializations are disallowed.
_initializationLock :: Lock
{-# NOINLINE _initializationLock #-}
_initializationLock = unsafePerformIO newLock

-- | Initialize the library. This must be done before other APIs can succeed.
initialize :: IO ()
initialize = withLock _initializationLock _init

-- | Return a FEC with the given parameters.
fec ::
-- | the number of primary blocks
Int ->
-- | the total number blocks, must be < 256
Int ->
FECParams
fec :: Int -- ^ the number of primary blocks
-> Int -- ^ the total number blocks, must be < 256
-> IO FECParams
fec k n =
if not (isValidConfig k n)
then error $ "Invalid FEC parameters: " ++ show k ++ " " ++ show n
else
unsafePerformIO
( do
cfec' <- _new (fromIntegral k) (fromIntegral n)
-- new will return null if the library hasn't been
-- initialized.
if cfec' == nullPtr
then throwIO Uninitialized
else do
params <- newForeignPtr _free cfec'
return $ FECParams params k n
)
if not (isValidConfig k n)
then error $ "Invalid FEC parameters: " ++ show k ++ " " ++ show n
else do
cfec <- _new (fromIntegral k) (fromIntegral n)
params <- newForeignPtr _free cfec
return $ FECParams params k n

-- | Create a C array of unsigned from an input array
uintCArray :: [Int] -> (Ptr CUInt -> IO a) -> IO a
@@ -213,42 +175,22 @@ createByteStringArray n size f = do
)
)

{- | Generate the secondary blocks from a list of the primary blocks. The
primary blocks must be in order and all of the same size. There must be
@k@ primary blocks.
-}
encode ::
FECParams ->
-- | a list of @k@ input blocks
[B.ByteString] ->
-- | (n - k) output blocks
[B.ByteString]
-- | Generate the secondary blocks from a list of the primary blocks. The
-- primary blocks must be in order and all of the same size. There must be
-- @k@ primary blocks.
encode :: FECParams
-> [B.ByteString] -- ^ a list of @k@ input blocks
-> IO [B.ByteString] -- ^ (n - k) output blocks
encode (FECParams params k n) inblocks
| length inblocks /= k = error "Wrong number of blocks to FEC encode"
| not (allByteStringsSameLength inblocks) = error "Not all inputs to FEC encode are the same length"
| otherwise =
unsafePerformIO
( do
let sz = B.length $ head inblocks
withForeignPtr
params
( \cfec' -> do
byteStringsToArray
inblocks
( \src -> do
createByteStringArray
(n - k)
sz
( \fecs -> do
uintCArray
[k .. (n - 1)]
( \block_nums -> do
_encode cfec' src fecs block_nums (fromIntegral (n - k)) $ fromIntegral sz
)
)
)
)
)
| length inblocks /= k = error "Wrong number of blocks to FEC encode"
| not (allByteStringsSameLength inblocks) = error "Not all inputs to FEC encode are the same length"
| otherwise = do
let sz = B.length $ head inblocks
withForeignPtr params (\cfec -> do
byteStringsToArray inblocks (\src -> do
createByteStringArray (n - k) sz (\fecs -> do
uintCArray [k..(n - 1)] (\block_nums -> do
_encode cfec src fecs block_nums (fromIntegral (n - k)) $ fromIntegral sz))))

-- | A sort function for tagged assoc lists
sortTagged :: [(Int, a)] -> [(Int, a)]
@@ -268,50 +210,29 @@ reorderPrimaryBlocks n blocks = inner (sortTagged pBlocks) sBlocks []
then inner ps sBlocks' (acc ++ [(tag, a)])
else inner pBlocks' ss (acc ++ [s])

{- | Recover the primary blocks from a list of @k@ blocks. Each block must be
tagged with its number (see the module comments about block numbering)
-}
decode ::
FECParams ->
-- | a list of @k@ blocks and their index
[(Int, B.ByteString)] ->
-- | a list the @k@ primary blocks
[B.ByteString]
-- | Recover the primary blocks from a list of @k@ blocks. Each block must be
-- tagged with its number (see the module comments about block numbering)
decode :: FECParams
-> [(Int, B.ByteString)] -- ^ a list of @k@ blocks and their index
-> IO [B.ByteString] -- ^ a list the @k@ primary blocks
decode (FECParams params k n) inblocks
| length (nub $ map fst inblocks) /= length inblocks = error "Duplicate input blocks in FEC decode"
| any ((\f -> f < 0 || f >= n) . fst) inblocks = error "Invalid block numbers in FEC decode"
| length inblocks /= k = error "Wrong number of blocks to FEC decode"
| not (allByteStringsSameLength $ map snd inblocks) = error "Not all inputs to FEC decode are same length"
| otherwise =
unsafePerformIO
( do
let sz = B.length $ snd $ head inblocks
inblocks' = reorderPrimaryBlocks k inblocks
presentBlocks = map fst inblocks'
withForeignPtr
params
( \cfec' -> do
byteStringsToArray
(map snd inblocks')
( \src -> do
b <-
createByteStringArray
(n - k)
sz
( \out -> do
uintCArray
presentBlocks
( \block_nums -> do
_decode cfec' src out block_nums $ fromIntegral sz
)
)
let blocks = [0 .. (n - 1)] \\ presentBlocks
tagged = zip blocks b
allBlocks = sortTagged $ tagged ++ inblocks'
return $ take k $ map snd allBlocks
)
)
)
| length (nub $ map fst inblocks) /= length (inblocks) = error "Duplicate input blocks in FEC decode"
| any (\f -> f < 0 || f >= n) $ map fst inblocks = error "Invalid block numbers in FEC decode"
| length inblocks /= k = error "Wrong number of blocks to FEC decode"
| not (allByteStringsSameLength $ map snd inblocks) = error "Not all inputs to FEC decode are same length"
| otherwise = do
let sz = B.length $ snd $ head inblocks
inblocks' = reorderPrimaryBlocks k inblocks
presentBlocks = map fst inblocks'
withForeignPtr params (\cfec -> do
byteStringsToArray (map snd inblocks') (\src -> do
b <- createByteStringArray (n - k) sz (\out -> do
uintCArray presentBlocks (\block_nums -> do
_decode cfec src out block_nums $ fromIntegral sz))
let blocks = [0..(n - 1)] \\ presentBlocks
tagged = zip blocks b
allBlocks = sortTagged $ tagged ++ inblocks'
return $ take k $ map snd allBlocks))

{- | Break a ByteString into @n@ parts, equal in length to the original, such
that all @n@ are required to reconstruct the original, but having less
@@ -355,50 +276,44 @@ secureCombine [a] = a
secureCombine [a, b] = B.pack $ B.zipWith xor a b
secureCombine (a : rest) = B.pack $ B.zipWith xor a $ secureCombine rest

{- | A utility function which takes an arbitary input and FEC encodes it into a
number of blocks. The order the resulting blocks doesn't matter so long
as you have enough to present to @deFEC@.
-}
enFEC ::
-- | the number of blocks required to reconstruct
Int ->
-- | the total number of blocks
Int ->
-- | the data to divide
B.ByteString ->
-- | the resulting blocks
[B.ByteString]
enFEC k n input = taggedPrimaryBlocks ++ taggedSecondaryBlocks
-- | A utility function which takes an arbitary input and FEC encodes it into a
-- number of blocks. The order the resulting blocks doesn't matter so long
-- as you have enough to present to @deFEC@.
enFEC :: Int -- ^ the number of blocks required to reconstruct
-> Int -- ^ the total number of blocks
-> B.ByteString -- ^ the data to divide
-> IO [B.ByteString] -- ^ the resulting blocks
enFEC k n input = do
params <- fec k n
secondaryBlocks <- encode params primaryBlocks
pure $ taggedPrimaryBlocks ++ (taggedSecondaryBlocks secondaryBlocks)
where
taggedPrimaryBlocks = zipWith B.cons [0 ..] primaryBlocks
taggedSecondaryBlocks = zipWith B.cons [(fromIntegral k) ..] secondaryBlocks
taggedPrimaryBlocks = map (uncurry B.cons) $ zip [0..] primaryBlocks
taggedSecondaryBlocks sb = map (uncurry B.cons) $ zip [(fromIntegral k)..] sb
remainder = B.length input `mod` k
paddingLength = if remainder >= 1 then k - remainder else k
paddingBytes = B.replicate (paddingLength - 1) 0 `B.append` B.singleton (fromIntegral paddingLength)
paddingLength = if remainder >= 1 then (k - remainder) else k
paddingBytes = (B.replicate (paddingLength - 1) 0) `B.append` (B.singleton $ fromIntegral paddingLength)
divide a bs
| B.null bs = []
| otherwise = B.take a bs : divide a (B.drop a bs)
| otherwise = (B.take a bs) : (divide a $ B.drop a bs)
input' = input `B.append` paddingBytes
blockSize = B.length input' `div` k
primaryBlocks = divide blockSize input'
secondaryBlocks = encode params primaryBlocks
params = fec k n


-- | Reverses the operation of @enFEC@.
deFEC ::
-- | the number of blocks required (matches call to @enFEC@)
Int ->
-- | the total number of blocks (matches call to @enFEC@)
Int ->
-- | a list of k, or more, blocks from @enFEC@
[B.ByteString] ->
B.ByteString
deFEC :: Int -- ^ the number of blocks required (matches call to @enFEC@)
-> Int -- ^ the total number of blocks (matches call to @enFEC@)
-> [B.ByteString] -- ^ a list of k, or more, blocks from @enFEC@
-> IO B.ByteString
deFEC k n inputs
| length inputs < k = error "Too few inputs to deFEC"
| otherwise = B.take (B.length fecOutput - paddingLength) fecOutput
where
paddingLength = fromIntegral $ B.last fecOutput
inputs' = take k inputs
taggedInputs = map (\bs -> (fromIntegral $ B.head bs, B.tail bs)) inputs'
fecOutput = B.concat $ decode params taggedInputs
params = fec k n
| length inputs < k = error "Too few inputs to deFEC"
| otherwise =
let
paddingLength output = fromIntegral $ B.last output
inputs' = take k inputs
taggedInputs = map (\bs -> (fromIntegral $ B.head bs, B.tail bs)) inputs'
in do
params <- fec k n
fecOutput <- B.concat <$> decode params taggedInputs
pure $ B.take (B.length fecOutput - paddingLength fecOutput) fecOutput
104 changes: 51 additions & 53 deletions haskell/test/FECTest.hs
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
{-# LANGUAGE DerivingStrategies #-}


module Main where

import Test.Hspec (describe, hspec, it, parallel)
import Test.Hspec (describe, hspec, it, parallel, Expectation, shouldBe)

import Control.Monad (replicateM_)

import qualified Codec.FEC as FEC
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Data.List (sortOn)
import Data.Word (Word16, Word8)
import System.Random (Random (randoms), mkStdGen)
import Test.QuickCheck (
Arbitrary (arbitrary),
Property,
Testable (property),
choose,
conjoin,
once,
withMaxSuccess,
(===),
)
import Test.QuickCheck ( Arbitrary (arbitrary), Property, Testable (property), choose, once, withMaxSuccess)
import Test.QuickCheck.Monadic (assert, monadicIO, run)

-- Imported for the orphan Arbitrary ByteString instance.

import Control.Monad (replicateM_)
import Test.QuickCheck.Instances.ByteString ()

-- | Valid ZFEC parameters.
@@ -36,9 +28,10 @@ data Params = Params

-- | A somewhat efficient generator for valid ZFEC parameters.
instance Arbitrary Params where
arbitrary =
choose (1, 255)
>>= \req -> Params req <$> choose (req, 255)
arbitrary = do
req <- choose (1, 255)
tot <- choose (req, 255)
return $ Params req tot

randomTake :: Int -> Int -> [a] -> [a]
randomTake seed n values = map snd $ take n sortedValues
@@ -63,24 +56,26 @@ testFEC ::
Int ->
-- | True if the encoded input was reconstructed by decoding, False
-- otherwise.
Bool
testFEC fec len seed = FEC.decode fec someTaggedBlocks == origBlocks
where
-- Construct some blocks. Each will just be the byte corresponding to the
-- block number repeated to satisfy the requested length.
origBlocks = B.replicate (fromIntegral len) . fromIntegral <$> [0 .. (FEC.paramK fec - 1)]

Expectation
testFEC fec len seed = do
-- Encode the data to produce the "secondary" blocks which (might) add
-- redundancy to the original blocks.
secondaryBlocks = FEC.encode fec origBlocks
secondaryBlocks <- FEC.encode fec origBlocks

-- Tag each block with its block number because the decode API requires
-- this information.
taggedBlocks = zip [0 ..] (origBlocks ++ secondaryBlocks)
let -- Tag each block with its block number because the decode API requires
-- this information.
taggedBlocks = zip [0 ..] (origBlocks ++ secondaryBlocks)

-- Choose enough of the tagged blocks (some combination of original and
-- secondary) to try to use for decoding.
someTaggedBlocks = randomTake seed (FEC.paramK fec) taggedBlocks
-- Choose enough of the tagged blocks (some combination of original and
-- secondary) to try to use for decoding.
someTaggedBlocks = randomTake seed (FEC.paramK fec) taggedBlocks

decoded <- FEC.decode fec someTaggedBlocks
decoded `shouldBe` origBlocks
where
-- Construct some blocks. Each will just be the byte corresponding to the
-- block number repeated to satisfy the requested length.
origBlocks = B.replicate (fromIntegral len) . fromIntegral <$> [0 .. (FEC.paramK fec - 1)]

-- | @FEC.secureDivide@ is the inverse of @FEC.secureCombine@.
prop_divide :: Word16 -> Word8 -> Word8 -> Property
@@ -91,30 +86,27 @@ prop_divide size byte divisor = monadicIO $ do

-- | @FEC.encode@ is the inverse of @FEC.decode@.
prop_decode :: Params -> Word16 -> Int -> Property
prop_decode (Params req tot) len seed = property $ do
testFEC fec len seed === True
where
fec = FEC.fec req tot
prop_decode (Params req tot) len seed =
monadicIO . run $ do
fec <- FEC.fec req tot
testFEC fec len seed

-- | @FEC.enFEC@ is the inverse of @FEC.deFEC@.
prop_deFEC :: Params -> B.ByteString -> Property
prop_deFEC (Params req tot) testdata =
FEC.deFEC req tot minimalShares === testdata
where
allShares = FEC.enFEC req tot testdata
minimalShares = take req allShares
prop_deFEC (Params req tot) testdata = monadicIO $ do
encoded <- run $ FEC.enFEC req tot testdata
decoded <- run $ FEC.deFEC req tot (take req encoded)
assert $ testdata == decoded

prop_primary_copies :: Params -> BL.ByteString -> Property
prop_primary_copies (Params _ tot) primary = property $ do
conjoin $ (BL.toStrict primary ===) <$> secondary
where
fec = FEC.fec 1 tot
secondary = FEC.encode fec [BL.toStrict primary]
prop_primary_copies :: Params -> B.ByteString -> Property
prop_primary_copies (Params _ tot) primary = monadicIO $ do
fec <- run $ FEC.fec 1 tot
secondary <- run $ FEC.encode fec [primary]
assert $ all (primary ==) secondary

main :: IO ()
main = do
-- Be sure to do the required zfec initialization first.
FEC.initialize
hspec . parallel $ do
describe "encode" $ do
-- This test originally caught a bug in multi-threaded
@@ -143,11 +135,17 @@ main = do
it "is the inverse of secureDivide n" $ once $ prop_divide 1024 65 3

describe "deFEC" $ do
replicateM_ 10 $
it "is the inverse of enFEC" $
property prop_deFEC
it "is the inverse of enFEC" $ withMaxSuccess 200 prop_deFEC

describe "decode" $ do
it "is (nearly) the inverse of encode" $ withMaxSuccess 200 prop_decode
it "works with total=255" $ property $ prop_decode (Params 1 255)
it "works with required=255" $ property $ prop_decode (Params 255 255)

describe "decode" $
replicateM_ 10 $ do
it "is (nearly) the inverse of encode" $ property $ prop_decode
it "works with required=255" $ property $ prop_decode (Params 255 255)
describe "encode" $ do
-- Since a single property won't result in parallel execution, add a
-- few of these.
replicateM_ 10 $
it "returns copies of the primary block for all 1 of N encodings" $
property $
withMaxSuccess 10000 prop_primary_copies
6 changes: 6 additions & 0 deletions zfec/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# This is not part of the main build but is included for reference only.
# It is used to generate tables.c, but that's been done and the result committed to source control.

tables.c: write.c fec.c fec.h Makefile
touch tables.c
gcc -o write write.c && ./write > tables.c
Comment on lines +4 to +6
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

2 changes: 0 additions & 2 deletions zfec/_fecmodule.c
Original file line number Diff line number Diff line change
@@ -706,8 +706,6 @@ init_fec(void) {
py_fec_error = PyErr_NewException("_fec.Error", NULL, NULL);
PyDict_SetItemString(module_dict, "Error", py_fec_error);

fec_init();

#if PY_MAJOR_VERSION >= 3
return module;
#endif
113 changes: 1 addition & 112 deletions zfec/fec.c
Original file line number Diff line number Diff line change
@@ -8,12 +8,8 @@
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "tables.c"

/*
* Primitive polynomials - see Lin & Costello, Appendix A,
* and Lee & Messerschmitt, p. 453.
*/
static const char*const Pp="101110001";


/*
@@ -26,7 +22,6 @@ static const char*const Pp="101110001";
*/

static gf gf_exp[510]; /* index->poly form conversion table */
static int gf_log[256]; /* Poly->index form conversion table */
static gf inverse[256]; /* inverse of field elem. */
/* inv[\alpha**i]=\alpha**(GF_SIZE-i-1) */

@@ -64,93 +59,9 @@ static gf gf_mul_table[256][256];
#define GF_MULC0(c) __gf_mulc_ = gf_mul_table[c]
#define GF_ADDMULC(dst, x) dst ^= __gf_mulc_[x]

/*
* Generate GF(2**m) from the irreducible polynomial p(X) in p[0]..p[m]
* Lookup tables:
* index->polynomial form gf_exp[] contains j= \alpha^i;
* polynomial form -> index form gf_log[ j = \alpha^i ] = i
* \alpha=x is the primitive element of GF(2^m)
*
* For efficiency, gf_exp[] has size 2*GF_SIZE, so that a simple
* multiplication of two numbers can be resolved without calling modnn
*/
static void
_init_mul_table(void) {
int i, j;
for (i = 0; i < 256; i++)
for (j = 0; j < 256; j++)
gf_mul_table[i][j] = gf_exp[modnn (gf_log[i] + gf_log[j])];

for (j = 0; j < 256; j++)
gf_mul_table[0][j] = gf_mul_table[j][0] = 0;
}

#define NEW_GF_MATRIX(rows, cols) \
(gf*)malloc(rows * cols)

/*
* initialize the data structures used for computations in GF.
*/
static void
generate_gf (void) {
int i;
gf mask;

mask = 1; /* x ** 0 = 1 */
gf_exp[8] = 0; /* will be updated at the end of the 1st loop */
/*
* first, generate the (polynomial representation of) powers of \alpha,
* which are stored in gf_exp[i] = \alpha ** i .
* At the same time build gf_log[gf_exp[i]] = i .
* The first 8 powers are simply bits shifted to the left.
*/
for (i = 0; i < 8; i++, mask <<= 1) {
gf_exp[i] = mask;
gf_log[gf_exp[i]] = i;
/*
* If Pp[i] == 1 then \alpha ** i occurs in poly-repr
* gf_exp[8] = \alpha ** 8
*/
if (Pp[i] == '1')
gf_exp[8] ^= mask;
}
/*
* now gf_exp[8] = \alpha ** 8 is complete, so can also
* compute its inverse.
*/
gf_log[gf_exp[8]] = 8;
/*
* Poly-repr of \alpha ** (i+1) is given by poly-repr of
* \alpha ** i shifted left one-bit and accounting for any
* \alpha ** 8 term that may occur when poly-repr of
* \alpha ** i is shifted.
*/
mask = 1 << 7;
for (i = 9; i < 255; i++) {
if (gf_exp[i - 1] >= mask)
gf_exp[i] = gf_exp[8] ^ ((gf_exp[i - 1] ^ mask) << 1);
else
gf_exp[i] = gf_exp[i - 1] << 1;
gf_log[gf_exp[i]] = i;
}
/*
* log(0) is not defined, so use a special value
*/
gf_log[0] = 255;
/* set the extended gf_exp values for fast multiply */
for (i = 0; i < 255; i++)
gf_exp[i + 255] = gf_exp[i];

/*
* again special cases. 0 has no inverse. This used to
* be initialized to 255, but it should make no difference
* since noone is supposed to read from here.
*/
inverse[0] = 0;
inverse[1] = 1;
for (i = 2; i <= 255; i++)
inverse[i] = gf_exp[255 - gf_log[i]];
}

/*
* Various linear algebra operations that i use often.
@@ -393,24 +304,6 @@ _invert_vdm (gf* src, unsigned k) {
return;
}

/* There are few (if any) ordering guarantees that apply to reads and writes
* of this static int across threads. This is the reason for some of the
* tight requirements for how `fec_init` is called. If we could use a mutex
* or a C11 atomic here we might be able to provide more flexibility to
* callers. It's tricky to do that while remaining compatible with all of
* macOS/Linux/Windows and CPython's MSVC requirements and not switching to
* C++ (or something even more different).
*/
static int fec_initialized = 0;

void
fec_init (void) {
if (fec_initialized == 0) {
generate_gf();
_init_mul_table();
fec_initialized = 1;
}
}

/*
* This section contains the proper FEC encoding/decoding routines.
@@ -439,10 +332,6 @@ fec_new(unsigned short k, unsigned short n) {
assert(n <= 256);
assert(k <= n);

if (fec_initialized == 0) {
return NULL;
}

retval = (fec_t *) malloc (sizeof (fec_t));
retval->k = k;
retval->n = n;
12 changes: 0 additions & 12 deletions zfec/fec.h
Original file line number Diff line number Diff line change
@@ -20,18 +20,6 @@ typedef struct {
#define restrict
#endif

/** Initialize the fec library.
*
* Call this:
*
* - at least once
* - from at most one thread at a time
* - before calling any other APIs from the library
* - before creating any other threads that will use APIs from the library
*
*/
void fec_init(void);

/**
* param k the number of blocks required to reconstruct
* param m the total number of blocks created
262 changes: 262 additions & 0 deletions zfec/tables.c

Large diffs are not rendered by default.

150 changes: 150 additions & 0 deletions zfec/write.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// This is not part of the main build but is included for reference onby.
// It's used to generate tables.c, but that's been done and the result committed to source control.

#include "fec.c"

/*
* Primitive polynomials - see Lin & Costello, Appendix A,
* and Lee & Messerschmitt, p. 453.
*/
static const char*const Pp="101110001";
static int gf_log[256]; /* Poly->index form conversion table */

/*
* Generate GF(2**m) from the irreducible polynomial p(X) in p[0]..p[m]
* Lookup tables:
* index->polynomial form gf_exp[] contains j= \alpha^i;
* polynomial form -> index form gf_log[ j = \alpha^i ] = i
* \alpha=x is the primitive element of GF(2^m)
*
* For efficiency, gf_exp[] has size 2*GF_SIZE, so that a simple
* multiplication of two numbers can be resolved without calling modnn
*/
static void
_init_mul_table(void) {
int i, j;
for (i = 0; i < 256; i++)
for (j = 0; j < 256; j++)
gf_mul_table[i][j] = gf_exp[modnn (gf_log[i] + gf_log[j])];

for (j = 0; j < 256; j++)
gf_mul_table[0][j] = gf_mul_table[j][0] = 0;
}

/*
* initialize the data structures used for computations in GF.
*/
static void
generate_gf (void) {
int i;
gf mask;

mask = 1; /* x ** 0 = 1 */
gf_exp[8] = 0; /* will be updated at the end of the 1st loop */
/*
* first, generate the (polynomial representation of) powers of \alpha,
* which are stored in gf_exp[i] = \alpha ** i .
* At the same time build gf_log[gf_exp[i]] = i .
* The first 8 powers are simply bits shifted to the left.
*/
for (i = 0; i < 8; i++, mask <<= 1) {
gf_exp[i] = mask;
gf_log[gf_exp[i]] = i;
/*
* If Pp[i] == 1 then \alpha ** i occurs in poly-repr
* gf_exp[8] = \alpha ** 8
*/
if (Pp[i] == '1')
gf_exp[8] ^= mask;
}
/*
* now gf_exp[8] = \alpha ** 8 is complete, so can also
* compute its inverse.
*/
gf_log[gf_exp[8]] = 8;
/*
* Poly-repr of \alpha ** (i+1) is given by poly-repr of
* \alpha ** i shifted left one-bit and accounting for any
* \alpha ** 8 term that may occur when poly-repr of
* \alpha ** i is shifted.
*/
mask = 1 << 7;
for (i = 9; i < 255; i++) {
if (gf_exp[i - 1] >= mask)
gf_exp[i] = gf_exp[8] ^ ((gf_exp[i - 1] ^ mask) << 1);
else
gf_exp[i] = gf_exp[i - 1] << 1;
gf_log[gf_exp[i]] = i;
}
/*
* log(0) is not defined, so use a special value
*/
gf_log[0] = 255;
/* set the extended gf_exp values for fast multiply */
for (i = 0; i < 255; i++)
gf_exp[i + 255] = gf_exp[i];

/*
* again special cases. 0 has no inverse. This used to
* be initialized to 255, but it should make no difference
* since noone is supposed to read from here.
*/
inverse[0] = 0;
inverse[1] = 1;
for (i = 2; i <= 255; i++)
inverse[i] = gf_exp[255 - gf_log[i]];
}


/* This is the logic for setting up gf_exp, gf_log, gf_inverse and gf_mul_table.
* It is not used at run time but has been invoked by write.c to generate
* tables.c in which the contents of these tables are initialised verbatim.
*/
void
fec_init_internal (void) {
generate_gf();
_init_mul_table();
}

/* The above comprises all code used purely for table generation.
* Now we just print the results
*/

void print_table_int(const char * name, const int data[], int len) {
printf("static int %s[%d]={", name, len);
printf("%d", data[0]);
for (int i=1; i<len; i++) printf(",%d", data[i]);
printf("};\n\n");
}

void print_gfs(const gf data[], int len) {
printf("{%d", data[0]);
for (int i=1; i<len; i++) printf(",%d", data[i]);
printf("}");
}

void print_table_gf(const char * name, const gf data[], int len) {
printf("static gf %s[%d]=", name, len);
print_gfs(data, len);
printf(";\n\n");
}

void print_table_gf_256_256(const char * name, const gf data[256][256]) {
printf("static gf %s[256][256]={", name);
print_gfs(data[0], 256);
for (int i=1; i<256; i++) {
printf(",\n");
print_gfs(data[i], 256);
}
printf("};\n");
}

int main()
{
fec_init_internal();
print_table_gf("gf_exp", gf_exp, 510);
print_table_int("gf_log", gf_log, 256);
print_table_gf("inverse", inverse, 256);
print_table_gf_256_256("gf_mul_table", gf_mul_table);
}