I've been doing a lot of libc interaction lately a...
# stdlib
k
I've been doing a lot of libc interaction lately and dealing with bitmasks is quite painful in Kotlin. Furthermore, figuring out how to expose an API to work with bitmasks is even more challenging. For example the
statx
system call defines a bitmask for attributes of a file you'd like to get. I've come up with the following abstraction around this but am not pleased with it because it requires an additional allocation.
Copy code
public value class FileStatusRequest internal constructor(internal val bitMask: UInt)

@FileRequestDsl public class FileStatusRequestBuilder internal constructor() {

    private var bitMask: UInt = 0u

    public fun requestFileSize() { bitMask = bitMask or STATX_SIZE }

    public fun requestBlocksOccupied() { bitMask = bitMask or STATX_BLOCKS }

    public fun build(): FileStatusRequest = FileStatusRequest(bitMask)
}
Swift has the
OptionSet
protocol which could abstract bitmask operations quite nicely. A Kotlin equivalent might look like the following.
Copy code
value class FileStatusRequest(internal var bitMask: UInt) : OptionSet<FileStatusTrait> {
  
  override fun insert(trait: FileStatusTrait) { bitMask = bitMask or trait.statxMask }
  override fun remove(trait: FileStatusTrait) { bitMask = bitMask and trait.statxMask.inv() }
}

enum class FileStatusTrait(internal val statxMask: UInt) { ... }
Has the stdlib team considered something like
OptionSet
as a means of wrangling (mainly) bitmasks?
This of course hinges on the potential to have
var
in a value class to present any benefit.
e
why not a simple wrapper with some operations?
Copy code
@JvmInline
value class StatxMask(val mask: Int) {
    operator fun plus(other: StatxMask): StatxMask =
        StatxMask(this.mask or other.mask)
    operator fun minus(other: StatxMask): StatxMask =
        StatxMask(this.mask and other.mask.inv())
}

val STATX_TYPE = StatxMask(platform.posix.STATX_TYPE)
// etc.
I don't see what you gain from a builder
k
A more descriptive API that doesn't lead the consumer of the API to believe they're using bitmasks
e
Kotlin internally has a common pattern for representing bitflags, e.g. https://github.com/JetBrains/kotlin/blob/master/libraries/kotlinx-metadata/src/kotlinx/metadata/Flag.kt but those are quite complex things spread among multiple fields (which warrants the API complexity)
value class(var)
isn't going to happen.
copy var
might, but isn't implemented. https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md#copyable-properties-of-immutable-types
c
I don't think it's in the Kotlin standard library yet, but I would love to have access to Java's
EnumSet
: it's a bit mask implementation based on an enum, which is very idiomatic in Kotlin
👍 2
r
I was about to ask how hard it would be to port
EnumSet
to Kotlin - there's nothing native in it I can see.
c
As far as I know, it's just a regular abstract class with two different implementations selected internally based on performance.
k
TIL about EnumSet. That's really neat.
Although it doesn't look like the underlying bitmask is accessible by the user, which I would need to submit to a statx syscall.
r
I assumed you were in kotlin native, so didn't have access to Java classes like
EnumSet
- I was thinking it would be a template for writing something similar in Kotlin, rather than something you could use out of the box, in which case of course it would be easy to expose the bitmask.
k
I am. I was thinking in the abstract for how Kotlin stdlib would adopt something like this though
c
Could be a good idea of a mini-library to provide a multiplatform implementation of EnumSet which has variants that expose the bitset
Bonus point: it could actually be a value class instead of needing an allocation like on the JVM
r
And hence immutable unlike the JVM one
c
Honestly it sounds like a fun exercise
k
I don’t think there’s much benefit to it being a value class since the underlying value is immutable.
You still need a builder / allocated set thing that negates the benefit of a value class.
c
Why? You just have to override
Set.plus
to return an
EnumSet
, and you can do
EnumSet<YourEnum>() + Field1 + Field2
with no allocations if
EnumSet
is a value class
k
Hm, I suppose you might be right. You could do the following when sealed value classes are a thing.
Copy code
sealed value class FileStatusRequest(internal val statxMask: UInt) : Set<FileStatusRequest> {
  
  object FileSize : FileStatusRequest(STATX_SIZE)
  object BlockCount : FileStatusRequest(STATX_BLOCKS)

  value class Composite(mask: UInt) : FileStatusRequest(mask)

  override operator fun plus(other: FileStatusRequest) = Composite(statxMask or other.statxMask)
  override operator fun minus(other: FileStatusRequest) = Composite(statxMask and other.statxMask.inv())
  // etc. 
}
I wrote that in slack so who knows if the compiler would complain
c
You don't need sealed classes
Copy code
@JvmInline value class Bitmask<T : Enum<T>>(val mask: UInt): Set<T> {
    override operator fun plus(other: T) = Bitmask(mask or other.ordinal.toUInt())
    // …

    companion object {
        fun noneOf<T: Enum<T>>() = Bitmask<T>(0u)
    }
}

enum class FileStatus {
    FileSize,
    BlockCount,
}

// Usage:
val mask = noneOf<FileStatus>() + FileStatus.FileSize
(wrote in Slack too)
k
You could also split things up into a separate set thingy and trait thingies
yes
beat me to it
c
Depending on your use case you probably don't want to use
Enum.ordinal
, but you get the point 🙂
k
This is all good food for though and I’ll play with it in my library
c
Plus, this way, you only have to declare a new enum that matches the C constants, and you're good to go
If I were you I would also write a mutable variant for building though, it's probably easier for the compiler to optimize (I did not check however)
k
This is what I ended up going with. It’s totally allocation free and avoids pointer chasing of enums.
Copy code
interface OptionSet<Option> {
    operator fun plus(other: Option): OptionSet<Option>
    operator fun minus(other: Option): OptionSet<Option>
    operator fun contains(element: Option): Boolean
}

value class FileStatusRequest private constructor(
    internal val mask: UInt
) : OptionSet<FileStatusRequest> {

    override fun plus(other: FileStatusRequest): FileStatusRequest = FileStatusRequest(
        mask or other.mask
    )

    override fun minus(other: FileStatusRequest): FileStatusRequest = FileStatusRequest(
        mask and other.mask.inv()
    )

    override fun contains(element: FileStatusRequest): Boolean {
        return mask and element.mask == element.mask
    }

    companion object {
        val Empty = FileStatusRequest(0u)
        val First = FileStatusRequest(1u)
        val Second = FileStatusRequest(2u)
    }
}

FileStatusRequest.Empty + FileStatusRequest.First + FileStatusRequest.Second // mask: 3u
It is, to @ephemient’s point, pretty much what they recommended
e
not sure what you mean by "allocation free" - as soon as you upcast a value type to an interface, it must be boxed
k
You’re not expected to upcast ever
I could honestly drop the interface but it’s a nice implementation guardrail
e
not sure what it's guarding but ok, if it's never used
👍 1