Are you suggesting that it should have an "IO Int" result type to force sequencing? Is this warranted?
Yes. This is warranted. That's why Foreign.Storable.peek has IO in its result type. On any CString with a finite lifetime, it is necessary to sequence any reads and writes, and IO is the way this is done in base. By contrast, on a CString that is both immutable and has an infinite lifetime, we do not need to sequence reads. What kinds of CStrings fit the bill? Only those backed by primitive string literals. So, for example, if you have:
myString :: CString
myString = Ptr "foobar"#
Since, myString is backed by something in the rodata section of a binary (meaning that it will never change and it will never be deallocated), then we do not care if reads get floated around. There are no functions in base for unsequenced reads, but in primitive, you'll find Data.Primitive.Ptr.indexOffPtr, which is unsequenced. So something like this would be ok:
someOctet :: Word8
someOctet = Data.Primitive.Ptr.indexOffPtr myString 3
The cstringLength# in GHC.CString is similar to indexOffPtr. In fact, it could be implemented using indexOffPtr. The reason that cstringLength# exists (and in base of all places) is so that a built-in rewrite rule perform this transformation:
cstringLength "foobar"#
==>
6#
This will eventually be used to great effect in bytestring. See
https://github.com/haskell/bytestring/pull/191.
To get back to the original question, I think that any user-facing cstringLength
function should probably be:
cstringLength :: CString -> IO Int
We need a separate FFI call that returns its result in IO to accomplish this. But
this just be done in base rather than ghc-prim. There are no interesting rewrite
rules that exist for such a function.