diff --git a/jni.cabal b/jni.cabal
--- a/jni.cabal
+++ b/jni.cabal
@@ -1,5 +1,5 @@
 name:                jni
-version:             0.3.1
+version:             0.4.0
 synopsis:            Complete JNI raw bindings.
 description:         Please see README.md.
 homepage:            https://github.com/tweag/inline-java/tree/master/jni#readme
@@ -12,8 +12,6 @@
 build-type:          Simple
 cabal-version:       >=1.10
 extra-source-files:  README.md
-extra-tmp-files:
-  src/Foreign/JNI.c
 
 source-repository head
   type: git
@@ -22,13 +20,15 @@
 
 library
   hs-source-dirs: src
-  c-sources: src/Foreign/JNI.c
+  if impl(ghc < 8.2.1)
+    c-sources: src/Foreign/JNI.c
   cc-options: -std=c11
   extra-libraries: jvm
   exposed-modules:
     Foreign.JNI
     Foreign.JNI.Types
     Foreign.JNI.String
+    Foreign.JNI.Internal
   other-modules:
     Foreign.JNI.NativeMethod
   build-depends:
@@ -36,8 +36,15 @@
     bytestring >=0.10,
     choice >= 0.2.0,
     containers >=0.5,
-    inline-c >=0.5.6.1,
+    constraints >=0.8,
+    deepseq >= 1.4.2,
     singletons >= 2.0,
     thread-local-storage >=0.1
+  if impl(ghc < 8.2.1)
+    build-depends:
+      inline-c >=0.5.6.1
+  else
+    build-depends:
+      inline-c >=0.6
   build-tools: cpphs
   default-language: Haskell2010
diff --git a/src/Foreign/JNI.c b/src/Foreign/JNI.c
--- a/src/Foreign/JNI.c
+++ b/src/Foreign/JNI.c
@@ -1,6 +1,8 @@
 
 #include <jni.h>
 
+#include <stdio.h>
+
 #include <errno.h>
 
 #include <stdlib.h>
diff --git a/src/Foreign/JNI.cpphs b/src/Foreign/JNI.cpphs
--- a/src/Foreign/JNI.cpphs
+++ b/src/Foreign/JNI.cpphs
@@ -36,12 +36,18 @@
 
 module Foreign.JNI
   ( -- * JNI functions
-    -- ** VM creation
+    -- ** VM management
     withJVM
+  , newJVM
+  , destroyJVM
     -- ** Class loading
   , defineClass
   , JNINativeMethod(..)
   , registerNatives
+    -- ** String wrappers
+  , ReferenceTypeName
+  , MethodSignature
+  , Signature
     -- ** Exceptions
   , JVMException(..)
   , throw
@@ -56,6 +62,8 @@
     -- ** Reference manipulation
   , newGlobalRef
   , deleteGlobalRef
+  , newGlobalRefNonFinalized
+  , deleteGlobalRefNonFinalized
   , newLocalRef
   , deleteLocalRef
   , pushLocalFrame
@@ -187,6 +195,7 @@
   , newForeignPtr_
   , withForeignPtr
   )
+import Foreign.JNI.Internal
 import Foreign.JNI.NativeMethod
 import Foreign.JNI.Types
 import qualified Foreign.JNI.String as JNI
@@ -195,12 +204,14 @@
 import GHC.ForeignPtr (newConcForeignPtr)
 import qualified Language.C.Inline as C
 import qualified Language.C.Inline.Unsafe as CU
+import System.IO (fixIO)
 import System.IO.Unsafe (unsafePerformIO)
 import Prelude hiding (String)
 
 C.context (C.baseCtx <> C.bsCtx <> jniCtx)
 
 C.include "<jni.h>"
+C.include "<stdio.h>"
 C.include "<errno.h>"
 C.include "<stdlib.h>"
 
@@ -269,16 +280,15 @@
             \st@(readers, aim) -> ((readers - 1, aim), st)
     case st of
       -- Notify the writer if I'm the last reader.
-      (0, Writing mv) -> putMVar mv ()
+      (1, Writing mv) -> putMVar mv ()
       _               -> return ()
 
 -- | Waits until the current read locks are released and grants a write lock.
 acquireWriteLock :: RWLock -> IO ()
 acquireWriteLock (RWLock ref) = do
     mv <- newEmptyMVar
-    join $ atomicModifyIORef ref $ \case
-      (0, _) -> ((0, Writing mv),   return ())
-      st     -> (               st, takeMVar mv)
+    join $ atomicModifyIORef ref $ \(readers, _) ->
+      ((readers, Writing mv), when (readers > 0) (takeMVar mv))
 
 -- | This lock is used to avoid the JVM from dying before any finalizers
 -- deleting global references are finished.
@@ -331,40 +341,46 @@
   foldr (\str k cstrs -> BS.useAsCString str $ \cstr -> k (cstr:cstrs)) m strs []
 
 -- | Create a new JVM, with the given arguments. /Can only be called once/. Best
--- practice: use it to wrap your @main@ function.
+-- practice: use 'withJVM' instead. Only useful for GHCi.
+newJVM :: [ByteString] -> IO JVM
+newJVM options = JVM_ <$> do
+    useAsCStrings options $ \cstrs -> do
+      withArray cstrs $ \(coptions :: Ptr (Ptr CChar)) -> do
+        let n = fromIntegral (length cstrs) :: C.CInt
+        [C.block| JavaVM * {
+          JavaVM *jvm;
+          JNIEnv *env;
+          JavaVMInitArgs vm_args;
+          JavaVMOption *options = malloc(sizeof(JavaVMOption) * $(int n));
+          for(int i = 0; i < $(int n); i++)
+                  options[i].optionString = $(char **coptions)[i];
+          vm_args.version = JNI_VERSION_1_6;
+          vm_args.nOptions = $(int n);
+          vm_args.options = options;
+          vm_args.ignoreUnrecognized = 0;
+          JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
+          free(options);
+          return jvm; } |]
+
+-- | Deallocate a 'JVM' created using 'newJVM'.
+destroyJVM :: JVM -> IO ()
+destroyJVM (JVM_ jvm) = do
+    acquireWriteLock globalJVMLock
+    [C.block| void { (*$(JavaVM *jvm))->DestroyJavaVM($(JavaVM *jvm)); } |]
+
+-- | Create a new JVM, with the given arguments. Destroy it once the given
+-- action completes. /Can only be called once/. Best practice: use it to wrap
+-- your @main@ function.
 withJVM :: [ByteString] -> IO a -> IO a
-withJVM options action =
-    bracket ini fini (const action)
-  where
-    ini = do
-      useAsCStrings options $ \cstrs -> do
-        withArray cstrs $ \(coptions :: Ptr (Ptr CChar)) -> do
-          let n = fromIntegral (length cstrs) :: C.CInt
-          [C.block| JavaVM * {
-            JavaVM *jvm;
-            JNIEnv *env;
-            JavaVMInitArgs vm_args;
-            JavaVMOption *options = malloc(sizeof(JavaVMOption) * $(int n));
-            for(int i = 0; i < $(int n); i++)
-                    options[i].optionString = $(char **coptions)[i];
-            vm_args.version = JNI_VERSION_1_6;
-            vm_args.nOptions = $(int n);
-            vm_args.options = options;
-            vm_args.ignoreUnrecognized = 0;
-            JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
-            free(options);
-            return jvm; } |]
-    fini jvm = do
-      acquireWriteLock globalJVMLock
-      [C.block| void { (*$(JavaVM *jvm))->DestroyJavaVM($(JavaVM *jvm)); } |]
+withJVM options action = bracket (newJVM options) destroyJVM (const action)
 
 defineClass
   :: Coercible o (J ('Class "java.lang.ClassLoader"))
-  => JNI.String -- ^ Class name
-  -> o          -- ^ Loader
+  => ReferenceTypeName -- ^ Class name
+  -> o -- ^ Loader
   -> ByteString -- ^ Bytecode buffer
   -> IO JClass
-defineClass name (coerce -> upcast -> loader) buf = withJNIEnv $ \env ->
+defineClass (coerce -> name) (coerce -> upcast -> loader) buf = withJNIEnv $ \env ->
     throwIfException env $
     JNI.withString name $ \namep ->
     objectFromPtr =<<
@@ -404,16 +420,16 @@
                                    $(char *msgp)) } |]
 
 findClass
-  :: JNI.String -- ^ Class name
+  :: ReferenceTypeName -- ^ Class name
   -> IO JClass
-findClass name = withJNIEnv $ \env ->
+findClass (coerce -> name) = withJNIEnv $ \env ->
     throwIfException env $
     JNI.withString name $ \namep ->
     objectFromPtr =<<
     [CU.exp| jclass { (*$(JNIEnv *env))->FindClass($(JNIEnv *env), $(char *namep)) } |]
 
-newObject :: JClass -> JNI.String -> [JValue] -> IO JObject
-newObject cls sig args = withJNIEnv $ \env ->
+newObject :: JClass -> MethodSignature -> [JValue] -> IO JObject
+newObject cls (coerce -> sig) args = withJNIEnv $ \env ->
     throwIfException env $
     withJValues args $ \cargs -> do
       constr <- getMethodID cls "<init>" sig
@@ -426,9 +442,9 @@
 getFieldID
   :: JClass -- ^ A class object as returned by 'findClass'
   -> JNI.String -- ^ Field name
-  -> JNI.String -- ^ JNI signature
+  -> Signature -- ^ JNI signature
   -> IO JFieldID
-getFieldID cls fieldname sig = withJNIEnv $ \env ->
+getFieldID cls fieldname (coerce -> sig) = withJNIEnv $ \env ->
     throwIfException env $
     JNI.withString fieldname $ \fieldnamep ->
     JNI.withString sig $ \sigp ->
@@ -441,9 +457,9 @@
 getStaticFieldID
   :: JClass -- ^ A class object as returned by 'findClass'
   -> JNI.String -- ^ Field name
-  -> JNI.String -- ^ JNI signature
+  -> Signature -- ^ JNI signature
   -> IO JFieldID
-getStaticFieldID cls fieldname sig = withJNIEnv $ \env ->
+getStaticFieldID cls fieldname (coerce -> sig) = withJNIEnv $ \env ->
     throwIfException env $
     JNI.withString fieldname $ \fieldnamep ->
     JNI.withString sig $ \sigp ->
@@ -548,9 +564,9 @@
 getMethodID
   :: JClass -- ^ A class object as returned by 'findClass'
   -> JNI.String -- ^ Field name
-  -> JNI.String -- ^ JNI signature
+  -> MethodSignature -- ^ JNI signature
   -> IO JMethodID
-getMethodID cls methodname sig = withJNIEnv $ \env ->
+getMethodID cls methodname (coerce -> sig) = withJNIEnv $ \env ->
     throwIfException env $
     JNI.withString methodname $ \methodnamep ->
     JNI.withString sig $ \sigp ->
@@ -563,9 +579,9 @@
 getStaticMethodID
   :: JClass -- ^ A class object as returned by 'findClass'
   -> JNI.String -- ^ Field name
-  -> JNI.String -- ^ JNI signature
+  -> MethodSignature -- ^ JNI signature
   -> IO JMethodID
-getStaticMethodID cls methodname sig = withJNIEnv $ \env ->
+getStaticMethodID cls methodname (coerce -> sig) = withJNIEnv $ \env ->
     throwIfException env $
     JNI.withString methodname $ \methodnamep ->
     JNI.withString sig $ \sigp ->
@@ -593,20 +609,35 @@
       [CU.exp| jobject {
         (*$(JNIEnv *env))->NewGlobalRef($(JNIEnv *env),
                                         $fptr-ptr:(jobject obj)) } |]
-    coerce <$> J <$> newConcForeignPtr gobj (finalize gobj)
-  where
-    finalize gobj = do
-      bracket (tryAcquireReadLock globalJVMLock)
-              (\doRead -> when (toBool doRead) $ releaseReadLock globalJVMLock)
-              $ \doRead ->
-        when (toBool doRead) $ withJNIEnv $ \env ->
-          [CU.block| void { (*$(JNIEnv *env))->DeleteGlobalRef($(JNIEnv *env)
-                                                              ,$(jobject gobj));
-                          } |]
+    fixIO $ \j ->
+      coerce <$> J <$> newConcForeignPtr gobj (deleteGlobalRefNonFinalized j)
 
 deleteGlobalRef :: Coercible o (J ty) => o -> IO ()
 deleteGlobalRef (coerce -> J p) = finalizeForeignPtr p
 
+-- | Like 'newGlobalRef' but it doesn't attach a finalizer to destroy
+-- the reference when it is not longer reachable. Use
+-- 'deleteGlobalRefNonFinalized' to destroy this reference.
+newGlobalRefNonFinalized :: Coercible o (J ty) => o -> IO o
+newGlobalRefNonFinalized (coerce -> upcast -> obj) = withJNIEnv $ \env -> do
+    gobj <-
+      [CU.exp| jobject {
+        (*$(JNIEnv *env))->NewGlobalRef($(JNIEnv *env),
+                                        $fptr-ptr:(jobject obj)) } |]
+    coerce <$> J <$> newForeignPtr_ gobj
+
+-- | Like 'deleteGlobalRef' but it can be used only on references created with
+-- 'newGlobalRefNonFinalized'.
+deleteGlobalRefNonFinalized :: Coercible o (J ty) => o -> IO ()
+deleteGlobalRefNonFinalized (coerce -> upcast -> obj) = do
+    bracket (tryAcquireReadLock globalJVMLock)
+            (\doRead -> when (toBool doRead) $ releaseReadLock globalJVMLock)
+            $ \doRead ->
+      when (toBool doRead) $ withJNIEnv $ \env -> do
+        [CU.block| void { (*$(JNIEnv *env))->DeleteGlobalRef($(JNIEnv *env)
+                                                            ,$fptr-ptr:(jobject obj));
+                        } |]
+
 -- NB: Cannot add a finalizer to local references because it may
 -- run in a thread where the reference is not valid.
 newLocalRef :: Coercible o (J ty) => o -> IO (J ty)
@@ -819,16 +850,32 @@
                                             $fptr-ptr:(jstring jstr),
                                             $(jchar *chars)) } |]
 
-getObjectArrayElement :: Coercible o (JArray a) => o -> Int32 -> IO (J a)
-getObjectArrayElement (coerce -> upcast -> array) i = withJNIEnv $ \env ->
-    unsafeCast <$> (objectFromPtr =<<)
+getObjectArrayElement
+  :: forall a o.
+     (IsReferenceType a, Coercible o (J a))
+  => JArray a
+  -> Int32
+  -> IO o
+getObjectArrayElement (arrayUpcast -> array) i = withJNIEnv $ \env ->
+    ( (coerce :: J a -> o)
+    . (unsafeCast :: JObject -> J a)
+    ) <$> (objectFromPtr =<<)
     [C.exp| jobject {
       (*$(JNIEnv *env))->GetObjectArrayElement($(JNIEnv *env),
                                                $fptr-ptr:(jarray array),
                                                $(jsize i)) } |]
 
-setObjectArrayElement :: Coercible o (J a) => JObjectArray -> Int32 -> o -> IO ()
-setObjectArrayElement array i (coerce -> upcast -> x) = withJNIEnv $ \env ->
+setObjectArrayElement
+  :: forall a o.
+     (IsReferenceType a, Coercible o (J a))
+  => JArray a
+  -> Int32
+  -> o
+  -> IO ()
+setObjectArrayElement (arrayUpcast -> array)
+                      i
+                      ((coerce :: o -> J a) -> upcast -> x) =
+    withJNIEnv $ \env ->
     [C.exp| void {
       (*$(JNIEnv *env))->SetObjectArrayElement($(JNIEnv *env),
                                                $fptr-ptr:(jobjectArray array),
diff --git a/src/Foreign/JNI/Internal.hs b/src/Foreign/JNI/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Foreign/JNI/Internal.hs
@@ -0,0 +1,25 @@
+module Foreign.JNI.Internal where
+
+import qualified Foreign.JNI.String as JNI
+
+-- | A reference type name is not just any 'JNI.String', but a fully qualified
+-- identifier well-formed by construction.
+newtype ReferenceTypeName = ReferenceTypeName JNI.String
+  deriving (Eq, Ord)
+
+instance Show ReferenceTypeName where
+  show (ReferenceTypeName str) = show str
+
+-- | A string representing a signature, well-formed by construction.
+newtype Signature = Signature JNI.String
+  deriving (Eq, Ord)
+
+instance Show Signature where
+  show (Signature str) = show str
+
+-- | A string representing a method signature, well-formed by construction.
+newtype MethodSignature = MethodSignature JNI.String
+  deriving (Eq, Ord)
+
+instance Show MethodSignature where
+  show (MethodSignature str) = show str
diff --git a/src/Foreign/JNI/NativeMethod.hsc b/src/Foreign/JNI/NativeMethod.hsc
--- a/src/Foreign/JNI/NativeMethod.hsc
+++ b/src/Foreign/JNI/NativeMethod.hsc
@@ -6,6 +6,8 @@
 module Foreign.JNI.NativeMethod where
 
 import qualified Data.ByteString.Unsafe as BS
+import Data.Coerce (coerce)
+import Foreign.JNI.Internal
 import qualified Foreign.JNI.String as JNI
 import Foreign.Ptr (FunPtr)
 import Foreign.Storable (Storable(..))
@@ -18,7 +20,7 @@
 
 data JNINativeMethod = forall a. JNINativeMethod
   { jniNativeMethodName :: JNI.String
-  , jniNativeMethodSignature :: JNI.String
+  , jniNativeMethodSignature :: MethodSignature
   , jniNativeMethodFunPtr :: FunPtr a
   }
 
@@ -32,9 +34,9 @@
       return $
         JNINativeMethod
           (JNI.unsafeFromByteString name)
-          (JNI.unsafeFromByteString sig)
+          (coerce (JNI.unsafeFromByteString sig))
           fptr
   poke ptr JNINativeMethod{..} = do
       JNI.withString jniNativeMethodName $ #{poke JNINativeMethod, name} ptr
-      JNI.withString jniNativeMethodSignature $ #{poke JNINativeMethod, signature} ptr
+      JNI.withString (coerce jniNativeMethodSignature) $ #{poke JNINativeMethod, signature} ptr
       #{poke JNINativeMethod, fnPtr} ptr jniNativeMethodFunPtr
diff --git a/src/Foreign/JNI/String.hs b/src/Foreign/JNI/String.hs
--- a/src/Foreign/JNI/String.hs
+++ b/src/Foreign/JNI/String.hs
@@ -23,6 +23,7 @@
   ) where
 
 import qualified Data.ByteString as BS
+import qualified Data.ByteString.Internal as BS
 import qualified Data.ByteString.Unsafe as BS
 import Data.ByteString (ByteString)
 import Data.String (IsString(..))
@@ -33,6 +34,7 @@
 import qualified Prelude
 import Prelude hiding (String)
 
+-- A JNI string is represented as a NUL terminated bytestring in UTF-8 encoding.
 newtype String = String ByteString
   deriving (Eq, Ord)
 
@@ -44,8 +46,11 @@
 
 fromChars :: Prelude.String -> String
 {-# INLINE [0] fromChars #-}
-fromChars str = unsafeDupablePerformIO $ do
-    String <$> (BS.unsafePackCString =<< GHC.newCString GHC.utf8 str)
+fromChars str = unsafeDupablePerformIO $
+    GHC.withCString GHC.utf8 str $ \cstr -> do
+      -- we need to copy the trailing NUL
+      len <- BS.c_strlen cstr
+      String <$> BS.packCStringLen (cstr, fromIntegral len + 1)
 
 toChars :: String -> Prelude.String
 toChars (String bs) = unsafeDupablePerformIO $ BS.unsafeUseAsCString bs $ GHC.peekCString GHC.utf8
@@ -53,8 +58,9 @@
 withString :: String -> (CString -> IO a) -> IO a
 withString (String bs) f = BS.unsafeUseAsCString bs f
 
+-- Discards the trailing NUL.
 toByteString :: String -> ByteString
-toByteString (String bs) = bs
+toByteString (String bs) = BS.init bs
 
 -- | O(1) if the input is null-terminated. Otherwise the input is copied into
 -- a null-terminated buffer first.
diff --git a/src/Foreign/JNI/Types.hs b/src/Foreign/JNI/Types.hs
--- a/src/Foreign/JNI/Types.hs
+++ b/src/Foreign/JNI/Types.hs
@@ -5,6 +5,7 @@
 {-# LANGUAGE GADTs #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE RoleAnnotations #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TemplateHaskell #-}
@@ -26,12 +27,17 @@
   , J(..)
   , jnull
   , upcast
+  , arrayUpcast
   , unsafeCast
   , generic
   , unsafeUngeneric
   , jtypeOf
+  , ReferenceTypeName
+  , singToIsReferenceType
   , referenceTypeName
+  , Signature
   , signature
+  , MethodSignature
   , methodSignature
   , JVM(..)
   , JNIEnv(..)
@@ -61,11 +67,13 @@
   , jniCtx
   ) where
 
+import Control.DeepSeq (NFData)
 import qualified Data.ByteString.Lazy as BSL
 import qualified Data.ByteString.Builder as Builder
 import qualified Data.ByteString.Builder.Prim as Prim
 import Data.ByteString.Builder (Builder)
 import Data.Char (chr, ord)
+import Data.Constraint (Dict(..))
 import Data.Int
 import qualified Data.Map as Map
 import Data.Monoid ((<>))
@@ -77,6 +85,7 @@
   , KProxy(..)
 #endif
   )
+import Data.Singletons.Prelude (Sing(..))
 import Data.Singletons.TypeLits (KnownSymbol, symbolVal)
 import Data.Word
 import Foreign.C (CChar)
@@ -87,6 +96,7 @@
   , withForeignPtr
   )
 import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
+import Foreign.JNI.Internal
 import Foreign.JNI.NativeMethod
 import qualified Foreign.JNI.String as JNI
 import Foreign.Marshal.Alloc (allocaBytesAligned)
@@ -99,19 +109,19 @@
 
 -- | A JVM instance.
 newtype JVM = JVM_ (Ptr JVM)
-  deriving (Eq, Show, Storable)
+  deriving (Eq, Show, Storable, NFData)
 
 -- | The thread-local JNI context. Do not share this object between threads.
 newtype JNIEnv = JNIEnv_ (Ptr JNIEnv)
-  deriving (Eq, Show, Storable)
+  deriving (Eq, Show, Storable, NFData)
 
 -- | A thread-local reference to a field of an object.
 newtype JFieldID = JFieldID_ (Ptr JFieldID)
-  deriving (Eq, Show, Storable)
+  deriving (Eq, Show, Storable, NFData)
 
 -- | A thread-local reference to a method of an object.
 newtype JMethodID = JMethodID_ (Ptr JMethodID)
-  deriving (Eq, Show, Storable)
+  deriving (Eq, Show, Storable, NFData)
 
 -- | Not part of the JNI. The kind of 'J' type indices. Useful to reflect the
 -- object's class at the type-level.
@@ -133,7 +143,22 @@
 instance IsReferenceType ('Array ty)
 instance IsReferenceType ty => IsReferenceType ('Generic ty tys)
 
+-- | Produces evidence for IsReferenceType from a `Sing ty`.
+singToIsReferenceType :: Sing (ty :: JType) -> Maybe (Dict (IsReferenceType ty))
+singToIsReferenceType tysing = case tysing of
+    SClass _ -> Just Dict
+    SPrim _ -> Nothing
+    SIface _ -> Just Dict
+    SArray _ -> Just Dict
+    SGeneric tysing' _ -> (\Dict -> Dict) <$> singToIsReferenceType tysing'
+    SVoid -> Nothing
+
 data instance Sing (a :: JType) where
+  -- Using String instead of JNI.String for the singleton data constructors
+  -- is an optimization. Otherwise, the comparisons in Language.Java.call
+  -- and callStatic would involve allocations and cannot be cached.
+  --
+  -- See commit 3da51a4 and https://github.com/tweag/inline-java/issues/11
   SClass :: String -> Sing ('Class sym)
   SIface :: String -> Sing ('Iface sym)
   SPrim :: String -> Sing ('Prim sym)
@@ -141,6 +166,24 @@
   SGeneric :: Sing ty -> Sing tys -> Sing ('Generic ty tys)
   SVoid :: Sing 'Void
 
+instance Show (Sing (a :: JType)) where
+  showsPrec d (SClass s) = showParen (d > 10) $
+      showString "SClass " . showsPrec 11 s
+  showsPrec d (SIface s) = showParen (d > 10) $
+      showString "SIface " . showsPrec 11 s
+  showsPrec d (SPrim s) = showParen (d > 10) $
+      showString "SPrim " . showsPrec 11 s
+  showsPrec d (SArray s) = showParen (d > 10) $
+      showString "SArray " . showsPrec 11 s
+  showsPrec d (SGeneric s sargs) = showParen (d > 10) $
+      showString "SGeneric " . showsPrec 11 s . showsPrec 11 sargs
+  showsPrec _ SVoid = showString "SVoid"
+
+instance Show (Sing (a :: [JType])) where
+  showsPrec _ SNil = showString "SNil"
+  showsPrec d (SCons ty tys) = showParen (d > 10) $
+      showString "SCons " . showsPrec 11 ty . showChar ' ' . showsPrec 11 tys
+
 -- XXX SingI constraint temporary hack because GHC 7.10 has trouble inferring
 -- this constraint in 'signature'.
 instance (KnownSymbol sym, SingI sym) => SingI ('Class (sym :: Symbol)) where
@@ -173,6 +216,10 @@
 upcast :: J a -> JObject
 upcast = unsafeCast
 
+-- | Any array of a reference type can be casted to an array of @Object@s.
+arrayUpcast :: IsReferenceType ty => J ('Array ty) -> JObjectArray
+arrayUpcast = unsafeCast
+
 -- | Unsafe type cast. Should only be used to downcast.
 unsafeCast :: J a -> J b
 unsafeCast (J x) = J (castForeignPtr x)
@@ -278,10 +325,10 @@
   (<> Builder.char7 '\NUL')
 
 -- | The name of a type, suitable for passing to 'Foreign.JNI.findClass'.
-referenceTypeName :: IsReferenceType ty => Sing (ty :: JType) -> JNI.String
-referenceTypeName (SClass sym) = build $ classSymbolBuilder sym
-referenceTypeName (SIface sym) = build $ classSymbolBuilder sym
-referenceTypeName ty@(SArray _) = build $ signatureBuilder ty
+referenceTypeName :: IsReferenceType ty => Sing (ty :: JType) -> ReferenceTypeName
+referenceTypeName (SClass sym) = ReferenceTypeName $ build $ classSymbolBuilder sym
+referenceTypeName (SIface sym) = ReferenceTypeName $ build $ classSymbolBuilder sym
+referenceTypeName ty@(SArray _) = ReferenceTypeName $ build $ signatureBuilder ty
 referenceTypeName (SGeneric ty@(SClass _) _) = referenceTypeName ty
 referenceTypeName (SGeneric ty@(SIface _) _) = referenceTypeName ty
 referenceTypeName _ = error "referenceTypeName: Impossible."
@@ -311,8 +358,8 @@
 signatureBuilder SVoid = Builder.char7 'V'
 
 -- | Construct a JNI type signature from a Java type.
-signature :: Sing (ty :: JType) -> JNI.String
-signature = build . signatureBuilder
+signature :: Sing (ty :: JType) -> Signature
+signature = Signature . build . signatureBuilder
 
 -- | Construct a method's JNI type signature, given the type of the arguments
 -- and the return type.
@@ -323,8 +370,9 @@
   :: [SomeSing ('KProxy :: KProxy JType)]
 #endif
   -> Sing (ty :: JType)
-  -> JNI.String
+  -> MethodSignature
 methodSignature args ret =
+    MethodSignature $
     build $
     Builder.char7 '(' <>
     mconcat (map (\(SomeSing s) -> signatureBuilder s) args) <>
