inline-java 0.5.1 → 0.6
raw patch · 6 files changed
+202/−53 lines, 6 filesdep +Cabaldep +directorydep −monad-loopsdep ~basedep ~jni
Dependencies added: Cabal, directory
Dependencies removed: monad-loops
Dependency ranges changed: base, jni
Files
- CHANGELOG.md +37/−0
- README.md +35/−2
- inline-java.cabal +8/−3
- src/Language/Java/Inline.hs +36/−48
- src/Language/Java/Inline/Cabal.hs +78/−0
- tests/Language/Java/InlineSpec.hs +8/−0
+ CHANGELOG.md view
@@ -0,0 +1,37 @@+# Change Log++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](http://keepachangelog.com/).++## [0.6.0] - 2016-12-13++### Added++* Can set a custom `CLASSPATH` in `Setup.hs` from Gradle build+ configurations to use when compiling inline expressions.+* GHC 8 compatibility+* Support inline expressions that compile to multiple .class files+ (e.g for anonymous classes and anonymous function literals).++### Changed++* The return type of inline expressions no longer needs+ `Reify`/`Reflect` instances.++### Fixed++* Fix antiquotation: in [java| $obj.foo() |], `obj` is now recognized+ as an antiquotation variable.+* Passing multiple options to the JVM using `withJVM`.++## [0.5.0] - 2016-12-13++### Added++* First release with support for inline Java expressions.++### Changed++* Split lower-level and mid-level bindings into separate packages: jni+ and jvm.
README.md view
@@ -1,6 +1,6 @@ # inline-java: Call any JVM function from Haskell -[](https://circleci.com/gh/tweag/inline-java)+[](https://app.wercker.com/project/byKey/dfeba78838cc77d4c5e06eedc5c0833d) **NOTE: you'll need GHC >= 8.0.2 to compile and use this package. Use** ```@@ -16,7 +16,7 @@ `inline-r` for calling R, `inline-java` lets you name any function to call inline in your code. -# Example+## Example Graphical Hello World using Java Swing: @@ -39,6 +39,39 @@ [java| { javax.swing.JOptionPane.showMessageDialog(null, $message); return 0; } |] ```++## Building it++**Requirements:**+* the [Stack][stack] build tool (version 1.2 or above);+* either, the [Nix][nix] package manager,+* or, OpenJDK, Gradle and Spark (version 1.6) installed from your distro.++To build:++```+$ stack build+```++You can optionally get Stack to download a JDK in a local sandbox+(using [Nix][nix]) for good build results reproducibility. **This is+the recommended way to build inline-java.** Alternatively, you'll need+it installed through your OS distribution's package manager for the+next steps (and you'll need to tell Stack how to find the JVM header+files and shared libraries).++To use Nix, set the following in your `~/.stack/config.yaml` (or pass+`--nix` to all Stack commands, see the [Stack manual][stack-nix] for+more):++```yaml+nix:+ enable: true+```++[stack]: https://github.com/commercialhaskell/stack+[stack-nix]: https://docs.haskellstack.org/en/stable/nix_integration/#configuration+[nix]: http://nixos.org/nix ## License
inline-java.cabal view
@@ -1,5 +1,5 @@ name: inline-java-version: 0.5.1+version: 0.6 synopsis: Java interop via inline Java code in Haskell modules. description: Please see README.md. homepage: http://github.com/tweag/inline-java#readme@@ -11,7 +11,9 @@ category: FFI, JVM, Java build-type: Simple cabal-version: >=1.10-extra-source-files: README.md+extra-source-files:+ CHANGELOG.md+ README.md extra-tmp-files: src/Foreign/JNI.c @@ -23,12 +25,15 @@ hs-source-dirs: src exposed-modules: Language.Java.Inline+ Language.Java.Inline.Cabal build-depends: -- Can't build at all with GHC < 8.0.2. base > 4.9.0.0 && < 5, binary >=0.7, bytestring >=0.10,+ Cabal >= 1.24, containers >=0.5,+ directory >=1.2, distributed-closure >=0.3, filepath >= 1, ghc-heap-view >= 0.5,@@ -36,7 +41,6 @@ jni >= 0.1, jvm >= 0.1, language-java >= 0.2,- monad-loops >= 0.4, process >= 1.2, singletons >= 2.0, syb >= 0.6,@@ -58,6 +62,7 @@ build-depends: base, bytestring,+ jni, jvm, hspec, inline-java,
src/Language/Java/Inline.hs view
@@ -51,7 +51,6 @@ import Control.Monad (forM_, unless) import Control.Monad.Fix (mfix)-import Control.Monad.Loops (unfoldM) import qualified Data.ByteString.Char8 as BS import Data.Generics (everything, mkQ) import Data.List (intercalate, isPrefixOf, isSuffixOf)@@ -75,10 +74,10 @@ import qualified Language.Java.Syntax as Java import Language.Haskell.TH.Quote import qualified Language.Haskell.TH as TH-import qualified Language.Haskell.TH.Ppr as TH import qualified Language.Haskell.TH.Syntax as TH import Language.Haskell.TH (Q) import System.FilePath ((</>), (<.>))+import System.Directory (listDirectory) import System.IO.Temp (withSystemTempDirectory) import System.IO.Unsafe (unsafePerformIO) import System.Process (callProcess)@@ -119,6 +118,11 @@ -- printf format string). An antiquotation variable @$foo@ is well-scoped if -- there exists a variable with the name @foo@ in the Haskell context of the -- quasiquote, whose type is 'Coercible' to a Java primitive or reference type.+--+-- __NOTE:__ In GHC 8.0.2 and earlier, due to+-- <https://ghc.haskell.org/trac/ghc/ticket/12778 #12778>, a quasiquote must+-- always return a boxed value (i.e. an object, not void or a primitive type).+-- This limitation may be lifted in the future. java :: QuasiQuoter java = QuasiQuoter { quoteExp = \txt -> blockOrExpQQ txt@@ -128,7 +132,7 @@ } antis :: Java.Block -> [String]-antis = everything (++) (mkQ [] (\case Java.Name [Java.Ident ('$':av)] -> [av]; _ -> []))+antis = everything (++) (mkQ [] (\case Java.Name (Java.Ident ('$':av):_) -> [av]; _ -> [])) toJavaType :: Sing (a :: JType) -> Java.Type toJavaType ty = case Java.parser Java.ttype (pretty ty) of@@ -261,29 +265,34 @@ , classBytecode :: BS.ByteString } +instance TH.Lift DotClass where+ lift DotClass{..} =+ [| DotClass+ (JNI.fromChars $(TH.lift (JNI.toChars className)))+ (BS.pack $(TH.lift (BS.unpack classBytecode)))+ |]+ embedAsBytecode :: String -> String -> Java.CompilationUnit -> Q () embedAsBytecode pkg name unit = do- bcode <- TH.runIO $ do+ dcs <- TH.runIO $ do withSystemTempDirectory "inlinejava" $ \dir -> do let src = dir </> name <.> "java" emit src unit callProcess "javac" [src]- BS.readFile (dir </> name <.> "class")- f <- TH.newName "inlinejava__bytecode"- TH.addTopDecls =<<- sequence- [ TH.sigD f [t| StaticPtr DotClass |]- , TH.valD- (TH.varP f)- (TH.normalB- [| static- (DotClass (fromString $(TH.lift (pkg ++ "/" ++ name)))- (BS.pack $(TH.lift (BS.unpack bcode)))) |])- []- ]--newtype ClassLoader = ClassLoader (J ('Class "java.lang.ClassLoader"))-instance Coercible ClassLoader ('Class "java.lang.ClassLoader")+ -- A single compilation unit can produce multiple class files.+ classFiles <- filter (".class" `isSuffixOf`) <$> listDirectory dir+ forM classFiles $ \classFile -> do+ bcode <- BS.readFile (dir </> classFile)+ -- Strip the .class suffix.+ let klass = pkg ++ "/" ++ takeWhile (/= '.') classFile+ return $ DotClass (JNI.fromChars klass) bcode+ forM_ (zip dcs [(0 :: Int)..]) $ \(dc, i) -> do+ ptr <- TH.newName $ "inlinejava__bytecode" ++ show i+ TH.addTopDecls =<<+ sequence+ [ TH.sigD ptr [t| StaticPtr DotClass |]+ , TH.valD (TH.varP ptr) (TH.normalB [| static $(TH.lift dc) |]) []+ ] -- | Idempotent action that loads all wrappers in every module of the current -- program into the JVM.@@ -293,8 +302,8 @@ {-# NOINLINE doit #-} doit = unsafePerformIO $ do keys <- staticPtrKeys- loader :: ClassLoader <-- callStatic (classOf (undefined :: ClassLoader)) "getSystemClassLoader" []+ loader :: J ('Class "java.lang.ClassLoader") <-+ callStatic (sing :: Sing "java.lang.ClassLoader") "getSystemClassLoader" [] forM_ keys $ \key -> do sptr :: StaticPtr Any <- fromJust <$> unsafeLookupStaticPtr key let !x = deRefStaticPtr sptr@@ -382,29 +391,8 @@ -- always return java.lang.Object. This works, because in Java >= 5 if -- what you have is a primitive type but what you're requesting is an -- object type, then the value of primitive type gets autoboxed. So now we- -- have to guess on the Haskell side what autoboxing did. We assume- -- autoboxing is equivalent to reflecting a value at primitive type.- --- -- We have to write part of this programmatically due to a TH limitation,- -- https://ghc.haskell.org/trac/ghc/ticket/12164. It stands for:- --- -- @- -- [| -- Determine what Java type we'd get if we reflected the result.- -- -- That's the type we need to reify from.- -- mfix $ \x -> case Some (reflect x) of- -- Some (_ :: IO (J ty)) -> do- -- y <- $funcall- -- reify (unsafeCast y :: J ty)- -- |]- -- @- castReturnType funcall = do- ty <- TH.newName "ty"- [| mfix $ \x ->- $(TH.caseE- [| Some (reflect x) |]- [TH.match- (TH.conP 'Some [TH.sigP TH.wildP [t| IO (J $(TH.varT ty)) |]])- (TH.normalB [| do- y <- $funcall- reify (unsafeCast y :: J ty) |])- []]) |]+ -- have to guess on the Haskell side what autoboxing did, to reverse its+ -- effect. Alternatively, we can say that for now we only support+ -- returning boxed values. Once this limitation of the compiler gets+ -- lifted, we'll support returning unboxed values, just like `call` does.+ castReturnType funcall = [| unsafeUncoerce . coerce <$> $funcall |]
+ src/Language/Java/Inline/Cabal.hs view
@@ -0,0 +1,78 @@+-- | This module contains Cabal @Setup.hs@ hooks to set the @CLASSPATH@ to use+-- when compiling inline code. The @CLASSPATH@ environment variable specifies+-- where to find JVM package dependencies, such as third party packages+-- downloaded from <http://search.maven.org/ Maven Central>.+--+-- You can set the @CLASSPATH@ manually, or extract one from an external build+-- system configuration. Currently supported build systems:+--+-- * <https://gradle.org/ Gradle>++module Language.Java.Inline.Cabal+ ( gradleHooks+ , setGradleClasspath+ , gradleBuild+ ) where++import Distribution.Simple+import Distribution.Simple.Setup (BuildFlags)+import Distribution.Simple.LocalBuildInfo (LocalBuildInfo)+import Distribution.PackageDescription (HookedBuildInfo, PackageDescription)+import System.Directory (doesFileExist, getCurrentDirectory)+import System.Environment (lookupEnv, setEnv)+import System.FilePath+import System.IO (hClose)+import System.IO.Temp (withSystemTempFile)+import System.Process (callProcess, readProcess)++-- | Adds the 'setGradleClasspath' and 'gradleBuild' hooks.+gradleHooks :: UserHooks -> UserHooks+gradleHooks hooks = hooks+ { preBuild = setGradleClasspath+ , buildHook = buildHook hooks >> gradleBuild+ }++gradleBuildFile :: FilePath+gradleBuildFile = "build.gradle"++findGradleBuild :: FilePath -> IO (Maybe FilePath)+findGradleBuild cwd = do+ let path = cwd </> gradleBuildFile+ yes <- doesFileExist path+ if yes then return (Just path) else return Nothing++-- | Set the @CLASSPATH@ from a Gradle build configuration. Uses the classpath+-- for the @main@ source set.+getGradleClasspath :: FilePath -> IO String+getGradleClasspath parentBuildfile = do+ withSystemTempFile "build.gradle" $ \buildfile h -> do+ hClose h+ writeFile buildfile $+ unlines+ [ "apply from: '" ++ parentBuildfile ++ "'"+ , "task classpath { doLast { println sourceSets.main.compileClasspath.getAsPath() } }"+ ]+ readProcess "gradle" ["-q", "-b", buildfile, "classpath"] ""++-- | Set the @CLASSPATH@ from a Gradle build configuration. Does not override+-- the @CLASSPATH@ if one exists.+setGradleClasspath :: Args -> BuildFlags -> IO HookedBuildInfo+setGradleClasspath _ _ = do+ here <- getCurrentDirectory+ origclasspath <- lookupEnv "CLASSPATH"+ case origclasspath of+ Nothing -> do+ mbbuildfile <- findGradleBuild here+ case mbbuildfile of+ Nothing -> fail $ unwords [gradleBuildFile, "file not found in", here]+ Just buildfile -> do+ classpath <- getGradleClasspath buildfile+ setEnv "CLASSPATH" classpath+ Just _ -> return ()+ return (Nothing, [])++-- | Call @gradle build@ as part of the Cabal build. Useful to e.g. build+-- auxiliary Java source code and to create packages.+gradleBuild :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()+gradleBuild _ _ _ _ = do+ callProcess "gradle" ["build"]
tests/Language/Java/InlineSpec.hs view
@@ -5,6 +5,7 @@ module Language.Java.InlineSpec where import Data.Int+import Foreign.JNI.Types (JObject) import Language.Java.Inline import Test.Hspec @@ -33,3 +34,10 @@ it "Supports antiquotation variables in blocks" $ do let z = 1 :: Int32 [java| { return $z + 1; } |] `shouldReturn` (2 :: Int32)++ it "Supports anonymous classes" $ do+ _ :: JObject <- [java| new Object() {} |]+ return ()++ it "Supports multiple anonymous classes" $ do+ [java| new Object() {}.equals(new Object() {}) |] `shouldReturn` False