I have a data structure which has members that are not thread safe and the caller needs to lock the resource for reading and writing as appropriate. Here's a minimal code sample:
class ExampleResource : LockableProjectItem {
override val readWriteLock: ReadWriteLock = ReentrantReadWriteLock()
@RequiresReadLock
val nonThreadSafeMember: String = ""
}
interface LockableProjectItem {
val readWriteLock: ReadWriteLock
}
fun <T : LockableProjectItem, Out> T.readLock(block: T.() -> Out): Out {
try {
readWriteLock.readLock().lock()
return block(this)
} finally {
readWriteLock.readLock().unlock()
}
}
fun <T : LockableProjectItem, Out> T.writeLock(block: T.() -> Out): Out {
try {
readWriteLock.writeLock().lock()
return block(this)
} finally {
readWriteLock.writeLock().unlock()
}
}
annotation class RequiresReadLock
A call ExampleResource.nonThreadSafeMember
might then look like this:
val resource = ExampleResource()
val readResult = resource.readLock { nonThreadSafeMember }
To make sure that the caller is aware that the resource needs to be locked, I would like the IDE to issue a warning for any members that are annotated with @RequiresReadLock
and are not surrounded with a readLock
block. Is there any way to do this in IntelliJ without writing a custom plugin for the IDE?
CodePudding user response:
I think this is sort of a hack, but using context receivers might work. I don't think they are intended to be used in this way though.
You can declare a dummy object
to act as the context receiver, and add that as a context receiver to the property:
object ReadLock
class ExampleResource : LockableProjectItem {
override val readWriteLock: ReadWriteLock = ReentrantReadWriteLock()
// properties with context receivers cannot have a backing field, so we need to explicitly declare this
private val nonThreadSafeMemberField: String = ""
context(ReadLock)
val nonThreadSafeMember: String
get() = nonThreadSafeMemberField
}
Then in readLock
, you pass the object
:
fun <T : LockableProjectItem, Out> T.readLock(block: context(ReadLock) T.() -> Out): Out {
try {
readWriteLock.readLock().lock()
return block(ReadLock, this)
} finally {
readWriteLock.readLock().unlock()
}
}
Notes:
This will give you an error if you try to access
nonThreadSafeMember
without the context receiver:val resource = ExampleResource() val readResult = resource.nonThreadSafeMember //error
You can still access
nonThreadSafeMember
without acquiring a read lock by doing e.g.with(ReadLock) { // with(ReadLock) doesn't acquire the lock, just gets the context receiver resource.nonThreadSafeMember // no error }
But it's way harder to accidentally write something like this, which I think is what you are trying to prevent.
If you call another function inside
readLock
, and you want to accessnonThreadSafeMember
inside that function, you should mark that function withcontext(ReadLock)
too. e.g.fun main() { val resource = ExampleResource() val readResult = resource.readLock { foo(this) } } context(ReadLock) fun foo(x: ExampleResource) { x.nonThreadSafeMember }
The context receiver is propagated through.