- Type: Design proposal
- Author: Andrey Breslav
- Contributors: Vladimir Reshetnikov, Stanislav Erokhin
- Status: Under consideration
- Prototype: Not started
Discussion of this proposal is held in this issue.
An iterator (for example, one generated by a coroutine) may iterate over a file or some other disposable resource. If the iteration completes normally (i.e. by reaching the last item), the iterator can dispose the underlying resource, but if
- an exception occurs during one of the iterations,
break
,continue
orreturn
cause early termination of the loop,
the resource will never be disposed.
To overcome this issue, C# has all for
-loops wrapped in try
/finally
, where the finally
block checks whether the iterator implements IDisposable
and if so, calls the Dispose()
method.
Here we propose the same for Kotlin.
- Description: Handling
finally
blocks in Kotlin coroutines - Issue #1: Handling of
finally
blocks - Issue #9: Allow suspension points in
finally
blocks? - C# Iterator block implementation details: auto-generated state machines
The idea is to wrap every for
-loop that uses an iterator into a try
/finally
. The handler in the finally
block is calling the following function (to be added to kotlin-runtime
):
internal fun disposeIfNeeded(obj: Any?) {
if (obj is Disposable) {
obj.dispose()
}
}
The aforementioned Disposable
interface should be added to kotlin-runtime
as follows:
package kotlin
public interface Disposable {
fun dispose()
}
The Standard Library code should be revised and any iteration utilities there that iterate without using for
-loops must be updated to provide the same semantics.
Apart from adding these two items to kotlin-runtime
this results in generating extra byte code instructions for every for
-loop that uses an iterator (note that loops that enumerate number ranges don't use iterators most of the time). This will amount to a minimum of 6 instructions per for
-loop (+ TRYCATCHBLOCK
entries in the Code Attribute). To be precise, this amounts to two instructions per copy of the finally
block):
ALOAD 1 # iterator
INVOKESTATIC disposeIfNeeded(Ljava/lang/Object;)V
There are at least two copies of the finally
block: one for normal termination, and another for exceptional termination. An extra copy is generated for each exit point, such as break
, continue
and return
inside the loop.
Another two instructions need to be added for the implicit catch
block (which has to be generated to implement the try
/finally
semantics): one to jump over the catch
block in case of normal termination, another — to rethrow the exception in the catch
block.
Here's the byte code for teh simplest case:
ALOAD 0 ; iterable
INVOKEINTERFACE java/lang/Iterable.iterator ()Ljava/util/Iterator;
ASTORE 2 ; tmp_iterator
START_TRY:
LOOP:
ALOAD 2 ; tmp_iterator
INVOKEINTERFACE java/util/Iterator.hasNext ()Z
IFEQ FINALLY
ALOAD 2 ; tmp_iterator
INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object;
ASTORE 1 ; loop_variable
// loop body
// ...
GOTO LOOP
FINALLY:
ALOAD 2 ; tmp_iterator ; overhead
INVOKESTATIC disposeIfNeeded (Ljava/lang/Object;)V ; overhead
; overhead
GOTO AFTER_CATCH
CATCH:
ALOAD 2 ; tmp_iterator ; overhead
INVOKESTATIC disposeIfNeeded (Ljava/lang/Object;)V ; overhead
ATHROW ; overhead
AFTER_CATCH:
// code after the loop
// ...
The code compiled with any pre-1.1 will run against the new library, but the for
-loops there won't be disposing their iterators. This is not exactly a binary incompatibility: all code will keep working as before, but old clients for the new code will be unprepared, and won't hold their part of the deal (that is expected by the new code).
A minor and incomplete mitigation for this will be having the iterators dispose themselves when teh iteration completes normally, i.e. when
hasNext()
returnsfalse
for the first time. This won't help the case of an exceptions or early termination of a loop, though.
If we are ready to live with this, i.e. warn the users to recompile their old code, some new concerns arise:
- the recompiled code will depend on the new
kotlin-runtime
, - recompiling the old code with Kotlin 1.1 may be undesirable for setup/compiler changes reasons.
This leads to thinking of adding this feature as a minimal change to Kotlin 1.0.X (under a flag, probably), and making it emit code that is tolerant to the old runtime (e.g. doesn't fail if disposeIfNeeded()
or Disposable
are missing).
The possible options here are:
- Use
Class.forName
to check for presense ofDisposable
(do nothing if it's not present), - Catch
NoSuchMethodError
around the call todisposeIfNeeded()
and swallow it.
Neither of these approaches will stop ProGuard from complaining.
Both these approaches are a bit costly when applied straightforwardly:
- too many instructions,
- time-consuming operations (class lookup or filling the stack trace for an error).
We can have fewer instructions by emitting a method that encapsulates this logic into
- each module (troublesome for incremental compilation),
- each file (more code).
We can mitigate time costs by caching the results: having a static flag for either whether Disposable
is present, or disposeIfNeeded()
.
TODO
Looks like an explicit try
/finally
has be generated around for
-loops.
- Maybe the compatibility issues are prohibitive for this feature?
- Other ways of iterating, that do not use
for
-loops, won't become disposable-aware through this proposal - This is introducing a whole new concept of disposability into the ecosystem