views:

87

answers:

1

I have the following code:

function p (str)
    Response.Write VBLF & str
end function

function nop: end function

class Test1
    private sub class_initialize
        p "Test1 Start"
    end sub

    private sub class_terminate
        p "Test1 End"
    end sub
end class

class Test2
    private sub class_initialize
        p "    Test2 Start"
    end sub

    private sub class_terminate
        p "    Test2 End"
    end sub
end class

When I run:

with new Test1
    with new Test2
    end with
end with

I expect the output:

Test1 Start
    Test2 Start
    Test2 End
Test1 End

But I get:

Test1 Start
    Test2 Start
Test1 End
    Test2 End

I do, however, get what I expected if I run either of the following:

with new Test1
    with new Test2
        nop
    end with
end with

with new Test1
    with new Test2
    end with
    nop
end with

But not the following:

with new Test1
    nop
    with new Test2
    end with
end with

VBScript has a fairly strong guarantee about GC'ing objects right away, and I'm using (abusing?) this guarantee for a variety of purposes in my applications. Without the nop, why is it collecting Test1 and Test2 in the "wrong" order?

+8  A: 

What a fascinating bug.

I do not have a debug build of the VBScript engine handy. (The ten year old hard disk that I was keeping it on has apparently gone bad some time in the two years since I last looked at it.) However, I can easily guess what is going on here. My suspicion is that I did not mark the instruction corresponding to the "end with" as a point at which a statement has ended and the collector must run.

Let's consider that hypothesis.

In your first example, when would the GC run? When the program ends. At that point, the terminators are run in the order that the objects were created. (This is an implementation detail that you should not rely upon.)

In your second and third example the end of the call to nop triggers a GC, and that happens right next to the end with. (I'm not quite sure why the second example triggers a GC when the object is eligible for collection; I don't recall the exact semantics of the codegen for popping the object reference out of the with block scope.)

In your fourth example, the GC is triggered before the inner with, and does nothing.

Since I wrote both the termination logic and a fair amount of the "with" processing code I undoubtedly caused this bug, so many apologies. Since apparently we've lived with it with no major problems for the last eleven or twelve years, and since it only affects "with" blocks that have no contents, I don't imagine it will ever be fixed.

Regardless, you should not rely on termination order. That's a bad programming practice.

For more on the fascinating subject of VBScript termination logic see:

http://stackoverflow.com/questions/3728970/what-is-the-order-of-destruction-of-objects-in-vbscript

Eric Lippert
Thanks for answering. I actually spent a couple of hours combing through your blog before posting this. I'm glad to know what the cause of the problem is, even though Microsoft probably isn't going to rush out a fix for a bug in a twelve-year-old language so obscure it went that long undetected. And you're right, of course, that this is a slightly nutty thing to do to begin with, even before factoring in that the actual code uses the finalizers to print HTML closing tags and alter global variables. Without lambdas, blocks, or `using`, though, I still think it may be the best option.
Thom Smith
"you should not rely on termination order. That's a bad programming practice." - do you mean specifically with respect to VBScript? In some languages, where object termination order is very well specified, relying on termination order can be a very good programming practice (with [RAII](http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization) in C++, for example).
bacar
@bacar: I mean specifically with respect to VBScript. However, I do not believe RAII to be a good programming practice in C++. I am aware that I am in the minority on this point. Basically, if the execution of a destructor has an important semantic or correctness impact on a program, then I would like that to be written like this: "foo.DoSomethingImportantForTheCorrectnessOfTheProgram()" so that the maintenance programmer reads it, understands it, and maintains the correctness of the program.
Eric Lippert
With RAII, the important code is written like this: "} } } }" Now tell me which one of those close braces means "do something vitally important for the correctness of this program". RAII makes it easy on the writer of the code and hard on the maintainer, which is exactly the opposite of how it should be.
Eric Lippert
@Eric - I would have thought that _hard_ on the writer _and_ hard on the maintainer would be exactly the opposite of how it should be... Do you think that there is an inherent tradeoff between ease of writing and ease of maintenance?
kvb
@kvb: I mean that it is better to spend some effort up front on writing code that is explicit in its clear correctness than to be clever about relying on a deterministic order in automatic cleanup. It's OK to be hard on the original writer of the code; the code only gets written once. It gets read and modified a lot.
Eric Lippert
@Eric: What's easier on the maintainer? Needing to learn a unique cleanup scenario for every feature, or knowing that it will be dealt with automatically? Checking a single point for correct cleanup, or checking _every single usage of the feature?_RAII makes code *correct by default* and *self-documenting*. Learning - *once* - that "}" idiomatically executes important code is a small deal.C++ has exceptions and no 'finally' - exception safety without RAII is error-prone. Even with 'finally' support, _the developer/maintainer/user may still forgot to call the cleanup!_
bacar