Should Swift apply "statement scope" for ARC


(John Holdsworth) #1

Hi,

For complex statements in C++ any temporary instances created in the course
of an expression have their lifetime extended to the completion of the current
statement after which they are all deallocated en masse. This makes certain
types of language usage possible and easier to reason with.

I’m bringing this up as I had a problem with some code crashing only when
compiled with release configuration and the problem could have been avoided
if Swift deferred deallocation to the end of a statement. While Swift’s ARC policy
is consistent in itself this seems to be a particular problem interfacing between
language/reference counting systems. My problem code was a Java-Swift Bridge.

A contrived example:

import Foundation

protocol Storage {
    var fp: UnsafeMutablePointer<FILE> { get }
}

class FileStorage: Storage {

    let fp: UnsafeMutablePointer<FILE>

    init?(path: String, mode: String = "w") {
        print("Opening")
        let fp = fopen(path, mode)
        if fp == nil {
            return nil
        }
        self.fp = fp!
    }

    deinit {
        print("Closing")
        fclose(fp)
    }
}

func save(string: String, to: Storage?) {
    if let data = string.data(using: String.Encoding.utf8) {
        print("Saving1")
        if let fp = to?.fp {
            print("Saving2")
            data.withUnsafeBytes {
                _ = fwrite($0, 1, data.count, fp)
            }
            print("Saving3")
        }
    }
}

save(string: "Hello World\n", to: FileStorage(path: "/tmp/a.txt"))

In debug configuration is prints:
Opening
Saving1
Saving2
Saving3
Closing

Whereas in release configuration it prints:
Opening
Saving1
Closing <!!!
Saving2
Saving3

The optimiser is vigorously deallocating objects when they are no longer referenced regardless
of whether an variable referencing it is still in scope (In fairness this particular problem only occurs
for Optional augments of Protocols) but this behaviour seems to be implicit in the current language
spec. The alternative is to retain arguments themselves as I believe they are in Objective-C ARC.

This would have been avoided if the temporary FileStorage instance has been considered to have
a lifetime up to the end of the statement calling function save() and hence the duration of the call.
This needed increase ARC overhead in any way. Just alter the timing of it to be more conservative.

John


(Joe Groff) #2

I haven't used it myself, but is this the use case addressed by `withExtendedLifetime(_:_:)`?

Yeah, if you want to vend resources managed by an object to consumers outside of that object like this, you need to use withExtendedLifetime to keep the object alive for as long as you're using the resources. A cleaner way to model this might be to put the class or protocol in control of handling the I/O to the file handle, instead of vending the file handle itself, so that the ownership semantics fall out more naturally:

protocol Storage {
  func write(bytes: UnsafeRawPointer, count: Int)
}

func save(string: String, to: Storage?) {
    if let data = string.data(using: String.Encoding.utf8) {
        data.withUnsafeBytes {
            _ = to?.write(bytes: $0, count: data.count)
        }
    }
}

-Joe

···

On Sep 21, 2016, at 3:14 PM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

On Wed, Sep 21, 2016 at 16:54 John Holdsworth via swift-evolution <swift-evolution@swift.org> wrote:
Hi,

For complex statements in C++ any temporary instances created in the course
of an expression have their lifetime extended to the completion of the current
statement after which they are all deallocated en masse. This makes certain
types of language usage possible and easier to reason with.

I’m bringing this up as I had a problem with some code crashing only when
compiled with release configuration and the problem could have been avoided
if Swift deferred deallocation to the end of a statement. While Swift’s ARC policy
is consistent in itself this seems to be a particular problem interfacing between
language/reference counting systems. My problem code was a Java-Swift Bridge.

A contrived example:

import Foundation

protocol Storage {
    var fp: UnsafeMutablePointer<FILE> { get }
}

class FileStorage: Storage {

    let fp: UnsafeMutablePointer<FILE>

    init?(path: String, mode: String = "w") {
        print("Opening")
        let fp = fopen(path, mode)
        if fp == nil {
            return nil
        }
        self.fp = fp!
    }

    deinit {
        print("Closing")
        fclose(fp)
    }
}

func save(string: String, to: Storage?) {
    if let data = string.data(using: String.Encoding.utf8) {
        print("Saving1")
        if let fp = to?.fp {
            print("Saving2")
            data.withUnsafeBytes {
                _ = fwrite($0, 1, data.count, fp)
            }
            print("Saving3")
        }
    }
}

save(string: "Hello World\n", to: FileStorage(path: "/tmp/a.txt"))

In debug configuration is prints:
Opening
Saving1
Saving2
Saving3
Closing

Whereas in release configuration it prints:
Opening
Saving1
Closing <!!!
Saving2
Saving3

The optimiser is vigorously deallocating objects when they are no longer referenced regardless
of whether an variable referencing it is still in scope (In fairness this particular problem only occurs
for Optional augments of Protocols) but this behaviour seems to be implicit in the current language
spec. The alternative is to retain arguments themselves as I believe they are in Objective-C ARC.

This would have been avoided if the temporary FileStorage instance has been considered to have
a lifetime up to the end of the statement calling function save() and hence the duration of the call.
This needed increase ARC overhead in any way. Just alter the timing of it to be more conservative.

John

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Xiaodi Wu) #3

I haven't used it myself, but is this the use case addressed by
`withExtendedLifetime(_:_:)`?

···

On Wed, Sep 21, 2016 at 16:54 John Holdsworth via swift-evolution < swift-evolution@swift.org> wrote:

Hi,

For complex statements in C++ any temporary instances created in the course
of an expression have their lifetime extended to the completion of the
current
statement after which they are all deallocated en masse. This makes certain
types of language usage possible and easier to reason with.

I’m bringing this up as I had a problem with some code crashing only when
compiled with release configuration and the problem could have been avoided
if Swift deferred deallocation to the end of a statement. While Swift’s
ARC policy
is consistent in itself this seems to be a particular problem interfacing
between
language/reference counting systems. My problem code was a Java-Swift
Bridge.

A contrived example:

import Foundation

protocol Storage {
    var fp: UnsafeMutablePointer<FILE> { get }
}

class FileStorage: Storage {

    let fp: UnsafeMutablePointer<FILE>

    init?(path: String, mode: String = "w") {
        print("Opening")
        let fp = fopen(path, mode)
        if fp == nil {
            return nil
        }
        self.fp = fp!
    }

    deinit {
        print("Closing")
        fclose(fp)
    }
}

func save(string: String, to: Storage?) {
    if let data = string.data(using: String.Encoding.utf8) {
        print("Saving1")
        if let fp = to?.fp {
            print("Saving2")
            data.withUnsafeBytes {
                _ = fwrite($0, 1, data.count, fp)
            }
            print("Saving3")
        }
    }
}

save(string: "Hello World\n", to: FileStorage(path: "/tmp/a.txt"))

In debug configuration is prints:
*Opening*
*Saving1*
*Saving2*
*Saving3*
*Closing*

Whereas in release configuration it prints:
*Opening*
*Saving1*
*Closing <!!!*
*Saving2*
*Saving3*

The optimiser is vigorously deallocating objects when they are no longer
referenced regardless
of whether an variable referencing it is still in scope (In fairness this
particular problem only occurs
for Optional augments of Protocols) but this behaviour seems to be
implicit in the current language
spec. The alternative is to retain arguments themselves as I believe they
are in Objective-C ARC.

This would have been avoided if the temporary FileStorage instance has
been considered to have
a lifetime up to the end of the statement calling function save() and
hence the duration of the call.
This needed increase ARC overhead in any way. Just alter the timing of it
to be more conservative.

John

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(John Holdsworth) #4

My contrived example was a bit flimsy. I’d better unpack the full story. The real
code I had problems with was based around the following Java instance wrapper:

open class JNIObject: JNIObjectProtocol {

    var _javaObject: jobject?

    open var javaObject: jobject? {
        get {
            return _javaObject
        }
        set(newValue) {
            if newValue != _javaObject {
                let oldValue = _javaObject
                if newValue != nil {
                    _javaObject = JNI.api.NewGlobalRef( JNI.env, newValue )
                }
                else {
                    _javaObject = nil
                }
                if oldValue != nil {
                    JNI.api.DeleteGlobalRef( JNI.env, oldValue )
                }
            }
        }
    }

    deinit {
        javaObject = nil
    }

As a result the following transfer of a Java instance always worked:

    init(imageProducer:ImageProducer) {
        let supr = CanvasBase()
        super.init( javaObject: supr.javaObject )
        image = createImage(imageProducer)
    }

But the following only worked for debug compiles:

    init(imageProducer:ImageProducer) {
        super.init( javaObject: CanvasBase().javaObject )
        image = createImage(imageProducer)
    }

Felt like a bit of a bear trap is all. Statement scope would avoid problems like this.

John

···

On 21 Sep 2016, at 23:34, Joe Groff <jgroff@apple.com> wrote:

On Sep 21, 2016, at 3:14 PM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

I haven't used it myself, but is this the use case addressed by `withExtendedLifetime(_:_:)`?

Yeah, if you want to vend resources managed by an object to consumers outside of that object like this, you need to use withExtendedLifetime to keep the object alive for as long as you're using the resources. A cleaner way to model this might be to put the class or protocol in control of handling the I/O to the file handle, instead of vending the file handle itself, so that the ownership semantics fall out more naturally:

protocol Storage {
func write(bytes: UnsafeRawPointer, count: Int)
}

func save(string: String, to: Storage?) {
   if let data = string.data(using: String.Encoding.utf8) {
       data.withUnsafeBytes {
           _ = to?.write(bytes: $0, count: data.count)
       }
   }
}

-Joe

On Wed, Sep 21, 2016 at 16:54 John Holdsworth via swift-evolution <swift-evolution@swift.org> wrote:
Hi,

For complex statements in C++ any temporary instances created in the course
of an expression have their lifetime extended to the completion of the current
statement after which they are all deallocated en masse. This makes certain
types of language usage possible and easier to reason with.

I’m bringing this up as I had a problem with some code crashing only when
compiled with release configuration and the problem could have been avoided
if Swift deferred deallocation to the end of a statement. While Swift’s ARC policy
is consistent in itself this seems to be a particular problem interfacing between
language/reference counting systems. My problem code was a Java-Swift Bridge.

A contrived example:

import Foundation

protocol Storage {
   var fp: UnsafeMutablePointer<FILE> { get }
}

class FileStorage: Storage {

   let fp: UnsafeMutablePointer<FILE>

   init?(path: String, mode: String = "w") {
       print("Opening")
       let fp = fopen(path, mode)
       if fp == nil {
           return nil
       }
       self.fp = fp!
   }

   deinit {
       print("Closing")
       fclose(fp)
   }
}

func save(string: String, to: Storage?) {
   if let data = string.data(using: String.Encoding.utf8) {
       print("Saving1")
       if let fp = to?.fp {
           print("Saving2")
           data.withUnsafeBytes {
               _ = fwrite($0, 1, data.count, fp)
           }
           print("Saving3")
       }
   }
}

save(string: "Hello World\n", to: FileStorage(path: "/tmp/a.txt"))

In debug configuration is prints:
Opening
Saving1
Saving2
Saving3
Closing

Whereas in release configuration it prints:
Opening
Saving1
Closing <!!!
Saving2
Saving3

The optimiser is vigorously deallocating objects when they are no longer referenced regardless
of whether an variable referencing it is still in scope (In fairness this particular problem only occurs
for Optional augments of Protocols) but this behaviour seems to be implicit in the current language
spec. The alternative is to retain arguments themselves as I believe they are in Objective-C ARC.

This would have been avoided if the temporary FileStorage instance has been considered to have
a lifetime up to the end of the statement calling function save() and hence the duration of the call.
This needed increase ARC overhead in any way. Just alter the timing of it to be more conservative.

John

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Joe Groff) #5

True, though statement scope is also a pretty massive barrier that would prevent important optimizations that are valid for most classes. You might be able to avoid the bear trap by changing the `javaObject` interface into a callback-based interface, that ensures the object remains alive while you have access to it. Instead of:

protocol JNIObjectProtocol {
  var javaObject: jobject { get }
}

would it work to have something like this?

protocol JNIObjectProtocol {
  func withJavaObject(_ body: (jobject) -> ())
}

-Joe

···

On Sep 21, 2016, at 4:01 PM, John Holdsworth <mac@johnholdsworth.com> wrote:

My contrived example was a bit flimsy. I’d better unpack the full story. The real
code I had problems with was based around the following Java instance wrapper:

open class JNIObject: JNIObjectProtocol {

    var _javaObject: jobject?

    open var javaObject: jobject? {
        get {
            return _javaObject
        }
        set(newValue) {
            if newValue != _javaObject {
                let oldValue = _javaObject
                if newValue != nil {
                    _javaObject = JNI.api.NewGlobalRef( JNI.env, newValue )
                }
                else {
                    _javaObject = nil
                }
                if oldValue != nil {
                    JNI.api.DeleteGlobalRef( JNI.env, oldValue )
                }
            }
        }
    }

    deinit {
        javaObject = nil
    }

As a result the following transfer of a Java instance always worked:

    init(imageProducer:ImageProducer) {
        let supr = CanvasBase()
        super.init( javaObject: supr.javaObject )
        image = createImage(imageProducer)
    }

But the following only worked for debug compiles:

    init(imageProducer:ImageProducer) {
        super.init( javaObject: CanvasBase().javaObject )
        image = createImage(imageProducer)
    }

Felt like a bit of a bear trap is all. Statement scope would avoid problems like this.


(Michael Gottesman) #6

My contrived example was a bit flimsy. I’d better unpack the full story. The real
code I had problems with was based around the following Java instance wrapper:

open class JNIObject: JNIObjectProtocol {

    var _javaObject: jobject?

    open var javaObject: jobject? {
        get {
            return _javaObject
        }
        set(newValue) {
            if newValue != _javaObject {
                let oldValue = _javaObject
                if newValue != nil {
                    _javaObject = JNI.api.NewGlobalRef( JNI.env, newValue )
                }
                else {
                    _javaObject = nil
                }
                if oldValue != nil {
                    JNI.api.DeleteGlobalRef( JNI.env, oldValue )
                }
            }
        }
    }

    deinit {
        javaObject = nil
    }

As a result the following transfer of a Java instance always worked:

    init(imageProducer:ImageProducer) {
        let supr = CanvasBase()
        super.init( javaObject: supr.javaObject )
        image = createImage(imageProducer)
    }

But the following only worked for debug compiles:

    init(imageProducer:ImageProducer) {
        super.init( javaObject: CanvasBase().javaObject )
        image = createImage(imageProducer)
    }

Felt like a bit of a bear trap is all. Statement scope would avoid problems like this.

You are thinking about this the inverse way. That the first case works is an artifact of the optimizer failing to do a good enough job. Future improved ARC optimization can cause both to fail.

···

On Sep 21, 2016, at 4:01 PM, John Holdsworth via swift-evolution <swift-evolution@swift.org> wrote:

John

On 21 Sep 2016, at 23:34, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Sep 21, 2016, at 3:14 PM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I haven't used it myself, but is this the use case addressed by `withExtendedLifetime(_:_:)`?

Yeah, if you want to vend resources managed by an object to consumers outside of that object like this, you need to use withExtendedLifetime to keep the object alive for as long as you're using the resources. A cleaner way to model this might be to put the class or protocol in control of handling the I/O to the file handle, instead of vending the file handle itself, so that the ownership semantics fall out more naturally:

protocol Storage {
func write(bytes: UnsafeRawPointer, count: Int)
}

func save(string: String, to: Storage?) {
   if let data = string.data(using: String.Encoding.utf8) {
       data.withUnsafeBytes {
           _ = to?.write(bytes: $0, count: data.count)
       }
   }
}

-Joe

On Wed, Sep 21, 2016 at 16:54 John Holdsworth via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
Hi,

For complex statements in C++ any temporary instances created in the course
of an expression have their lifetime extended to the completion of the current
statement after which they are all deallocated en masse. This makes certain
types of language usage possible and easier to reason with.

I’m bringing this up as I had a problem with some code crashing only when
compiled with release configuration and the problem could have been avoided
if Swift deferred deallocation to the end of a statement. While Swift’s ARC policy
is consistent in itself this seems to be a particular problem interfacing between
language/reference counting systems. My problem code was a Java-Swift Bridge.

A contrived example:

import Foundation

protocol Storage {
   var fp: UnsafeMutablePointer<FILE> { get }
}

class FileStorage: Storage {

   let fp: UnsafeMutablePointer<FILE>

   init?(path: String, mode: String = "w") {
       print("Opening")
       let fp = fopen(path, mode)
       if fp == nil {
           return nil
       }
       self.fp = fp!
   }

   deinit {
       print("Closing")
       fclose(fp)
   }
}

func save(string: String, to: Storage?) {
   if let data = string.data(using: String.Encoding.utf8) {
       print("Saving1")
       if let fp = to?.fp {
           print("Saving2")
           data.withUnsafeBytes {
               _ = fwrite($0, 1, data.count, fp)
           }
           print("Saving3")
       }
   }
}

save(string: "Hello World\n", to: FileStorage(path: "/tmp/a.txt"))

In debug configuration is prints:
Opening
Saving1
Saving2
Saving3
Closing

Whereas in release configuration it prints:
Opening
Saving1
Closing <!!!
Saving2
Saving3

The optimiser is vigorously deallocating objects when they are no longer referenced regardless
of whether an variable referencing it is still in scope (In fairness this particular problem only occurs
for Optional augments of Protocols) but this behaviour seems to be implicit in the current language
spec. The alternative is to retain arguments themselves as I believe they are in Objective-C ARC.

This would have been avoided if the temporary FileStorage instance has been considered to have
a lifetime up to the end of the statement calling function save() and hence the duration of the call.
This needed increase ARC overhead in any way. Just alter the timing of it to be more conservative.

John

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(John Holdsworth) #7

Were this the case I think it would be a step in the wrong direction. Swift is getting
very eager at deallocating objects hence all the "withXYZ()" methods of late which
seem like noise to me. Certainly, having something perform differently from debug
to release builds was not a feature! Viva la Statement Scope which solves all this.

John

···

On 22 Sep 2016, at 23:57, Michael Gottesman <mgottesman@apple.com> wrote:

As a result the following transfer of a Java instance always worked:

    init(imageProducer:ImageProducer) {
        let supr = CanvasBase()
        super.init( javaObject: supr.javaObject )
        image = createImage(imageProducer)
    }

But the following only worked for debug compiles:

    init(imageProducer:ImageProducer) {
        super.init( javaObject: CanvasBase().javaObject )
        image = createImage(imageProducer)
    }

Felt like a bit of a bear trap is all. Statement scope would avoid problems like this.

You are thinking about this the inverse way. That the first case works is an artifact of the optimizer failing to do a good enough job. Future improved ARC optimization can cause both to fail.


(Dave Abrahams) #8

Having had a background in C++, where that rule is de rigeur, and after
working on its core language definition including move semantics, I am
*really* happy we're not making the same mistake in Swift.

The lack of such a guarantee, which is very seldom actually useful
anyhow, is what allows us to turn costly copies (with associated
refcount traffic and, often CoW allocation and copying fallout) into
moves, which are practically free. Adopting it would basically kill our
performance story for CoW.

···

on Thu Sep 22 2016, John Holdsworth <swift-evolution@swift.org> wrote:

On 22 Sep 2016, at 23:57, Michael Gottesman <mgottesman@apple.com> wrote:

As a result the following transfer of a Java instance always worked:

    init(imageProducer:ImageProducer) {

        let supr = CanvasBase()
        super.init( javaObject: supr.javaObject )
        image = createImage(imageProducer)
    }

But the following only worked for debug compiles:

    init(imageProducer:ImageProducer) {
        super.init( javaObject: CanvasBase().javaObject )
        image = createImage(imageProducer)
    }

Felt like a bit of a bear trap is all. Statement scope would avoid problems like this.

You are thinking about this the inverse way. That the first case
works is an artifact of the optimizer failing to do a good enough
job. Future improved ARC optimization can cause both to fail.

Were this the case I think it would be a step in the wrong direction. Swift is getting
very eager at deallocating objects hence all the "withXYZ()" methods of late which
seem like noise to me. Certainly, having something perform differently from debug
to release builds was not a feature! Viva la Statement Scope which solves all this.

--
-Dave


(Joe Groff) #9

Statement scope is a brittle solution these problems. There's no shortage of C++ code that ends up subtly broken when it's refactored and ends up breaking due to hidden dependencies on statement scope. The precise lifetime semantics of C++ also prevent practically any optimization of nontrivial types without the explicit blessing of a handful of special cases like NRVO.

-Joe

···

On Sep 22, 2016, at 5:13 PM, John Holdsworth <mac@johnholdsworth.com> wrote:

On 22 Sep 2016, at 23:57, Michael Gottesman <mgottesman@apple.com> wrote:

As a result the following transfer of a Java instance always worked:

    init(imageProducer:ImageProducer) {
        let supr = CanvasBase()
        super.init( javaObject: supr.javaObject )
        image = createImage(imageProducer)
    }

But the following only worked for debug compiles:

    init(imageProducer:ImageProducer) {
        super.init( javaObject: CanvasBase().javaObject )
        image = createImage(imageProducer)
    }

Felt like a bit of a bear trap is all. Statement scope would avoid problems like this.

You are thinking about this the inverse way. That the first case works is an artifact of the optimizer failing to do a good enough job. Future improved ARC optimization can cause both to fail.

Were this the case I think it would be a step in the wrong direction. Swift is getting
very eager at deallocating objects hence all the "withXYZ()" methods of late which
seem like noise to me. Certainly, having something perform differently from debug
to release builds was not a feature! Viva la Statement Scope which solves all this.


(John Holdsworth) #10

Sorry, I’m being a little slow on the uptake here. So in future optimised Swift the
scope of a variable continuing an object doesn't determine it’s lifetime at all!
That seems quite a departure.

This means you have to be very careful with contained UnsafePointers indeed.
I got as far as needing to do:

    init(imageProducer:ImageProducer) {
        withExtendedLifetime(CanvasBase()) {
            super.init(javaObject: $0.javaObject)
        }
        image = createImage(imageProducer)
    }

..but the compiler was having none of it. For now the rigorous alternative is:

    init(imageProducer:ImageProducer) {
        var locals = [jobject]()
        super.init(javaObject: CanvasBase().localJavaObject(&locals))
        JNI.DeleteLocalRef(locals[0])
        image = createImage(imageProducer)
    }

Some option to reinstate "strong-ness" of a var could be a more flexible alternative to
“withExtendedLifetime” for my particular use case.

    init(imageProducer:ImageProducer) {
        @strong var canvas = CanvasBase()
        super.init(javaObject: canvas.javaObject)
        image = createImage(imageProducer)
    }

John

···

On 23 Sep 2016, at 02:45, Joe Groff <jgroff@apple.com> wrote:

On Sep 22, 2016, at 5:13 PM, John Holdsworth <mac@johnholdsworth.com> wrote:

On 22 Sep 2016, at 23:57, Michael Gottesman <mgottesman@apple.com> wrote:

As a result the following transfer of a Java instance always worked:

   init(imageProducer:ImageProducer) {
       let supr = CanvasBase()
       super.init( javaObject: supr.javaObject )
       image = createImage(imageProducer)
   }

But the following only worked for debug compiles:

   init(imageProducer:ImageProducer) {
       super.init( javaObject: CanvasBase().javaObject )
       image = createImage(imageProducer)
   }

Felt like a bit of a bear trap is all. Statement scope would avoid problems like this.

You are thinking about this the inverse way. That the first case works is an artifact of the optimizer failing to do a good enough job. Future improved ARC optimization can cause both to fail.

Were this the case I think it would be a step in the wrong direction. Swift is getting
very eager at deallocating objects hence all the "withXYZ()" methods of late which
seem like noise to me. Certainly, having something perform differently from debug
to release builds was not a feature! Viva la Statement Scope which solves all this.

Statement scope is a brittle solution these problems. There's no shortage of C++ code that ends up subtly broken when it's refactored and ends up breaking due to hidden dependencies on statement scope. The precise lifetime semantics of C++ also prevent practically any optimization of nontrivial types without the explicit blessing of a handful of special cases like NRVO.

-Joe


(Dave Abrahams) #11

It's always been that way.

···

on Fri Sep 23 2016, John Holdsworth <swift-evolution@swift.org> wrote:

Sorry, I’m being a little slow on the uptake here. So in future
optimised Swift the scope of a variable continuing an object doesn't
determine it’s lifetime at all! That seems quite a departure.

--
-Dave


(Michael Gottesman) #12

Sorry, I’m being a little slow on the uptake here. So in future optimised Swift the
scope of a variable continuing an object doesn't determine it’s lifetime at all!
That seems quite a departure.

This means you have to be very careful with contained UnsafePointers indeed.
I got as far as needing to do:

    init(imageProducer:ImageProducer) {
        withExtendedLifetime(CanvasBase()) {
            super.init(javaObject: $0.javaObject)
        }
        image = createImage(imageProducer)
    }

..but the compiler was having none of it.

What was the error? I am assuming that super.init was not in the same function?

···

On Sep 23, 2016, at 3:29 PM, John Holdsworth <mac@johnholdsworth.com> wrote:

For now the rigorous alternative is:

    init(imageProducer:ImageProducer) {
        var locals = [jobject]()
        super.init(javaObject: CanvasBase().localJavaObject(&locals))
        JNI.DeleteLocalRef(locals[0])
        image = createImage(imageProducer)
    }

Some option to reinstate "strong-ness" of a var could be a more flexible alternative to
“withExtendedLifetime” for my particular use case.

    init(imageProducer:ImageProducer) {
        @strong var canvas = CanvasBase()
        super.init(javaObject: canvas.javaObject)
        image = createImage(imageProducer)
    }

John
  

On 23 Sep 2016, at 02:45, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Sep 22, 2016, at 5:13 PM, John Holdsworth <mac@johnholdsworth.com <mailto:mac@johnholdsworth.com>> wrote:

On 22 Sep 2016, at 23:57, Michael Gottesman <mgottesman@apple.com <mailto:mgottesman@apple.com>> wrote:

As a result the following transfer of a Java instance always worked:

   init(imageProducer:ImageProducer) {
       let supr = CanvasBase()
       super.init( javaObject: supr.javaObject )
       image = createImage(imageProducer)
   }

But the following only worked for debug compiles:

   init(imageProducer:ImageProducer) {
       super.init( javaObject: CanvasBase().javaObject )
       image = createImage(imageProducer)
   }

Felt like a bit of a bear trap is all. Statement scope would avoid problems like this.

You are thinking about this the inverse way. That the first case works is an artifact of the optimizer failing to do a good enough job. Future improved ARC optimization can cause both to fail.

Were this the case I think it would be a step in the wrong direction. Swift is getting
very eager at deallocating objects hence all the "withXYZ()" methods of late which
seem like noise to me. Certainly, having something perform differently from debug
to release builds was not a feature! Viva la Statement Scope which solves all this.

Statement scope is a brittle solution these problems. There's no shortage of C++ code that ends up subtly broken when it's refactored and ends up breaking due to hidden dependencies on statement scope. The precise lifetime semantics of C++ also prevent practically any optimization of nontrivial types without the explicit blessing of a handful of special cases like NRVO.

-Joe


(John Holdsworth) #13

Sorry, I should have included that in the email. The error was:

error: initializer chaining ('super.init') cannot be nested in another expression

Is it possible to build in an exception for withExtendedLifetime?

···

On 25 Sep 2016, at 04:07, Michael Gottesman <mgottesman@apple.com> wrote:

    init(imageProducer:ImageProducer) {
        withExtendedLifetime(CanvasBase()) {
            super.init(javaObject: $0.javaObject)
        }
        image = createImage(imageProducer)
    }

..but the compiler was having none of it.

What was the error? I am assuming that super.init was not in the same function?


(Joe Groff) #14

The block structure of `withExtendedLifetime` is a bit of a lie. It looks a bit goofy, but it's just as good to have an empty `withExtendedLifetime` call after the super.init:

init(imageProducer: ImageProducer) {
  let canvasBase = CanvasBase()
  super.init(javaObject: CanvasBase())
  withExtendedLifetime(canvasBase) {}
  image = createImage(imageProducer)
}

If you look at the standard library implementation of `withExtendedLifetime`, you'll see that all it does is `defer` a call to the underlying internal `_fixLifetime` operation:

https://github.com/apple/swift/blob/master/stdlib/public/core/LifetimeManager.swift#L15

It may be a better design to just expose `fixLifetime` directly instead of trying to tie it to a scope.

-Joe

···

On Sep 25, 2016, at 3:19 AM, John Holdsworth <mac@johnholdsworth.com> wrote:

On 25 Sep 2016, at 04:07, Michael Gottesman <mgottesman@apple.com <mailto:mgottesman@apple.com>> wrote:

    init(imageProducer:ImageProducer) {
        withExtendedLifetime(CanvasBase()) {
            super.init(javaObject: $0.javaObject)
        }
        image = createImage(imageProducer)
    }

..but the compiler was having none of it.

What was the error? I am assuming that super.init was not in the same function?

Sorry, I should have included that in the email. The error was:

error: initializer chaining ('super.init') cannot be nested in another expression

Is it possible to build in an exception for withExtendedLifetime?