diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2016-2017 LeapYear Technologies, Inc.
+
+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,38 @@
+# jvm-batching
+
+Provides batched marshalling of values between Java and Haskell.
+
+## Using it as a dependency
+
+Add `jvm-batching` to the list of dependencies in your .cabal file.
+Then edit the `Setup.hs` file to add the `jvm-batching.jar` to the
+classpath.
+
+```Haskell
+import Distribution.Simple
+import Language.Java.Inline.Cabal
+import qualified Language.Java.Batching.Jars
+
+main = do
+    jars <- Language.Java.Batching.Jars.getJars
+    defaultMainWithHooks (addJarsToClasspath jars simpleUserHooks)
+```
+
+Add a `custom-setup` stanza to your .cabal file.
+
+```
+custom-setup
+  setup-depends:
+    base,
+    Cabal,
+    inline-java,
+    jvm-batching
+```
+
+## Layout of source directories
+
+This is a multi-language package. We use
+Maven's [standard directory layout][maven-sdl] to organize source code
+in multiple languages side-by-side.
+
+[maven-sdl]: https://maven.apache.org/guides/introduction/introduction-to-the-standard-directory-layout.html
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,4 @@
+import Distribution.Simple
+import Language.Java.Inline.Cabal
+
+main = defaultMainWithHooks (gradleHooks simpleUserHooks)
diff --git a/benchmarks/Main.hs b/benchmarks/Main.hs
new file mode 100644
--- /dev/null
+++ b/benchmarks/Main.hs
@@ -0,0 +1,57 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Main where
+
+import Criterion.Main as Criterion
+import Control.DeepSeq
+import Data.Int
+import qualified Data.Vector as V
+import Data.List.Split (chunksOf)
+import qualified Data.Vector.Storable as VS
+import Language.Java
+import Language.Java.Batching ()
+
+roundtrip :: forall a. (Reify a, Reflect a) => a -> IO ()
+roundtrip a = pushWithSizeHint 30 $ (reflect a >>= reify :: IO a) >> pop
+
+intValues :: [Int32]
+intValues = [1..1000]
+
+vValues :: [VS.Vector Int32]
+vValues = [ VS.fromList [i, i] | i <- [1..500] ]
+
+benchBatching :: Benchmark
+benchBatching = deepseq intValues $
+    bgroup "roundtrips"
+    [ bgroup "ints"
+      [ bgroup "no batching"
+        [ bench "list" $ nfIO $ roundtrip intValues
+        , bench "storable array" $ nfIO $ roundtrip $ VS.fromList intValues
+        ]
+      , bgroup "batching"
+        [ bench "bare" $ nfIO $ roundtrip $ V.fromList intValues
+        , bench "unboxed vectors" $ nfIO $ roundtrip $
+            V.fromList $ map VS.fromList $
+              chunksOf (length intValues `div` 7) intValues
+        , bench "boxed vectors" $ nfIO $ roundtrip $
+            V.fromList $ map V.fromList $
+              chunksOf (length intValues `div` 7) intValues
+        ]
+      ]
+    , bgroup "int vectors"
+      [ bgroup "no batching"
+        [ bench "list" $ nfIO $ roundtrip vValues
+        ]
+      , bgroup "batching"
+        [ bench "bare" $ nfIO $ roundtrip $ V.fromList vValues
+        , bench "vectors of vectors" $ nfIO $ roundtrip $
+            V.fromList $ map V.fromList $
+              chunksOf (length vValues `div` 7) vValues
+        ]
+      ]
+    ]
+
+main :: IO ()
+main = withJVM ["-Djava.class.path=build/libs/jvm-batching.jar"] $
+    Criterion.defaultMain [benchBatching]
diff --git a/build.gradle b/build.gradle
new file mode 100644
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,5 @@
+apply plugin: 'java'
+
+dependencies {
+    compile files(buildscript.sourceFile.getParent() + "/build/libs/jvm-batching.jar")
+}
diff --git a/jvm-batching.cabal b/jvm-batching.cabal
new file mode 100644
--- /dev/null
+++ b/jvm-batching.cabal
@@ -0,0 +1,85 @@
+name:                jvm-batching
+version:             0.1
+synopsis:            Provides batched marshalling of values between Java and Haskell.
+description:         Please see README.md.
+homepage:            http://github.com/tweag/inline-java/tree/master/jvm-batching#readme
+license:             BSD3
+license-file:        LICENSE
+author:              Tweag I/O
+maintainer:          facundo.dominguez@tweag.io
+copyright:           2017 LeapYear Technologies
+category:            FFI, JVM, Java
+build-type:          Custom
+cabal-version:       >=1.10
+extra-source-files:
+  src/main/java/io/tweag/jvm/batching/Tuple2.java
+  src/main/java/io/tweag/jvm/batching/BatchWriter.java
+  src/main/java/io/tweag/jvm/batching/BatchWriters.java
+  src/main/java/io/tweag/jvm/batching/BatchReader.java
+  src/main/java/io/tweag/jvm/batching/BatchReaders.java
+  README.md
+  build.gradle
+
+source-repository head
+  type: git
+  location: https://github.com/tweag/inline-java
+  subdir: jvm-batching
+
+custom-setup
+  setup-depends:
+    base,
+    Cabal >= 1.24,
+    inline-java >= 0.7
+
+library
+  hs-source-dirs: src/main/haskell src/cabal/haskell
+  default-language: Haskell2010
+  exposed-modules:
+    Language.Java.Batching
+    Language.Java.Batching.Jars
+  other-modules:
+    Paths_jvm_batching
+  build-depends:
+    base >= 4.7 && < 5,
+    bytestring,
+    distributed-closure >= 0.3.5,
+    jni >= 0.6.0 && <0.7,
+    jvm >= 0.4.0 && <0.5,
+    inline-java >= 0.7 && <0.9,
+    singletons >= 2.2,
+    text,
+    vector
+
+test-suite spec
+  type:
+    exitcode-stdio-1.0
+  hs-source-dirs: src/test/haskell
+  main-is: Main.hs
+  other-modules:
+    Language.Java.BatchingSpec
+    Spec
+  build-depends:
+    base,
+    bytestring,
+    hspec,
+    inline-java,
+    jvm,
+    jvm-batching,
+    text,
+    vector
+  default-language: Haskell2010
+
+benchmark micro-benchmarks
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs: benchmarks
+  build-depends:
+    base >=4.8 && <5,
+    criterion,
+    deepseq,
+    jvm,
+    jvm-batching,
+    split,
+    vector
+  default-language: Haskell2010
+  ghc-options: -threaded
diff --git a/src/cabal/haskell/Language/Java/Batching/Jars.hs b/src/cabal/haskell/Language/Java/Batching/Jars.hs
new file mode 100644
--- /dev/null
+++ b/src/cabal/haskell/Language/Java/Batching/Jars.hs
@@ -0,0 +1,7 @@
+-- | Provides the paths to the jars needed to build with jvm-batching.
+module Language.Java.Batching.Jars where
+
+import Paths_jvm_batching (getDataFileName)
+
+getJars :: IO [String]
+getJars = (:[]) <$> getDataFileName "build/libs/jvm-batching.jar"
diff --git a/src/main/haskell/Language/Java/Batching.hs b/src/main/haskell/Language/Java/Batching.hs
new file mode 100644
--- /dev/null
+++ b/src/main/haskell/Language/Java/Batching.hs
@@ -0,0 +1,628 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PolyKinds #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE StaticPointers #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# OPTIONS_GHC -fplugin=Language.Java.Inline.Plugin #-}
+
+-- | This module provides composable batched marshalling.
+--
+-- === Batching
+--
+-- Calls to Java methods via JNI are slow in general. Marshalling an array of
+-- primitive values can be as slow as marshalling a single value.
+--
+-- Because of this, reifying an iterator or a container is best done by
+-- accumulates multiple elements on the java side before passing them to the
+-- Haskell side. And conversely, when reflecting an iterator or container,
+-- multiple Haskell values are put together before marshalling to the Java
+-- side.
+--
+-- Some Haskell values can be batched trivially into arrays of primitive values.
+-- 'Int32' can be batched in a java @int[]@, 'Double' can be batched in a java
+-- @double[]@, etc. However, other types like @Tuple2 Int32 Double@ would
+-- require more primitive arrays. Values of type @Tuple2 Int32 Double@ are
+-- batched in a pair of java arrays of type @int[]@ and @double[]@.
+--
+-- > data Tuple2 a b = Tuple2 a b
+--
+-- More generally, the design aims to provide composable batchers. If one knows
+-- how to batch types @a@ and @b@, one can also batch @Tuple2 a b@, @[a]@,
+-- @Vector a@, etc.
+--
+-- A reference to a batch of values in Java has the type @J (Batch a)@, where
+-- @a@ is the Haskell type of the elements in the batch. e.g.
+--
+-- > type instance Batch Int32 = 'Array ('Prim "int")
+-- > type instance Batch Double = 'Array ('Prim "double")
+-- > type instance Batch (Tuple2 a b) =
+-- >                 'Class "scala.Tuple2" <> '[Batch a, Batch b]
+--
+-- When defining batching for a new type, one needs to tell how batches are
+-- represented in Java by adding a type instance to the type family @Batch@.
+-- In addition, procedures for adding and extracting values from the batch
+-- need to be specified on both the Haskell and the Java side.
+--
+-- On the Java side, batches are built using the interface
+-- @io.tweag.jvm.batching.BatchWriter@. On the Haskell side, these
+-- batches are read using @reifyBatch@.
+--
+-- > class ( ... ) => BatchReify a where
+-- >   newBatchWriter
+-- >     :: proxy a
+-- >     -> IO (J ('Iface "io.tweag.jvm.batching.BatchWriter"
+-- >                  <> [Interp a, Batch a]
+-- >              )
+-- >           )
+-- >   reifyBatch :: J (Batch a) -> Int32 -> IO (V.Vector a)
+--
+-- @newReifyBatched@ produces a java object implementing the @BatchWriter@
+-- interface, and @reifyBatch@ allows to read a batch created in this fashion.
+--
+-- Conversely, batches can be read on the Java side using the interface
+-- @io.tweag.jvm.batching.BatchReader@. And on the Haskell side, these
+-- batches can be created with @reflectBatch@.
+--
+-- > class ( ... ) => BatchReflect a where
+-- >  newBatchReader
+-- >    :: proxy a
+-- >    -> IO (J ('Iface "io.tweag.jvm.batching.BatchReader"
+-- >                 <> [Batch a, Interp a]
+-- >              )
+-- >          )
+-- >  reflectBatch :: V.Vector a -> IO (J (Batch a))
+--
+-- @newBatchReader@ produces a java object implementing the @BatchReader@
+-- interface, and @reflectBatch@ allows to create these batches from vectors of
+-- Haskell values.
+--
+-- The methods of @BatchReify@ and @BatchReflect@ offer default
+-- implementations which marshal elements in the batch one at a time. Taking
+-- advantage of batching requires defining the methods explicitly. The default
+-- implementations are useful for cases where speed is not important, for
+-- instance when the iterators to reflect or reify contain a single element or
+-- just a very few.
+--
+-- 'Vector's and 'ByteString's are batched with the follow scheme.
+--
+-- > type instance Batch BS.ByteString
+-- >   = 'Class "io.tweag.jvm.batching.Tuple2" <>
+-- >        '[ 'Array ('Prim "byte")
+-- >         , 'Array ('Prim "int")
+-- >         ]
+--
+-- We use two arrays. One of the arrays contains the result of appending all of
+-- the 'ByteString's in the batch. The other array contains the offset of each
+-- vector in the resulting array. See 'ArrayBatch'.
+--
+module Language.Java.Batching
+  ( Batchable(..)
+  , BatchReify(..)
+  , BatchReflect(..)
+    -- * Array batching
+  , ArrayBatch
+  ) where
+
+import Control.Distributed.Closure.TH
+import Control.Exception (bracket)
+import Control.Monad (forM_, foldM)
+import qualified Data.ByteString             as BS
+import qualified Data.ByteString.Unsafe      as BS
+import Data.Int
+import Data.Singletons (SingI, Proxy(..))
+import qualified Data.Text as Text
+import qualified Data.Text.Foreign as Text
+import qualified Data.Vector as V
+import qualified Data.Vector.Mutable as VM
+import qualified Data.Vector.Storable as VS
+import Data.Word
+import Foreign.C.Types (CChar)
+import Foreign.ForeignPtr (newForeignPtr_, withForeignPtr)
+import Foreign.JNI
+import Foreign.Ptr
+import Foreign.Storable
+import Language.Java
+import Language.Java.Inline
+
+imports "io.tweag.jvm.batching.*"
+
+-- | A class of types whose values can be marshaled in batches.
+class (Interpretation a, SingI (Batch a)) => Batchable (a :: k) where
+  -- | The type of java batches for reifying and reflecting values of type @a@.
+  type family Batch a :: JType
+
+-- | A class for batching reification of values.
+--
+-- It has a method to create a batcher that creates batches in Java, and
+-- another method that refies a batch into a vector of haskell values.
+--
+-- The type of the batch used to appear as a class parameter but we run into
+-- https://ghc.haskell.org/trac/ghc/ticket/13582
+--
+class Batchable a => BatchReify a where
+  -- | Produces a batcher that aggregates elements of type @ty@ (such as @int@)
+  -- and produces collections of type @Batch a@ (such as @int[]@).
+  newBatchWriter
+    :: proxy a
+    -> IO (J ('Iface "io.tweag.jvm.batching.BatchWriter"
+                 <> [Interp a, Batch a]
+             )
+          )
+
+  -- The default implementation makes calls to the JVM for each element in the
+  -- batch.
+  default newBatchWriter
+    :: (Batch a ~ 'Array (Interp a))
+    => proxy a
+    -> IO (J ('Iface "io.tweag.jvm.batching.BatchWriter"
+                 <> [Interp a, Batch a]
+             )
+          )
+  newBatchWriter _ = generic <$> [java| new BatchWriters.ObjectBatchWriter() |]
+
+  -- | Reifies the values in a batch of type @Batch a@.
+  -- Gets the batch and the amount of elements it contains.
+  reifyBatch :: J (Batch a) -> Int32 -> IO (V.Vector a)
+
+  -- The default implementation makes calls to the JVM for each element in the
+  -- batch.
+  default reifyBatch
+    :: (Reify a, Batch a ~ 'Array (Interp a))
+    => J (Batch a) -> Int32 -> IO (V.Vector a)
+  reifyBatch jxs size =
+      V.generateM (fromIntegral size) $ \i ->
+      getObjectArrayElement jxs (fromIntegral i) >>= reify . unsafeCast
+
+-- | Helper for reifying batches of primitive types
+reifyPrimitiveBatch
+  :: Storable a
+  => (J ('Array ty) -> IO (Ptr a))
+  -> (J ('Array ty) -> Ptr a -> IO ())
+  -> J ('Array ty) -> Int32 -> IO (V.Vector a)
+reifyPrimitiveBatch getArrayElements releaseArrayElements jxs size = do
+    bracket (getArrayElements jxs) (releaseArrayElements jxs)
+      $ V.generateM (fromIntegral size) . peekElemOff
+
+-- | Batches of arrays of variable length
+--
+-- The first component is an array or batch containing the actual elements,
+-- and the second component is an @int[]@ where the ith position has the
+-- offset of the first position after the ith array.
+type ArrayBatch ty =
+    'Class "io.tweag.jvm.batching.Tuple2" <>
+       '[ ty
+        , 'Array ('Prim "int")
+        ]
+
+-- | Helper for reifying batches of vectors
+--
+-- Arrays are batched with two arrays. One of the arrays contains the result
+-- of appending all of the vectors in the batch. The other array contains the
+-- offset of each vector in the resulting array.
+--
+reifyArrayBatch
+  :: forall a b ty.
+     (Int32 -> J ty -> IO a) -- ^ reify the array/batch of values
+                             -- (takes the amount of elements in the array)
+  -> (Int -> Int -> a -> IO b) -- ^ slice at a given offset of given length of some value
+  -> J (ArrayBatch ty)
+  -> Int32
+  -> IO (V.Vector b)
+reifyArrayBatch reifyB slice batch0 batchSize = do
+    result <- VM.new (fromIntegral batchSize)
+    let batch = unsafeUngeneric batch0
+    vecEnds <- [java| (int[])$batch._2 |] >>= reify
+    let count = if VS.null vecEnds then 0 else VS.last vecEnds
+    vecValues <- [java| $batch._1 |] >>= reifyB count . fromObject
+    _ <- foldM (writeVector result vecEnds vecValues) 0 [0 .. batchSize - 1]
+    V.unsafeFreeze result
+  where
+    fromObject :: JObject -> J x
+    fromObject = unsafeCast
+
+    writeVector :: VM.IOVector b -> VS.Vector Int32 -> a -> Int32 -> Int32 -> IO Int32
+    writeVector mv ends values offset i32 = do
+        let i = fromIntegral i32
+        slice (fromIntegral offset)
+              (fromIntegral $ ends VS.! i - offset) values
+          >>= VM.unsafeWrite mv i
+        return $ ends VS.! i
+
+-- | A class for batching reflection of values.
+--
+-- It has a method to create a batch reader that reads batches in Java, and
+-- another method that reflects a vector of haskell values into a batch.
+--
+-- We considered having the type of the batch appear as a class parameter but
+-- we run into https://ghc.haskell.org/trac/ghc/ticket/13582
+--
+class Batchable a => BatchReflect a where
+  -- | Produces a batch reader that receives collections of type @ty1@
+  -- (such as @int[]@) and produces values of type @ty2@ (such as @int@).
+  newBatchReader
+    :: proxy a
+    -> IO (J ('Iface "io.tweag.jvm.batching.BatchReader"
+                 <> [Batch a, Interp a]
+             )
+          )
+
+  -- The default implementation makes calls to the JVM for each element in the
+  -- batch.
+  default newBatchReader
+    :: (Batch a ~ 'Array (Interp a))
+    => proxy a
+    -> IO (J ('Iface "io.tweag.jvm.batching.BatchReader"
+                       <> [Batch a, Interp a]
+             )
+          )
+  newBatchReader _ =
+      generic <$> [java| new BatchReaders.ObjectBatchReader() |]
+
+  -- | Reflects the values in a vector to a batch of type @ty@.
+  reflectBatch :: V.Vector a -> IO (J (Batch a))
+  -- The default implementation makes calls to the JVM for each element in the
+  -- batch.
+  default reflectBatch
+    :: (Reflect a, Batch a ~ 'Array (Interp a))
+    => V.Vector a -> IO (J (Batch a))
+  reflectBatch v = do
+      jxs <- newArray $ fromIntegral (V.length v)
+      forM_ [0 .. V.length v - 1] $ \i ->
+        withLocalRef (reflect (v V.! i))
+                     (setObjectArrayElement jxs (fromIntegral i))
+      return jxs
+
+-- | Helper for reflecting batches of primitive types
+reflectPrimitiveBatch
+  :: forall a ty. (Storable a, IsPrimitiveType ty)
+  => (J ('Array ty) -> Int32 -> Int32 -> Ptr a -> IO ())
+  -> V.Vector a -> IO (J ('Array ty))
+reflectPrimitiveBatch setArrayRegion v = do
+    let (fptr, offset, len) = VS.unsafeToForeignPtr (V.convert v)
+    withForeignPtr fptr $ \ptr -> do
+      jxs <- newArray (fromIntegral len)
+      let aOffset = offset * sizeOf (undefined :: a)
+      setArrayRegion jxs 0 (fromIntegral len) (plusPtr ptr aOffset)
+      return jxs
+
+-- | Helper for reflecting batches of vectors
+--
+-- The vector type is a, and vectors are manipulated exclusively with the
+-- polymorphic functions given as arguments.
+--
+-- Vectors are batched with two arrays. One of the arrays contains the result
+-- of appending all of the vectors in the batch. The other array contains the
+-- offset of each vector in the resulting array.
+--
+reflectArrayBatch
+  :: forall a b ty.
+     (b -> IO (J ty))
+  -> (a -> Int) -- ^ get length
+  -> ([a] -> IO b) -- ^ concat
+  -> V.Vector a
+  -> IO (J (ArrayBatch ty))
+reflectArrayBatch reflectB getLength concatenate vecs = do
+    let ends = V.convert $ V.postscanl' (+) 0 $
+                V.map (fromIntegral . getLength) vecs
+                  :: VS.Vector Int32
+    bigvec <- concatenate $ V.toList vecs
+    jvec <- reflectB bigvec
+    jends <- reflect ends
+    generic <$> Language.Java.new [ coerce (upcast jvec)
+                                  , coerce (upcast jends)
+                                  ]
+
+withStatic [d|
+  instance Batchable Bool where
+    type Batch Bool = 'Array ('Prim "boolean")
+
+  instance BatchReify Bool where
+    newBatchWriter _ = [java| new BatchWriters.BooleanBatchWriter() |]
+    reifyBatch jxs size = do
+        let toBool w = if w == 0 then False else True
+        bracket (getBooleanArrayElements jxs)
+                (releaseBooleanArrayElements jxs) $
+          \arr -> V.generateM (fromIntegral size)
+                              ((toBool <$>) . peekElemOff arr)
+
+  instance Batchable CChar where
+    type Batch CChar = 'Array ('Prim "byte")
+
+  instance BatchReify CChar where
+    newBatchWriter _ = [java| new BatchWriters.ByteBatchWriter() |]
+    reifyBatch =
+      reifyPrimitiveBatch getByteArrayElements releaseByteArrayElements
+
+  instance Batchable Word16 where
+    type Batch Word16 = 'Array ('Prim "char")
+
+  instance BatchReify Word16 where
+    newBatchWriter _ = [java| new BatchWriters.CharacterBatchWriter() |]
+    reifyBatch =
+      reifyPrimitiveBatch getCharArrayElements releaseCharArrayElements
+
+  instance Batchable Int16 where
+    type Batch Int16 = 'Array ('Prim "short")
+
+  instance BatchReify Int16 where
+    newBatchWriter _ = [java| new BatchWriters.ShortBatchWriter() |]
+    reifyBatch =
+      reifyPrimitiveBatch getShortArrayElements releaseShortArrayElements
+
+  instance Batchable Int32 where
+    type Batch Int32 = 'Array ('Prim "int")
+
+  instance BatchReify Int32 where
+    newBatchWriter _ = [java| new BatchWriters.IntegerBatchWriter() |]
+    reifyBatch =
+      reifyPrimitiveBatch getIntArrayElements releaseIntArrayElements
+
+  instance Batchable Int64 where
+    type Batch Int64 = 'Array ('Prim "long")
+
+  instance BatchReify Int64 where
+    newBatchWriter _ = [java| new BatchWriters.LongBatchWriter() |]
+    reifyBatch =
+      reifyPrimitiveBatch getLongArrayElements releaseLongArrayElements
+
+  instance Batchable Float where
+    type Batch Float = 'Array ('Prim "float")
+
+  instance BatchReify Float where
+    newBatchWriter _ = [java| new BatchWriters.FloatBatchWriter() |]
+    reifyBatch =
+      reifyPrimitiveBatch getFloatArrayElements releaseFloatArrayElements
+
+  instance Batchable Double where
+    type Batch Double = 'Array ('Prim "double")
+
+  instance BatchReify Double where
+    newBatchWriter _ = [java| new BatchWriters.DoubleBatchWriter() |]
+    reifyBatch =
+      reifyPrimitiveBatch getDoubleArrayElements releaseDoubleArrayElements
+
+  instance BatchReflect Bool where
+    newBatchReader _ = [java| new BatchReaders.BooleanBatchReader() |]
+    reflectBatch = reflectPrimitiveBatch setBooleanArrayRegion
+                 . V.map (\w -> if w then 1 else 0)
+
+  instance BatchReflect CChar where
+    newBatchReader _ = [java| new BatchReaders.ByteBatchReader() |]
+    reflectBatch = reflectPrimitiveBatch setByteArrayRegion
+
+  instance BatchReflect Word16 where
+    newBatchReader _ = [java| new BatchReaders.CharacterBatchReader() |]
+    reflectBatch = reflectPrimitiveBatch setCharArrayRegion
+
+  instance BatchReflect Int16 where
+    newBatchReader _ = [java| new BatchReaders.ShortBatchReader() |]
+    reflectBatch = reflectPrimitiveBatch setShortArrayRegion
+
+  instance BatchReflect Int32 where
+    newBatchReader _ = [java| new BatchReaders.IntegerBatchReader() |]
+    reflectBatch = reflectPrimitiveBatch setIntArrayRegion
+
+  instance BatchReflect Int64 where
+    newBatchReader _ = [java| new BatchReaders.LongBatchReader() |]
+    reflectBatch = reflectPrimitiveBatch setLongArrayRegion
+
+  instance BatchReflect Float where
+    newBatchReader _ = [java| new BatchReaders.FloatBatchReader() |]
+    reflectBatch = reflectPrimitiveBatch setFloatArrayRegion
+
+  instance BatchReflect Double where
+    newBatchReader _ = [java| new BatchReaders.DoubleBatchReader() |]
+    reflectBatch = reflectPrimitiveBatch setDoubleArrayRegion
+
+#if ! (__GLASGOW_HASKELL__ == 800 && __GLASGOW_HASKELL_PATCHLEVEL1__ == 1)
+  instance Batchable BS.ByteString where
+    type Batch BS.ByteString
+      = 'Class "io.tweag.jvm.batching.Tuple2" <>
+         '[ 'Array ('Prim "byte")
+          , 'Array ('Prim "int")
+          ]
+
+  instance Batchable (VS.Vector Word16) where
+    type Batch (VS.Vector Word16)
+      = 'Class "io.tweag.jvm.batching.Tuple2" <>
+         '[ 'Array ('Prim "char")
+          , 'Array ('Prim "int")
+          ]
+
+  instance Batchable (VS.Vector Int16) where
+    type Batch (VS.Vector Int16)
+      = 'Class "io.tweag.jvm.batching.Tuple2" <>
+         '[ 'Array ('Prim "short")
+          , 'Array ('Prim "int")
+          ]
+
+  instance Batchable (VS.Vector Int32) where
+    type Batch (VS.Vector Int32)
+      = 'Class "io.tweag.jvm.batching.Tuple2" <>
+         '[ 'Array ('Prim "int")
+          , 'Array ('Prim "int")
+          ]
+
+  instance Batchable (VS.Vector Int64) where
+    type Batch (VS.Vector Int64)
+      = 'Class "io.tweag.jvm.batching.Tuple2" <>
+         '[ 'Array ('Prim "long")
+          , 'Array ('Prim "int")
+          ]
+
+  instance Batchable (VS.Vector Float) where
+    type Batch (VS.Vector Float)
+      = 'Class "io.tweag.jvm.batching.Tuple2" <>
+         '[ 'Array ('Prim "float")
+          , 'Array ('Prim "int")
+          ]
+
+  instance Batchable (VS.Vector Double) where
+    type Batch (VS.Vector Double)
+      = 'Class "io.tweag.jvm.batching.Tuple2" <>
+         '[ 'Array ('Prim "double")
+          , 'Array ('Prim "int")
+          ]
+
+  instance Batchable Text.Text where
+    type Batch Text.Text
+      = 'Class "io.tweag.jvm.batching.Tuple2" <>
+         '[ 'Array ('Prim "char")
+          , 'Array ('Prim "int")
+          ]
+
+  instance BatchReify BS.ByteString where
+    newBatchWriter _ = [java| new BatchWriters.ByteArrayBatchWriter() |]
+    reifyBatch = reifyArrayBatch (const reify) bsUnsafeSlice
+      where
+        bsUnsafeSlice :: Int -> Int -> BS.ByteString -> IO BS.ByteString
+        bsUnsafeSlice offset sz = return . BS.unsafeTake sz . BS.unsafeDrop offset
+
+  instance BatchReify (VS.Vector Word16) where
+    newBatchWriter _ = [java| new BatchWriters.CharArrayBatchWriter() |]
+    reifyBatch =
+        reifyArrayBatch (const reify) (fmap (fmap return) . VS.unsafeSlice)
+
+  instance BatchReify (VS.Vector Int16) where
+    newBatchWriter _ = [java| new BatchWriters.ShortArrayBatchWriter() |]
+    reifyBatch =
+        reifyArrayBatch (const reify) (fmap (fmap return) . VS.unsafeSlice)
+
+  instance BatchReify (VS.Vector Int32) where
+    newBatchWriter _ = [java| new BatchWriters.IntArrayBatchWriter() |]
+    reifyBatch =
+        reifyArrayBatch (const reify) (fmap (fmap return) . VS.unsafeSlice)
+
+  instance BatchReify (VS.Vector Int64) where
+    newBatchWriter _ = [java| new BatchWriters.LongArrayBatchWriter() |]
+    reifyBatch =
+        reifyArrayBatch (const reify) (fmap (fmap return) . VS.unsafeSlice)
+
+  instance BatchReify (VS.Vector Float) where
+    newBatchWriter _ = [java| new BatchWriters.FloatArrayBatchWriter() |]
+    reifyBatch =
+        reifyArrayBatch (const reify) (fmap (fmap return) . VS.unsafeSlice)
+
+  instance BatchReify (VS.Vector Double) where
+    newBatchWriter _ = [java| new BatchWriters.DoubleArrayBatchWriter() |]
+    reifyBatch =
+        reifyArrayBatch (const reify) (fmap (fmap return) . VS.unsafeSlice)
+
+  instance BatchReify Text.Text where
+    newBatchWriter _ = [java| new BatchWriters.StringArrayBatchWriter() |]
+    reifyBatch = reifyArrayBatch (const reify) $ \o n vs ->
+                  (VS.unsafeWith (VS.unsafeSlice o n vs) $ \ptr ->
+                      Text.fromPtr ptr (fromIntegral n)
+                  )
+
+  instance BatchReflect BS.ByteString where
+    newBatchReader _ = [java| new BatchReaders.ByteArrayBatchReader() |]
+    reflectBatch = reflectArrayBatch reflect BS.length (return . BS.concat)
+
+  instance BatchReflect (VS.Vector Word16) where
+    newBatchReader _ = [java| new BatchReaders.CharArrayBatchReader() |]
+    reflectBatch = reflectArrayBatch reflect VS.length (return . VS.concat)
+
+  instance BatchReflect (VS.Vector Int16) where
+    newBatchReader _ = [java| new BatchReaders.ShortArrayBatchReader() |]
+    reflectBatch = reflectArrayBatch reflect VS.length (return . VS.concat)
+
+  instance BatchReflect (VS.Vector Int32) where
+    newBatchReader _ = [java| new BatchReaders.IntArrayBatchReader() |]
+    reflectBatch = reflectArrayBatch reflect VS.length (return . VS.concat)
+
+  instance BatchReflect (VS.Vector Int64) where
+    newBatchReader _ = [java| new BatchReaders.LongArrayBatchReader() |]
+    reflectBatch = reflectArrayBatch reflect VS.length (return . VS.concat)
+
+  instance BatchReflect (VS.Vector Float) where
+    newBatchReader _ = [java| new BatchReaders.FloatArrayBatchReader() |]
+    reflectBatch = reflectArrayBatch reflect VS.length (return . VS.concat)
+
+  instance BatchReflect (VS.Vector Double) where
+    newBatchReader _ = [java| new BatchReaders.DoubleArrayBatchReader() |]
+    reflectBatch = reflectArrayBatch reflect VS.length (return . VS.concat)
+
+  instance BatchReflect Text.Text where
+    newBatchReader _ = [java| new BatchReaders.StringArrayBatchReader() |]
+    reflectBatch = reflectArrayBatch reflect Text.length $ \ts ->
+                     Text.useAsPtr (Text.concat ts) $ \ptr len ->
+                       (`VS.unsafeFromForeignPtr0` fromIntegral len)
+                         <$> newForeignPtr_ ptr
+#endif
+
+  instance Interpretation a => Interpretation (V.Vector a) where
+    type Interp (V.Vector a) = 'Array (Interp a)
+
+  -- TODO: Fix GHC so it doesn't complain that variables used exclusively in
+  -- quasiquotes are unused. Thus we can stop prepending variable names with
+  -- '_'.
+
+  instance (Interpretation a, BatchReify a)
+           => Reify (V.Vector a) where
+    reify jv = do
+        _batcher <- unsafeUngeneric <$> newBatchWriter (Proxy :: Proxy a)
+        n <- getArrayLength jv
+        let _jvo = arrayUpcast jv
+        batch <- [java| {
+          $_batcher.start($n);
+          for(int i=0;i<$_jvo.length;i++)
+             $_batcher.set(i, $_jvo[i]);
+          return $_batcher.getBatch();
+          } |]
+        reifyBatch (fromObject batch) n
+      where
+        fromObject :: JObject -> J x
+        fromObject = unsafeCast
+
+  instance (Interpretation a, BatchReflect a)
+           => Reflect (V.Vector a) where
+    reflect v = do
+        _batch <- upcast <$> reflectBatch v
+        _batchReader <-
+          unsafeUngeneric <$> newBatchReader (Proxy :: Proxy a)
+        jv <- [java| {
+          $_batchReader.setBatch($_batch);
+          return $_batchReader.getSize();
+          } |] >>= newArray :: IO (J ('Array (Interp a)))
+        let _jvo = arrayUpcast jv
+        () <- [java| {
+          for(int i=0;i<$_jvo.length;i++)
+            $_jvo[i] = $_batchReader.get(i);
+          } |]
+        return jv
+
+  instance Batchable a => Batchable (V.Vector a) where
+    type Batch (V.Vector a) = ArrayBatch (Batch a)
+
+  instance (SingI (Interp a), SingI (Batch a), BatchReify a)
+           => BatchReify (V.Vector a) where
+    newBatchWriter _ = do
+        _b <- unsafeUngeneric <$> newBatchWriter (Proxy :: Proxy a)
+        generic <$> [java| new BatchWriters.ObjectArrayBatchWriter($_b) |]
+    reifyBatch =
+        reifyArrayBatch (flip reifyBatch) (fmap (fmap return) . V.unsafeSlice)
+
+  instance (SingI (Interp a), SingI (Batch a), BatchReflect a)
+           => BatchReflect (V.Vector a) where
+    newBatchReader _ = do
+        _b <- unsafeUngeneric <$> newBatchReader (Proxy :: Proxy a)
+        generic <$> [java| new BatchReaders.ObjectArrayBatchReader($_b) |]
+    reflectBatch =
+        reflectArrayBatch reflectBatch V.length (return . V.concat)
+ |]
diff --git a/src/main/java/io/tweag/jvm/batching/BatchReader.java b/src/main/java/io/tweag/jvm/batching/BatchReader.java
new file mode 100644
--- /dev/null
+++ b/src/main/java/io/tweag/jvm/batching/BatchReader.java
@@ -0,0 +1,16 @@
+package io.tweag.jvm.batching;
+
+/**
+ *  A BatchReader<A, B> provides access to the elements of type B in a
+ *  batch of type A.
+ */
+public interface BatchReader<A, B> {
+    /// Yields the batch size.
+    public int getSize();
+    /// Yields the object at position i of the batch.
+    public B get(int i);
+    /// Sets the batch read by get.
+    public void setBatch(A o);
+    /// Yields the class of the batched objects.
+    public Class<B> getElemClass();
+}
diff --git a/src/main/java/io/tweag/jvm/batching/BatchReaders.java b/src/main/java/io/tweag/jvm/batching/BatchReaders.java
new file mode 100644
--- /dev/null
+++ b/src/main/java/io/tweag/jvm/batching/BatchReaders.java
@@ -0,0 +1,304 @@
+package io.tweag.jvm.batching;
+
+/**
+ *  Various BatchReaders
+ * */
+public class BatchReaders {
+
+    public static final class BooleanBatchReader implements BatchReader<boolean[], Boolean> {
+        private boolean[] arr = new boolean[0];
+        public int getSize() { return arr.length; };
+        public Boolean get(int i) { return arr[i]; };
+        public void setBatch(boolean[] b) { arr = b; };
+        public Class<Boolean> getElemClass() { return Boolean.class; };
+    }
+
+    public static final class ByteBatchReader implements BatchReader<byte[], Byte> {
+        private byte[] arr = new byte[0];
+        public int getSize() { return arr.length; };
+        public Byte get(int i) { return arr[i]; };
+        public void setBatch(byte[] b) { arr = b; };
+        public Class<Byte> getElemClass() { return Byte.class; };
+    }
+
+    public static final class CharacterBatchReader implements BatchReader<char[], Character> {
+        private char[] arr = new char[0];
+        public int getSize() { return arr.length; };
+        public Character get(int i) { return arr[i]; };
+        public void setBatch(char[] b) { arr = b; };
+        public Class<Character> getElemClass() { return Character.class; };
+    }
+
+    public static final class ShortBatchReader implements BatchReader<short[], Short> {
+        private short[] arr = new short[0];
+        public int getSize() { return arr.length; };
+        public Short get(int i) { return arr[i]; };
+        public void setBatch(short[] b) { arr = b; };
+        public Class<Short> getElemClass() { return Short.class; };
+    }
+
+    public static final class IntegerBatchReader implements BatchReader<int[], Integer> {
+        private int[] arr = new int[0];
+        public int getSize() { return arr.length; };
+        public Integer get(int i) { return arr[i]; };
+        public void setBatch(int[] b) { arr = b; };
+        public Class<Integer> getElemClass() { return Integer.class; };
+    }
+
+    public static final class LongBatchReader implements BatchReader<long[], Long> {
+        private long[] arr = new long[0];
+        public int getSize() { return arr.length; };
+        public Long get(int i) { return arr[i]; };
+        public void setBatch(long[] b) { arr = b; };
+        public Class<Long> getElemClass() { return Long.class; };
+    }
+
+    public static final class FloatBatchReader implements BatchReader<float[], Float> {
+        private float[] arr = new float[0];
+        public int getSize() { return arr.length; };
+        public Float get(int i) { return arr[i]; };
+        public void setBatch(float[] b) { arr = b; };
+        public Class<Float> getElemClass() { return Float.class; };
+    }
+
+    public static final class DoubleBatchReader implements BatchReader<double[], Double> {
+        private double[] arr = new double[0];
+        public int getSize() { return arr.length; };
+        public Double get(int i) { return arr[i]; };
+        public void setBatch(double[] b) { arr = b; };
+        public Class<Double> getElemClass() { return Double.class; };
+    }
+
+    public static final class ObjectBatchReader implements BatchReader<Object[], Object> {
+        private Object[] arr = new Object[0];
+        public int getSize() { return arr.length; };
+        public Object get(int i) { return arr[i]; };
+        public void setBatch(Object[] b) { arr = b; };
+        public Class<Object> getElemClass() { return Object.class; };
+    }
+
+    public static final class ByteArrayBatchReader
+            implements BatchReader<Tuple2<byte[], int[]>, byte[]> {
+        private byte[] arr;
+        // end[i] tells the first position after the i-th array.
+        private int[] end = new int[0];
+        private int start;
+        public void setBatch(Tuple2<byte[], int[]> t) {
+            arr = t._1;
+            end = t._2;
+            start = 0;
+        }
+        public int getSize() { return end.length; }
+        public byte[] get(int i) {
+            final int len = end[i] - start;
+            byte[] res = new byte[len];
+            System.arraycopy(arr, start, res, 0, len);
+            start = end[i];
+            return res;
+        }
+        public Class<byte[]> getElemClass() { return byte[].class; };
+    }
+
+    public static final class BooleanArrayBatchReader
+            implements BatchReader<Tuple2<boolean[], int[]>, boolean[]> {
+        private boolean[] arr;
+        // end[i] tells the first position after the i-th array.
+        private int[] end = new int[0];
+        private int start;
+        public void setBatch(Tuple2<boolean[], int[]> t) {
+            arr = t._1;
+            end = t._2;
+            start = 0;
+        }
+        public int getSize() { return end.length; }
+        public boolean[] get(int i) {
+            final int len = end[i] - start;
+            boolean[] res = new boolean[len];
+            System.arraycopy(arr, start, res, 0, len);
+            start = end[i];
+            return res;
+        }
+        public Class<boolean[]> getElemClass() { return boolean[].class; };
+    }
+
+    public static final class CharArrayBatchReader
+            implements BatchReader<Tuple2<char[], int[]>, char[]> {
+        private char[] arr;
+        // end[i] tells the first position after the i-th array.
+        private int[] end = new int[0];
+        private int start;
+        public void setBatch(Tuple2<char[], int[]> t) {
+            arr = t._1;
+            end = t._2;
+            start = 0;
+        }
+        public int getSize() { return end.length; }
+        public char[] get(int i) {
+            final int len = end[i] - start;
+            char[] res = new char[len];
+            System.arraycopy(arr, start, res, 0, len);
+            start = end[i];
+            return res;
+        }
+        public Class<char[]> getElemClass() { return char[].class; };
+    }
+
+    public static final class ShortArrayBatchReader
+            implements BatchReader<Tuple2<short[], int[]>, short[]> {
+        private short[] arr;
+        // end[i] tells the first position after the i-th array.
+        private int[] end = new int[0];
+        private int start;
+        public void setBatch(Tuple2<short[], int[]> t) {
+            arr = t._1;
+            end = t._2;
+            start = 0;
+        }
+        public int getSize() { return end.length; }
+        public short[] get(int i) {
+            final int len = end[i] - start;
+            short[] res = new short[len];
+            System.arraycopy(arr, start, res, 0, len);
+            start = end[i];
+            return res;
+        }
+        public Class<short[]> getElemClass() { return short[].class; };
+    }
+
+    public static final class IntArrayBatchReader
+            implements BatchReader<Tuple2<int[], int[]>, int[]> {
+        private int[] arr;
+        // end[i] tells the first position after the i-th array.
+        private int[] end = new int[0];
+        private int start;
+        public void setBatch(Tuple2<int[], int[]> t) {
+            arr = t._1;
+            end = t._2;
+            start = 0;
+        }
+        public int getSize() { return end.length; }
+        public int[] get(int i) {
+            final int len = end[i] - start;
+            int[] res = new int[len];
+            System.arraycopy(arr, start, res, 0, len);
+            start = end[i];
+            return res;
+        }
+        public Class<int[]> getElemClass() { return int[].class; };
+    }
+
+    public static final class LongArrayBatchReader
+            implements BatchReader<Tuple2<long[], int[]>, long[]> {
+        private long[] arr;
+        // end[i] tells the first position after the i-th array.
+        private int[] end = new int[0];
+        private int start;
+        public void setBatch(Tuple2<long[], int[]> t) {
+            arr = t._1;
+            end = t._2;
+            start = 0;
+        }
+        public int getSize() { return end.length; }
+        public long[] get(int i) {
+            final int len = end[i] - start;
+            long[] res = new long[len];
+            System.arraycopy(arr, start, res, 0, len);
+            start = end[i];
+            return res;
+        }
+        public Class<long[]> getElemClass() { return long[].class; };
+    }
+
+    public static final class FloatArrayBatchReader
+            implements BatchReader<Tuple2<float[], int[]>, float[]> {
+        private float[] arr;
+        // end[i] tells the first position after the i-th array.
+        private int[] end = new int[0];
+        private int start;
+        public void setBatch(Tuple2<float[], int[]> t) {
+            arr = t._1;
+            end = t._2;
+            start = 0;
+        }
+        public int getSize() { return end.length; }
+        public float[] get(int i) {
+            final int len = end[i] - start;
+            float[] res = new float[len];
+            System.arraycopy(arr, start, res, 0, len);
+            start = end[i];
+            return res;
+        }
+        public Class<float[]> getElemClass() { return float[].class; };
+    }
+
+    public static final class DoubleArrayBatchReader
+            implements BatchReader<Tuple2<double[], int[]>, double[]> {
+        private double[] arr;
+        // end[i] tells the first position after the i-th array.
+        private int[] end = new int[0];
+        private int start;
+        public void setBatch(Tuple2<double[], int[]> t) {
+            arr = t._1;
+            end = t._2;
+            start = 0;
+        }
+        public int getSize() { return end.length; }
+        public double[] get(int i) {
+            final int len = end[i] - start;
+            double[] res = new double[len];
+            System.arraycopy(arr, start, res, 0, len);
+            start = end[i];
+            return res;
+        }
+        public Class<double[]> getElemClass() { return double[].class; };
+    }
+
+    public static final class ObjectArrayBatchReader<A, T>
+            implements BatchReader<Tuple2<A, int[]>, T[]> {
+        // end[i] tells the first position after the i-th array.
+        private int[] end = new int[0];
+        private int start;
+        private final BatchReader<A, T> br;
+        public ObjectArrayBatchReader(BatchReader<A, T> br) {
+            this.br = br;
+        }
+        public void setBatch(Tuple2<A, int[]> t) {
+            br.setBatch(t._1);
+            end = t._2;
+            start = 0;
+        }
+        public int getSize() { return end.length; }
+        public T[] get(int i) {
+            final int len = end[i] - start;
+            T[] res = (T[]) java.lang.reflect.Array.newInstance(br.getElemClass(), len);
+            for(int j=0;j<len;j++)
+                res[j] = br.get(start + j);
+            start = end[i];
+            return res;
+        }
+        public Class<T[]> getElemClass() {
+            return (Class<T[]>) java.lang.reflect.Array.newInstance(br.getElemClass(), 0).getClass();
+        }
+    }
+
+    public static final class StringArrayBatchReader
+            implements BatchReader<Tuple2<char[], int[]>, String> {
+        private char[] arr;
+        // end[i] tells the first position after the i-th array.
+        private int[] end = new int[0];
+        private int start;
+        public void setBatch(Tuple2<char[], int[]> t) {
+            arr = t._1;
+            end = t._2;
+            start = 0;
+        }
+        public int getSize() { return end.length; }
+        public String get(int i) {
+            final int len = end[i] - start;
+            String res = new String(arr, start, len);
+            start = end[i];
+            return res;
+        }
+        public Class<String> getElemClass() { return String.class; };
+    }
+}
diff --git a/src/main/java/io/tweag/jvm/batching/BatchWriter.java b/src/main/java/io/tweag/jvm/batching/BatchWriter.java
new file mode 100644
--- /dev/null
+++ b/src/main/java/io/tweag/jvm/batching/BatchWriter.java
@@ -0,0 +1,14 @@
+package io.tweag.jvm.batching;
+
+/**
+ *  A BatchWriter<A,B> is an object that takes elements of type A one at
+ *  a time and yields a batch of type B containing all of them.
+ */
+public interface BatchWriter<A, B> {
+    /// Starts a new batch of the given size
+    public void start(int batchSize);
+    /// Sets the object at position i of the batch.
+    public void set(int i, A o);
+    /// Yields the batch.
+    public B getBatch();
+}
diff --git a/src/main/java/io/tweag/jvm/batching/BatchWriters.java b/src/main/java/io/tweag/jvm/batching/BatchWriters.java
new file mode 100644
--- /dev/null
+++ b/src/main/java/io/tweag/jvm/batching/BatchWriters.java
@@ -0,0 +1,409 @@
+package io.tweag.jvm.batching;
+
+/**
+ *  Various batchers
+ *
+ * */
+public final class BatchWriters {
+
+    public static final class BooleanBatchWriter implements BatchWriter<Boolean, boolean[]> {
+        private boolean[] arr;
+        public void start(int batchSize) { arr = new boolean[batchSize]; };
+        public void set(int i, Boolean o) { arr[i] = o; };
+        public boolean[] getBatch() { return arr; };
+    }
+
+    public static final class CharacterBatchWriter implements BatchWriter<Character, char[]> {
+        private char[] arr;
+        public void start(int batchSize) { arr = new char[batchSize]; };
+        public void set(int i, Character o) { arr[i] = o; };
+        public char[] getBatch() { return arr; };
+    }
+
+    public static final class ByteBatchWriter implements BatchWriter<Byte, byte[]> {
+        private byte[] arr;
+        public void start(int batchSize) { arr = new byte[batchSize]; };
+        public void set(int i, Byte o) { arr[i] = o; };
+        public byte[] getBatch() { return arr; };
+    }
+
+    public static final class ShortBatchWriter implements BatchWriter<Short, short[]> {
+        private short[] arr;
+        public void start(int batchSize) { arr = new short[batchSize]; };
+        public void set(int i, Short o) { arr[i] = o; };
+        public short[] getBatch() { return arr; };
+    }
+
+    public static final class IntegerBatchWriter implements BatchWriter<Integer, int[]> {
+        private int[] arr;
+        public void start(int batchSize) { arr = new int[batchSize]; };
+        public void set(int i, Integer o) { arr[i] = o; };
+        public int[] getBatch() { return arr; };
+    }
+
+    public static final class LongBatchWriter implements BatchWriter<Long, long[]> {
+        private long[] arr;
+        public void start(int batchSize) { arr = new long[batchSize]; };
+        public void set(int i, Long o) { arr[i] = o; };
+        public long[] getBatch() { return arr; };
+    }
+
+    public static final class FloatBatchWriter implements BatchWriter<Float, float[]> {
+        private float[] arr;
+        public void start(int batchSize) { arr = new float[batchSize]; };
+        public void set(int i, Float o) { arr[i] = o; };
+        public float[] getBatch() { return arr; };
+    }
+
+    public static final class DoubleBatchWriter implements BatchWriter<Double, double[]> {
+        private double[] arr;
+        public void start(int batchSize) { arr = new double[batchSize]; };
+        public void set(int i, Double o) { arr[i] = o; };
+        public double[] getBatch() { return arr; };
+    }
+
+    public static final class ObjectBatchWriter implements BatchWriter<Object, Object[]> {
+        private Object[] arr;
+        public void start(int batchSize) { arr = new Object[batchSize]; };
+        public void set(int i, Object o) { arr[i] = o; };
+        public Object[] getBatch() { return arr; };
+    }
+
+    public static final class ByteArrayBatchWriter
+            implements BatchWriter<byte[], Tuple2<byte[], int[]> > {
+        private byte[][] arrays;
+        // end[i] tells the first position after the i-th array.
+        private int[] end;
+        // top tells the first position not occupied by arrays in the batch.
+        private int top;
+        public void start(int size) {
+            end = new int[size];
+            arrays = new byte[size][];
+            top = 0;
+        }
+        public void set(int i, byte[] vec) {
+            if (vec == null)
+                throw new RuntimeException("null vectors are unsupported when reifying");
+            arrays[i] = vec;
+            top += vec.length;
+            end[i] = top;
+        }
+        public Tuple2<byte[], int[]> getBatch() {
+            byte[] batch = new byte[top];
+            int pos = 0;
+            for(int i=0;i<end.length && arrays[i]!=null;i++) {
+                System.arraycopy(arrays[i], 0, batch, pos, arrays[i].length);
+                pos = end[i];
+                // Release the reference to the array so it can be reclaimed
+                // before the loop is over.
+                arrays[i] = null;
+            }
+            return new Tuple2<byte[], int[]>(batch, end);
+        }
+    }
+
+    public static final class BooleanArrayBatchWriter
+            implements BatchWriter<boolean[], Tuple2<boolean[], int[]> > {
+        private boolean[][] arrays;
+        // end[i] tells the first position after the i-th array.
+        private int[] end;
+        // top tells the first position not occupied by arrays in the batch.
+        private int top;
+        public void start(int size) {
+            end = new int[size];
+            arrays = new boolean[size][];
+            top = 0;
+        }
+        public void set(int i, boolean[] vec) {
+            if (vec == null)
+                throw new RuntimeException("null vectors are unsupported when reifying");
+            arrays[i] = vec;
+            top += vec.length;
+            end[i] = top;
+        }
+        public Tuple2<boolean[], int[]> getBatch() {
+            boolean[] batch = new boolean[top];
+            int pos = 0;
+            for(int i=0;i<end.length && arrays[i]!=null;i++) {
+                System.arraycopy(arrays[i], 0, batch, pos, arrays[i].length);
+                pos = end[i];
+                // Release the reference to the array so it can be reclaimed
+                // before the loop is over.
+                arrays[i] = null;
+            }
+            return new Tuple2<boolean[], int[]>(batch, end);
+        }
+    }
+
+    public static final class CharArrayBatchWriter
+            implements BatchWriter<char[], Tuple2<char[], int[]> > {
+        private char[][] arrays;
+        // end[i] tells the first position after the i-th array.
+        private int[] end;
+        // top tells the first position not occupied by arrays in the batch.
+        private int top;
+        public void start(int size) {
+            end = new int[size];
+            arrays = new char[size][];
+            top = 0;
+        }
+        public void set(int i, char[] vec) {
+            if (vec == null)
+                throw new RuntimeException("null vectors are unsupported when reifying");
+            arrays[i] = vec;
+            top += vec.length;
+            end[i] = top;
+        }
+        public Tuple2<char[], int[]> getBatch() {
+            char[] batch = new char[top];
+            int pos = 0;
+            for(int i=0;i<end.length && arrays[i]!=null;i++) {
+                System.arraycopy(arrays[i], 0, batch, pos, arrays[i].length);
+                pos = end[i];
+                // Release the reference to the array so it can be reclaimed
+                // before the loop is over.
+                arrays[i] = null;
+            }
+            return new Tuple2<char[], int[]>(batch, end);
+        }
+    }
+
+    public static final class ShortArrayBatchWriter
+            implements BatchWriter<short[], Tuple2<short[], int[]> > {
+        private short[][] arrays;
+        // end[i] tells the first position after the i-th array.
+        private int[] end;
+        // top tells the first position not occupied by arrays in the batch.
+        private int top;
+        public void start(int size) {
+            end = new int[size];
+            arrays = new short[size][];
+            top = 0;
+        }
+        public void set(int i, short[] vec) {
+            if (vec == null)
+                throw new RuntimeException("null vectors are unsupported when reifying");
+            arrays[i] = vec;
+            top += vec.length;
+            end[i] = top;
+        }
+        public Tuple2<short[], int[]> getBatch() {
+            short[] batch = new short[top];
+            int pos = 0;
+            for(int i=0;i<end.length && arrays[i]!=null;i++) {
+                System.arraycopy(arrays[i], 0, batch, pos, arrays[i].length);
+                pos = end[i];
+                // Release the reference to the array so it can be reclaimed
+                // before the loop is over.
+                arrays[i] = null;
+            }
+            return new Tuple2<short[], int[]>(batch, end);
+        }
+    }
+
+    public static final class IntArrayBatchWriter
+            implements BatchWriter<int[], Tuple2<int[], int[]> > {
+        private int[][] arrays;
+        // end[i] tells the first position after the i-th array.
+        private int[] end;
+        // top tells the first position not occupied by arrays in the batch.
+        private int top;
+        public void start(int size) {
+            end = new int[size];
+            arrays = new int[size][];
+            top = 0;
+        }
+        public void set(int i, int[] vec) {
+            if (vec == null)
+                throw new RuntimeException("null vectors are unsupported when reifying");
+            arrays[i] = vec;
+            top += vec.length;
+            end[i] = top;
+        }
+        public Tuple2<int[], int[]> getBatch() {
+            int[] batch = new int[top];
+            int pos = 0;
+            for(int i=0;i<end.length && arrays[i]!=null;i++) {
+                System.arraycopy(arrays[i], 0, batch, pos, arrays[i].length);
+                pos = end[i];
+                // Release the reference to the array so it can be reclaimed
+                // before the loop is over.
+                arrays[i] = null;
+            }
+            return new Tuple2<int[], int[]>(batch, end);
+        }
+    }
+
+    public static final class LongArrayBatchWriter
+            implements BatchWriter<long[], Tuple2<long[], int[]> > {
+        private long[][] arrays;
+        // end[i] tells the first position after the i-th array.
+        private int[] end;
+        // top tells the first position not occupied by arrays in the batch.
+        private int top;
+        public void start(int size) {
+            end = new int[size];
+            arrays = new long[size][];
+            top = 0;
+        }
+        public void set(int i, long[] vec) {
+            if (vec == null)
+                throw new RuntimeException("null vectors are unsupported when reifying");
+            arrays[i] = vec;
+            top += vec.length;
+            end[i] = top;
+        }
+        public Tuple2<long[], int[]> getBatch() {
+            long[] batch = new long[top];
+            int pos = 0;
+            for(int i=0;i<end.length && arrays[i]!=null;i++) {
+                System.arraycopy(arrays[i], 0, batch, pos, arrays[i].length);
+                pos = end[i];
+                // Release the reference to the array so it can be reclaimed
+                // before the loop is over.
+                arrays[i] = null;
+            }
+            return new Tuple2<long[], int[]>(batch, end);
+        }
+    }
+
+    public static final class FloatArrayBatchWriter
+            implements BatchWriter<float[], Tuple2<float[], int[]> > {
+        private float[][] arrays;
+        // end[i] tells the first position after the i-th array.
+        private int[] end;
+        // top tells the first position not occupied by arrays in the batch.
+        private int top;
+        public void start(int size) {
+            end = new int[size];
+            arrays = new float[size][];
+            top = 0;
+        }
+        public void set(int i, float[] vec) {
+            if (vec == null)
+                throw new RuntimeException("null vectors are unsupported when reifying");
+            arrays[i] = vec;
+            top += vec.length;
+            end[i] = top;
+        }
+        public Tuple2<float[], int[]> getBatch() {
+            float[] batch = new float[top];
+            int pos = 0;
+            for(int i=0;i<end.length && arrays[i]!=null;i++) {
+                System.arraycopy(arrays[i], 0, batch, pos, arrays[i].length);
+                pos = end[i];
+                // Release the reference to the array so it can be reclaimed
+                // before the loop is over.
+                arrays[i] = null;
+            }
+            return new Tuple2<float[], int[]>(batch, end);
+        }
+    }
+
+    public static final class DoubleArrayBatchWriter
+            implements BatchWriter<double[], Tuple2<double[], int[]> > {
+        private double[][] arrays;
+        // end[i] tells the first position after the i-th array.
+        private int[] end;
+        // top tells the first position not occupied by arrays in the batch.
+        private int top;
+        public void start(int size) {
+            end = new int[size];
+            arrays = new double[size][];
+            top = 0;
+        }
+        public void set(int i, double[] vec) {
+            if (vec == null)
+                throw new RuntimeException("null vectors are unsupported when reifying");
+            arrays[i] = vec;
+            top += vec.length;
+            end[i] = top;
+        }
+        public Tuple2<double[], int[]> getBatch() {
+            double[] batch = new double[top];
+            int pos = 0;
+            for(int i=0;i<end.length && arrays[i]!=null;i++) {
+                System.arraycopy(arrays[i], 0, batch, pos, arrays[i].length);
+                pos = end[i];
+                // Release the reference to the array so it can be reclaimed
+                // before the loop is over.
+                arrays[i] = null;
+            }
+            return new Tuple2<double[], int[]>(batch, end);
+        }
+    }
+
+    public static final class ObjectArrayBatchWriter<T, B>
+            implements BatchWriter<T[], Tuple2<B, int[]> > {
+        private final BatchWriter<T, B> ob;
+        private T[][] arrays;
+        // end[i] tells the first position after the i-th array.
+        private int[] end;
+        // top tells the first position not occupied by arrays in the batch.
+        private int top;
+
+        public ObjectArrayBatchWriter(BatchWriter<T, B> ob) {
+            this.ob = ob;
+        }
+
+        public void start(int size) {
+            end = new int[size];
+            arrays = (T[][]) new Object[size][];
+            top = 0;
+        }
+        public void set(int i, T[] vec) {
+            if (vec == null)
+                throw new RuntimeException("null vectors are unsupported when reifying");
+            arrays[i] = vec;
+            top += vec.length;
+            end[i] = top;
+        }
+        public Tuple2<B, int[]> getBatch() {
+            ob.start(top);
+            int k = 0;
+            for(int i=0;i<end.length && arrays[i]!=null;i++) {
+                for(int j=0;j<arrays[i].length;j++) {
+                    ob.set(k, arrays[i][j]);
+                    k++;
+                }
+                // Release the reference to the array so it can be reclaimed
+                // before the loop is over.
+                arrays[i] = null;
+            }
+            return new Tuple2<B, int[]>(ob.getBatch(), end);
+        }
+    }
+
+    public static final class StringArrayBatchWriter
+            implements BatchWriter<String, Tuple2<char[], int[]> > {
+        private String[] arrays;
+        // end[i] tells the first position after the i-th array.
+        private int[] end;
+        // top tells the first position not occupied by arrays in the batch.
+        private int top;
+        public void start(int size) {
+            end = new int[size];
+            arrays = new String[size];
+            top = 0;
+        }
+        public void set(int i, String s) {
+            if (s == null)
+                throw new RuntimeException("null vectors are unsupported when reifying");
+            arrays[i] = s;
+            top += s.length();
+            end[i] = top;
+        }
+        public Tuple2<char[], int[]> getBatch() {
+            char[] batch = new char[top];
+            int pos = 0;
+            for(int i=0;i<end.length && arrays[i]!=null;i++) {
+                arrays[i].getChars(0, arrays[i].length(), batch, pos);
+                pos = end[i];
+                // Release the reference to the array so it can be reclaimed
+                // before the loop is over.
+                arrays[i] = null;
+            }
+            return new Tuple2<char[], int[]>(batch, end);
+        }
+    }
+}
diff --git a/src/main/java/io/tweag/jvm/batching/Tuple2.java b/src/main/java/io/tweag/jvm/batching/Tuple2.java
new file mode 100644
--- /dev/null
+++ b/src/main/java/io/tweag/jvm/batching/Tuple2.java
@@ -0,0 +1,10 @@
+package io.tweag.jvm.batching;
+
+public class Tuple2<A, B> {
+    public A _1;
+    public B _2;
+    public Tuple2(A _1, B _2) {
+        this._1 = _1;
+        this._2 = _2;
+    }
+}
diff --git a/src/test/haskell/Language/Java/BatchingSpec.hs b/src/test/haskell/Language/Java/BatchingSpec.hs
new file mode 100644
--- /dev/null
+++ b/src/test/haskell/Language/Java/BatchingSpec.hs
@@ -0,0 +1,34 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# OPTIONS_GHC -fplugin=Language.Java.Inline.Plugin #-}
+
+module Language.Java.BatchingSpec where
+
+import qualified Data.ByteString as BS
+import Data.Int
+import Data.Text (Text)
+import qualified Data.Text as Text
+import qualified Data.Vector as V
+import qualified Data.Vector.Storable as VS
+import Language.Java
+import Language.Java.Batching ()
+import Test.Hspec
+
+spec :: Spec
+spec = do
+    let testrr :: (Eq a, Show a, Reify a, Reflect a) => a -> IO ()
+        testrr x = (reflect x >>= reify) `shouldReturn` x
+    describe "batching" $ do
+      it "succeeds on empty vectors" $ do
+        testrr (V.fromList [] :: V.Vector Text)
+      it "succeeds on non-empty vectors" $ do
+        testrr (V.fromList [1..10] :: V.Vector Int32)
+      it "succeeds on vectors of vectors" $ do
+        let xs :: V.Vector (V.Vector Int32)
+            xs = V.fromList (map V.fromList [ [i .. i + 4] | i <- [1,6..26] ])
+        testrr xs
+        testrr (V.empty `asTypeOf` xs)
+        testrr $ V.map (V.map (Text.pack . show)) xs
+        testrr $ V.map (V.map (BS.singleton . fromIntegral)) xs
+        testrr (V.map V.convert xs :: V.Vector (VS.Vector Int32))
diff --git a/src/test/haskell/Main.hs b/src/test/haskell/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/test/haskell/Main.hs
@@ -0,0 +1,10 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Language.Java (withJVM)
+import qualified Spec
+import Test.Hspec
+
+main :: IO ()
+main = withJVM ["-Djava.class.path=build/libs/jvm-batching.jar"] $
+    hspec Spec.spec
diff --git a/src/test/haskell/Spec.hs b/src/test/haskell/Spec.hs
new file mode 100644
--- /dev/null
+++ b/src/test/haskell/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}
