diff --git a/System/OsString/Common.hs b/System/OsString/Common.hs
--- a/System/OsString/Common.hs
+++ b/System/OsString/Common.hs
@@ -218,15 +218,24 @@
 unsafeEncodeUtf = either (error . displayException) id . encodeWith utf8
 #endif
 
+#ifdef WINDOWS
 -- | Encode a 'String' with the specified encoding.
-encodeWith :: TextEncoding
+--
+-- Note: We expect a "wide char" encoding (e.g. UCS-2 or UTF-16). Anything
+-- that works with @Word16@ boundaries. Picking an incompatible encoding may crash
+-- filepath operations.
+encodeWith :: TextEncoding  -- ^ text encoding (wide char)
            -> String
            -> Either EncodingException PLATFORM_STRING
 encodeWith enc str = unsafePerformIO $ do
-#ifdef WINDOWS
   r <- try @SomeException $ GHC.withCStringLen enc str $ \cstr -> WindowsString <$> BS8.packCStringLen cstr
   evaluate $ force $ first (flip EncodingError Nothing . displayException) r
 #else
+-- | Encode a 'String' with the specified encoding.
+encodeWith :: TextEncoding
+           -> String
+           -> Either EncodingException PLATFORM_STRING
+encodeWith enc str = unsafePerformIO $ do
   r <- try @SomeException $ GHC.withCStringLen enc str $ \cstr -> PosixString <$> BSP.packCStringLen cstr
   evaluate $ force $ first (flip EncodingError Nothing . displayException) r
 #endif
@@ -493,9 +502,20 @@
 
 -- | /O(1)/ The length of a 'OsString'.
 --
+-- This returns the number of code units
+-- (@Word8@ on unix and @Word16@ on windows), not
+-- bytes.
+--
+-- >>> length "abc"
+-- 3
+--
 -- @since 1.4.200.0
 length :: PLATFORM_STRING -> Int
+#ifdef WINDOWS
+length = coerce BSP.numWord16
+#else
 length = coerce BSP.length
+#endif
 
 -- | /O(n)/ 'map' @f xs@ is the OsString obtained by applying @f@ to each
 -- element of @xs@.
diff --git a/System/OsString/Internal.hs b/System/OsString/Internal.hs
--- a/System/OsString/Internal.hs
+++ b/System/OsString/Internal.hs
@@ -55,9 +55,13 @@
 unsafeEncodeUtf :: HasCallStack => String -> OsString
 unsafeEncodeUtf = OsString . PF.unsafeEncodeUtf
 
--- | Encode an 'OsString' given the platform specific encodings.
+-- | Encode a 'FilePath' with the specified encoding.
+--
+-- Note: on windows, we expect a "wide char" encoding (e.g. UCS-2 or UTF-16). Anything
+-- that works with @Word16@ boundaries. Picking an incompatible encoding may crash
+-- filepath operations.
 encodeWith :: TextEncoding  -- ^ unix text encoding
-           -> TextEncoding  -- ^ windows text encoding
+           -> TextEncoding  -- ^ windows text encoding (wide char)
            -> String
            -> Either EncodingException OsString
 #if defined(mingw32_HOST_OS) || defined(__MINGW32__)
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,9 @@
 # Changelog for [`os-string` package](http://hackage.haskell.org/package/os-string)
 
+## 2.0.3 *May 2024*
+
+* Fix `length` function wrt [#17](https://github.com/haskell/os-string/issues/17)
+
 ## 2.0.2.2 *May 2024*
 
 * Fix compilation on big-endian arches, by Andrew Lelechenko
diff --git a/os-string.cabal b/os-string.cabal
--- a/os-string.cabal
+++ b/os-string.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.2
 name:               os-string
-version:            2.0.2.2
+version:            2.0.3
 
 -- NOTE: Don't forget to update ./changelog.md
 license:            BSD-3-Clause
diff --git a/tests/bytestring-tests/Properties/Common.hs b/tests/bytestring-tests/Properties/Common.hs
--- a/tests/bytestring-tests/Properties/Common.hs
+++ b/tests/bytestring-tests/Properties/Common.hs
@@ -454,6 +454,10 @@
 
   , ("length" ,
    property $ \x -> numWord x === fromIntegral (length (B.unpack x)))
+#if defined(OSWORD) || defined(WIN) || defined(POSIX)
+  , ("length abc" ,
+   once $ B.length (B.pack [0xbb, 0x03]) == 2)
+#endif
   , ("count" ,
    property $ \(toElem -> c) x -> B.count c x === fromIntegral (length (elemIndices c (B.unpack x))))
   , ("filter" ,
