Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit 54ab34c

Browse files
authored
Merge pull request #1143 from eilseq/tidal-parse-ffi
Enable Tidal-Parse FFI for Cross-Language Integration
2 parents 08d4a8b + c5c2f58 commit 54ab34c

File tree

10 files changed

+989
-2
lines changed

10 files changed

+989
-2
lines changed

cabal.project

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
tests: True
2-
packages: ./ tidal-parse tidal-listener tidal-link
2+
packages: ./ tidal-parse tidal-parse-ffi tidal-listener tidal-link

tidal-parse-ffi/.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
dist
2+
dist-*
3+
cabal-dev
4+
*.o
5+
*.hi
6+
*.chi
7+
*.chs.h
8+
*.dyn_o
9+
*.dyn_hi
10+
.hpc
11+
.hsenv
12+
.cabal-sandbox/
13+
cabal.sandbox.config
14+
*.prof
15+
*.aux
16+
*.hp
17+
*.eventlog
18+
.stack-work/
19+
cabal.project.local
20+
cabal.project.local~
21+
.HTF/
22+
.ghc.environment.*
23+
dist-newstyle

tidal-parse-ffi/CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Changelog for tidal-parse-ffi
2+
3+
## Unreleased
4+
5+
### Added
6+
7+
- FFI bindings for `tidal-parse`.
8+
- `cabal.project` integration.
9+
10+
### Changed
11+
12+
- Updated dependencies and metadata.
13+
- Performance improvements.
14+
15+
### Fixed
16+
17+
- Stability and bug fixes.

tidal-parse-ffi/LICENSE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

tidal-parse-ffi/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# tidal-parse-ffi
2+
3+
`tidal-parse-ffi` is a Haskell library providing a Foreign Function Interface (FFI) for parsing Tidal patterns and exporting them to JSON format. It is designed to be used in conjunction with Rust and other languages via FFI.
4+
5+
## Features
6+
7+
- Exposes Haskell Tidal parsing functions through FFI.
8+
- Converts parsed patterns into JSON format.
9+
- Supports Rust integration via `libc`.
10+
11+
## Dependencies
12+
13+
- `tidal-parse`
14+
- `base`
15+
- `aeson`
16+
- `bytestring`
17+
- `containers`
18+
- `tidal`
19+
20+
## Installation
21+
22+
Clone the repository and navigate to the `tidal-parse-ffi` directory:
23+
24+
```sh
25+
cd tidal-parse-ffi
26+
cabal build
27+
```
28+
29+
## Usage
30+
31+
Include `tidal-parse-ffi` as a dependency in your Cabal project:
32+
33+
```cabal
34+
build-depends: tidal-parse-ffi
35+
```
36+
37+
### Haskell Example
38+
39+
```haskell
40+
import Foreign.C.String (newCString)
41+
main = do
42+
result <- eval_pattern_c "[bd sn]"
43+
putStrLn =<< peekCString result
44+
```
45+
46+
## Rust Integration
47+
48+
To use this library in Rust:
49+
50+
1. Add the dependency to `Cargo.toml`:
51+
```toml
52+
[dependencies]
53+
tidal-parse-ffi = { path = "../tidal-parse-ffi" }
54+
```
55+
2. Link the library in `build.rs`:
56+
```rust
57+
println!("cargo:rustc-link-lib=static=tidalparseffi");
58+
```
59+
60+
### Rust Example
61+
62+
```rust
63+
use std::ffi::{CString, CStr};
64+
use std::os::raw::c_char;
65+
66+
extern "C" {
67+
fn eval_pattern_c(input: *const c_char) -> *mut c_char;
68+
}
69+
70+
fn main() {
71+
let input = CString::new("[bd sn]").expect("CString::new failed");
72+
unsafe {
73+
let result_ptr = eval_pattern_c(input.as_ptr());
74+
let result = CStr::from_ptr(result_ptr).to_string_lossy().into_owned();
75+
println!("Parsed Pattern: {}", result);
76+
}
77+
}
78+
```
79+
80+
The library provides a single exported function:
81+
82+
```haskell
83+
foreign export ccall eval_pattern_c :: CString -> IO CString
84+
```
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
{-# LANGUAGE ForeignFunctionInterface #-}
3+
{-# LANGUAGE DeriveGeneric #-}
4+
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5+
6+
module Sound.Tidal.Parse.FFI where
7+
8+
import Foreign.C.String (CString, peekCString, newCString)
9+
import qualified Data.Aeson as Aeson
10+
import Data.Aeson (ToJSON(..), object, (.=))
11+
import qualified Data.ByteString.Lazy.Char8 as B
12+
import qualified Data.Map.Strict as Map
13+
import Data.Maybe (fromMaybe)
14+
import Text.Read (readMaybe)
15+
import GHC.Generics (Generic)
16+
17+
import Sound.Tidal.Parse (parseTidal)
18+
import Sound.Tidal.Pattern
19+
import Sound.Tidal.Params ()
20+
import Sound.Tidal.Show ()
21+
22+
-- Newtype wrappers to avoid orphan instances
23+
newtype JSONValue = JSONValue { unJSONValue :: Value }
24+
deriving (Generic)
25+
26+
instance ToJSON JSONValue where
27+
toJSON (JSONValue (VS str)) = toJSON str
28+
toJSON (JSONValue (VI i)) = toJSON i
29+
toJSON (JSONValue (VF f)) = toJSON f
30+
toJSON (JSONValue (VN num)) = toJSON $ show num
31+
toJSON (JSONValue (VR r)) = toJSON $ show r
32+
toJSON (JSONValue (VB b)) = toJSON b
33+
toJSON (JSONValue (VX xs)) = toJSON xs
34+
toJSON (JSONValue (VPattern pat)) = toJSON $ show pat
35+
toJSON (JSONValue (VState f)) = toJSON $ show $ f Map.empty
36+
toJSON (JSONValue (VList vs)) = toJSON $ map JSONValue vs
37+
38+
newtype JSONArcF = JSONArcF (ArcF Rational)
39+
deriving (Generic)
40+
41+
instance ToJSON JSONArcF where
42+
toJSON (JSONArcF (Arc arcStart arcStop)) =
43+
object ["start" .= (realToFrac arcStart :: Double),
44+
"stop" .= (realToFrac arcStop :: Double)]
45+
46+
newtype JSONEventF = JSONEventF (Event (Map.Map String Value))
47+
deriving (Generic)
48+
49+
instance ToJSON JSONEventF where
50+
toJSON (JSONEventF (Event _ctx evWhole evPart evValue)) =
51+
object [ "whole" .= fmap JSONArcF evWhole -- Handle Maybe
52+
, "part" .= JSONArcF evPart
53+
, "value" .= fmap JSONValue evValue ]
54+
55+
56+
57+
-- Foreign export wrapper function
58+
foreign export ccall eval_pattern_c :: CString -> CString -> IO CString
59+
eval_pattern_c :: CString -> CString -> IO CString
60+
eval_pattern_c cStr cArc = do
61+
hsStr <- peekCString cStr
62+
arcStr <- peekCString cArc
63+
let arcLength = fromMaybe 16 (readMaybe arcStr :: Maybe Double)
64+
result <- evalPattern hsStr arcLength
65+
newCString result
66+
67+
-- Function to evaluate and return pattern events as a JSON string
68+
evalPattern :: String -> Double -> IO String
69+
evalPattern pat arcLen = do
70+
let parsedResult = parseAndQuery pat arcLen
71+
return $ B.unpack $ Aeson.encode (either encodeError (encodeSuccess arcLen) parsedResult)
72+
73+
encodeError :: String -> Aeson.Value
74+
encodeError err = Aeson.object ["error" Aeson..= err]
75+
76+
encodeSuccess :: Double -> [Event (Map.Map String Value)] -> Aeson.Value
77+
encodeSuccess arcLen events =
78+
Aeson.object ["arcLen" .= arcLen, "events" .= map JSONEventF events]
79+
80+
-- Helper functions to handle parsing and querying
81+
parseAndQuery :: String -> Double -> Either String [Event (Map.Map String Value)]
82+
parseAndQuery str arcLen =
83+
case parseTidal str of
84+
Left err -> Left (show err)
85+
Right parsed ->
86+
let arcTime = toRational arcLen
87+
in Right $ query (stripContext parsed) (State (Arc 0 arcTime) Map.empty)
88+
89+
stripContext :: Pattern a -> Pattern a
90+
stripContext = setContext $ Context []
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
{-# LANGUAGE ForeignFunctionInterface #-}
3+
4+
module Sound.Tidal.TidalParseFFITest where
5+
6+
import Foreign.C.String (CString, peekCString, newCString)
7+
import Test.HUnit
8+
import Data.Aeson (Value, encode, object, (.=))
9+
import qualified Data.ByteString.Lazy.Char8 as B
10+
11+
-- Foreign function import
12+
foreign import ccall "eval_pattern_c" eval_pattern_c :: CString -> CString -> IO CString
13+
14+
-- Utility function to run FFI test
15+
ffiTest :: String -> String -> IO Bool
16+
ffiTest input arcLen = do
17+
cInput <- newCString input
18+
cArcLen <- newCString arcLen
19+
resultPtr <- eval_pattern_c cInput cArcLen
20+
result <- peekCString resultPtr
21+
let expected = B.unpack $ encode mockJSON
22+
return (result == expected)
23+
24+
-- Mock the exact expected JSON output
25+
mockJSON :: Value
26+
mockJSON = object
27+
[ "arcLen" .= (1 :: Int)
28+
, "events" .=
29+
[ object [
30+
"part" .= object ["start" .= (0 :: Double), "stop" .= (0.5 :: Double)]
31+
, "value" .= object ["s" .= ("bd" :: String)]
32+
, "whole" .= object ["start" .= (0 :: Double), "stop" .= (0.5 :: Double)]
33+
]
34+
, object [
35+
"part" .= object ["start" .= (0.5 :: Double), "stop" .= (1 :: Double)]
36+
, "value" .= object ["s" .= ("cd" :: String)]
37+
, "whole" .= object ["start" .= (0.5 :: Double), "stop" .= (1 :: Double)]
38+
]
39+
]
40+
]
41+
42+
-- Test case with the mocked JSON output
43+
testFullPattern :: Test
44+
testFullPattern = TestCase $ do
45+
result <- ffiTest "s $ \"bd cd\"" "1"
46+
assertBool "Full pattern 's $ \"bd cd\"' should return the expected JSON" result

tidal-parse-ffi/test/Test.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
import Test.HUnit
4+
import Sound.Tidal.TidalParseFFITest (testFullPattern)
5+
6+
main :: IO Counts
7+
main = runTestTT $ TestList [testFullPattern]

tidal-parse-ffi/tidal-parse-ffi.cabal

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: tidal-parse-ffi
2+
3+
license: GPL-3
4+
license-file: LICENSE
5+
extra-doc-files: CHANGELOG.md, README.md
6+
7+
version: 0.1.0
8+
build-type: Simple
9+
cabal-version: >=1.10
10+
11+
library
12+
exposed-modules: Sound.Tidal.Parse.FFI
13+
default-language: Haskell2010
14+
15+
ghc-options: -Wall
16+
hs-source-dirs: src
17+
18+
Build-depends:
19+
base
20+
, containers
21+
, tidal-parse
22+
, tidal
23+
, aeson
24+
, bytestring
25+
, vector
26+
27+
test-suite tests
28+
type: exitcode-stdio-1.0
29+
main-is: Test.hs
30+
hs-source-dirs: test
31+
ghc-options: -Wall
32+
other-modules: Sound.Tidal.TidalParseFFITest
33+
build-depends:
34+
base
35+
, containers
36+
, tidal-parse-ffi
37+
, tidal-parse
38+
, tidal
39+
, aeson
40+
, bytestring
41+
, HUnit
42+
, vector
43+
44+
source-repository head
45+
type: git
46+
location: https://github.com/tidalcycles/tidal-parse-ffi

tidal-parse/tidal-parse.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ library
2626
default-language: Haskell2010
2727

2828
Exposed-modules: Sound.Tidal.Parse
29-
other-modules: Sound.Tidal.Parse.TH
29+
other-modules: Sound.Tidal.Parse.TH
3030

3131
Build-depends:
3232
base >=4.8 && <5

0 commit comments

Comments
 (0)