diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright EURL Tweag (c) 2016
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Author name here nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# jvm-streaming
+
+Expose Java iterators as streams from the streaming package.
diff --git a/jvm-streaming.cabal b/jvm-streaming.cabal
new file mode 100644
--- /dev/null
+++ b/jvm-streaming.cabal
@@ -0,0 +1,51 @@
+name:                jvm-streaming
+version:             0.1
+synopsis:            Expose Java iterators as streams from the streaming package.
+description:         Please see README.md.
+homepage:            http://github.com/tweag/inline-java/tree/master/jvm-streaming#readme
+license:             BSD3
+license-file:        LICENSE
+author:              Tweag I/O
+maintainer:          m@tweag.io
+copyright:           2015-2016 EURL Tweag.
+category:            FFI, JVM, Java
+build-type:          Simple
+cabal-version:       >=1.10
+extra-source-files:  README.md
+
+source-repository head
+  type: git
+  location: https://github.com/tweag/inline-java
+  subdir: jvm-streaming
+
+library
+  hs-source-dirs: src
+  default-language: Haskell2010
+  exposed-modules:
+    Language.Java.Streaming
+  build-depends:
+    base >= 4.7 && < 5,
+    distributed-closure >= 0.3,
+    jni >= 0.2.1,
+    jvm >= 0.1,
+    inline-java >= 0.6,
+    singletons >= 2.2,
+    streaming >= 0.1.4
+
+test-suite spec
+  type:
+    exitcode-stdio-1.0
+  hs-source-dirs: tests
+  main-is: Main.hs
+  other-modules:
+    Language.Java.StreamingSpec
+    Spec
+  build-depends:
+    base,
+    hspec,
+    inline-java,
+    jvm,
+    jvm-streaming,
+    streaming
+  default-language: Haskell2010
+  extra-libraries: pthread
diff --git a/src/Language/Java/Streaming.hs b/src/Language/Java/Streaming.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Java/Streaming.hs
@@ -0,0 +1,148 @@
+-- | Expose Java iterators as streams from the
+-- <http://hackage.haskell.org/package/streaming streaming> package.
+
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}
+
+module Language.Java.Streaming () where
+
+import Control.Distributed.Closure.TH
+import Data.IORef (IORef, newIORef, readIORef, writeIORef)
+import Data.Int (Int64)
+import Data.Singletons (SomeSing(..))
+import Data.Word (Word8)
+import Foreign.Ptr (FunPtr, freeHaskellFunPtr, intPtrToPtr, ptrToIntPtr)
+import qualified Foreign.JNI as JNI
+import Foreign.JNI.Types (jnull)
+import GHC.Stable
+  ( castPtrToStablePtr
+  , castStablePtrToPtr
+  , deRefStablePtr
+  , freeStablePtr
+  , newStablePtr
+  )
+import Language.Java
+import Language.Java.Inline
+import Streaming (Stream, Of)
+import qualified Streaming.Prelude as Streaming
+
+isPoppableStream :: IORef (Stream (Of a) IO ()) -> IO Word8
+isPoppableStream ref = do
+    stream <- readIORef ref
+    Streaming.uncons stream >>= \case
+      Nothing -> return $ fromIntegral $ fromEnum False
+      Just _ -> return $ fromIntegral $ fromEnum True
+
+popStream :: Reflect a ty => IORef (Stream (Of a) IO ()) -> IO (J ty)
+popStream ref = do
+    stream <- readIORef ref
+    Streaming.uncons stream >>= \case
+      Nothing -> do
+        exc :: J ('Class "java.util.NoSuchElementException") <- new []
+        JNI.throw exc
+        return jnull
+      Just (x, stream') -> do
+        writeIORef ref stream'
+        reflect x
+
+type JNIFun a = JNIEnv -> JObject -> IO a
+
+foreign import ccall "wrapper" wrapObjectFun
+  :: JNIFun (J ty) -> IO (FunPtr (JNIFun (J ty)))
+foreign import ccall "wrapper" wrapBooleanFun
+  :: JNIFun Word8 -> IO (FunPtr (JNIFun Word8))
+
+-- Export only to get a FunPtr.
+foreign export ccall "jvm_streaming_freeIterator" freeIterator
+  :: JNIEnv -> JObject -> Int64 -> IO ()
+foreign import ccall "&jvm_streaming_freeIterator" freeIteratorPtr
+  :: FunPtr (JNIEnv -> JObject -> Int64 -> IO ())
+
+data FunPtrTable = forall ty. FunPtrTable
+  { hasNextPtr :: FunPtr (JNIFun Word8)
+  , nextPtr :: FunPtr (JNIFun (J ty))
+  }
+
+freeIterator :: JNIEnv -> JObject -> Int64 -> IO ()
+freeIterator _ _ ptr = do
+    let sptr = castPtrToStablePtr $ intPtrToPtr $ fromIntegral ptr
+    FunPtrTable{..} <- deRefStablePtr sptr
+    freeHaskellFunPtr hasNextPtr
+    freeHaskellFunPtr nextPtr
+    freeStablePtr sptr
+
+newIterator
+  :: Reflect a ty
+  => Stream (Of a) IO ()
+  -> IO (J ('Iface "java.util.Iterator"))
+newIterator stream = do
+    ref <- newIORef stream
+    hasNextPtr <- wrapBooleanFun $ \_ _ -> isPoppableStream ref
+    nextPtr <- wrapObjectFun $ \_ _ -> popStream ref
+    -- Keep FunPtr's in a table that can be referenced from the Java side, so
+    -- that they can be freed.
+    tblPtr :: Int64 <- fromIntegral . ptrToIntPtr . castStablePtrToPtr <$> newStablePtr FunPtrTable{..}
+    iterator <-
+      [java| new java.util.Iterator() {
+                @Override
+                public native boolean hasNext();
+
+                @Override
+                public native Object next();
+
+                @Override
+                public void remove() {
+                    throw new UnsupportedOperationException();
+                }
+
+                private native void hsFinalize(long tblPtr);
+
+                @Override
+                public void finalize() {
+                    hsFinalize($tblPtr);
+                }
+             } |]
+    klass <- JNI.getObjectClass iterator
+    JNI.registerNatives klass
+      [ JNI.JNINativeMethod
+          "hasNext"
+          (methodSignature [] (sing :: Sing ('Prim "boolean")))
+          hasNextPtr
+      , JNI.JNINativeMethod
+          "next"
+          (methodSignature [] (sing :: Sing ('Class "java.lang.Object")))
+          nextPtr
+      , JNI.JNINativeMethod
+          "hsFinalize"
+          (methodSignature [SomeSing (sing :: Sing ('Prim "long"))] (sing :: Sing 'Void))
+          freeIteratorPtr
+      ]
+    return iterator
+
+withStatic [d|
+  type instance Interp (Stream (Of a) m r) = 'Iface "java.util.Iterator"
+
+  instance Reify a ty => Reify (Stream (Of a) IO ()) ('Iface "java.util.Iterator") where
+    reify it = return $ Streaming.untilRight $ do
+        call it "hasNext" [] >>= \case
+          False -> return (Right ())
+          True -> do
+            obj <- call it "next" []
+            Left <$> reify (unsafeCast (obj :: JObject))
+
+  instance Reflect a ty => Reflect (Stream (Of a) IO ()) ('Iface "java.util.Iterator") where
+    reflect x = newIterator x
+  |]
diff --git a/tests/Language/Java/StreamingSpec.hs b/tests/Language/Java/StreamingSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Language/Java/StreamingSpec.hs
@@ -0,0 +1,36 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE QuasiQuotes #-}
+
+module Language.Java.StreamingSpec where
+
+import Data.Int
+import Language.Java
+import Language.Java.Inline
+import Language.Java.Streaming ()
+import Test.Hspec
+import Streaming (Stream, Of)
+import qualified Streaming.Prelude as Streaming
+
+spec :: Spec
+spec = do
+    describe "iteration" $ do
+      it "succeeds on empty lists" $ do
+        vals <- reflect [1..0 :: Int32]
+        iterator <- [java| java.util.Arrays.asList($vals).iterator() |]
+        stream <- reify iterator
+        Streaming.toList_ stream `shouldReturn` [1..0 :: Int32]
+      it "succeeds on singleton lists" $ do
+        vals <- reflect [1 :: Int32]
+        iterator <- [java| java.util.Arrays.asList($vals).iterator() |]
+        stream <- reify iterator
+        Streaming.toList_ stream `shouldReturn` [1 :: Int32]
+      it "succeeds on non-trivial lists" $ do
+        vals <- reflect [1..10000 :: Int32]
+        iterator <- [java| java.util.Arrays.asList($vals).iterator() |]
+        stream <- reify iterator
+        Streaming.toList_ stream `shouldReturn` [1..10000 :: Int32]
+    describe "streams" $ do
+      it "have the property that reify . reflect == id" $ do
+        iterator <- reflect (Streaming.each [1..10000] :: Stream (Of Int32) IO ())
+        stream <- reify iterator
+        Streaming.toList_ stream `shouldReturn` [1..10000 :: Int32]
diff --git a/tests/Main.hs b/tests/Main.hs
new file mode 100644
--- /dev/null
+++ b/tests/Main.hs
@@ -0,0 +1,8 @@
+module Main where
+
+import Language.Java (withJVM)
+import qualified Spec
+import Test.Hspec
+
+main :: IO ()
+main = withJVM [] $ hspec Spec.spec
diff --git a/tests/Spec.hs b/tests/Spec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}
