Thisexample shows that the 4GL provides completely dynamic class loading based on runtime state. Two completely unrelated classes that happen to have methods of the same name and signature (msg()) will load and work with no complaints just by modifying the propath at runtime. This doesn't just affect the class loading, it affects the type of the object reference (in this case the var named a). The type is considered Foo but what does Foo mean when multiple different Foo implementations can be assigned without complaint?
1. Is this duck typing problem also present in the class hierarchy? For example, if Bar is a child class of Foo, can you instantiate multiple instances of Bar with different Foo parent classes just by changing the propath between instantiation attempts?
2. To what degree does a method signature matter in this duck typed world? What aspects of the signature are checked at compile time and what parts are only checked at runtime when a method is executed? In other words: how dynamic is the method invocation and how weak is the compile-time typing?
At compile time, ABL verifies that the specified object type is within the class hierarchy of the specified object reference. At run time, the AVM checks the validity of the cast operation. Therefore, if you access a class member on the cast object reference that exists for the cast data type, but the referenced object at run time does not actually define the accessed class member, the AVM raises ERROR at run time.
You can also use the CAST function to perform all casting operations at compile time. The primary reason for using the DYNAMIC-CAST function is to cast object references based on run-time conditions that determine the object type to use for the cast.
STATIC specifies a static constructor that executes exactly once in an ABL session the first time you reference a class type that defines this constructor in its class hierarchy. You cannot invoke a static constructor in any other way or at any other time. You can define only one static constructor for a given class. If you do not define a static constructor, ABL defines a default static constructor to initialize the static members of a class.
When switching the PROPATH, and a different class file exists, is the static constructor invoked again (if this class was already loaded)? This might depend on how 4GL keeps the loaded class registry - by class name or something else. Also, what happens with the static members - do they get initialized?
And a quick note about the static c'tors - looks like they are invoked only once, the first time this class (resolved by its name) is loaded. If PROPATH changes and a different file defines the same class, the static c'tor will not be invoked. Anyway, more analysis is required for static members.
Use string(THIS-OBJECT) (or string(ooVar)) to determine the handle ID (this is suffixed to the class name, like oo.Bar_1001); but I think THIS-OBJECT will show you (at the ID) only the object, and not its super-classes - these might be hidden via the super-procedure chains, as compiler prohibits THIS-PROCEDURE (and other system-handle procedures) being executed from within a class code.
This (and a test on a static var) shows that the class is loaded only once, no matter if the propath changes, as the var reports the same value, before and after the propath change. I think 4GL creates a persistent copy of this program and associates it at runtime with all the static member access.
But using the POLY arguments at the method calls, makes this impossible - the compiler checks the full signature, unless we have a POLY via DB_REF_NON_STATIC reference (h::f1). In this case, the runtime does the argument validation - the compiler just checks the number of arguments. Although for OO cases, the POLY value conversion at runtime looks more strict than for RUN statement.
So, I don't think we can even emit Java calls for the method invocations (and using some dynamic proxy to determine the program where the target resides) - we will need to rely on dynamic invocation, like we do for RUN statements. This is mostly because the Java compiler will not be able to choose to the proper overloaded method,
My initial thought is that this should be rare. It is certainly a bad practice and will lead to strange results (as you've noted with the statics). I'm inclined to force such things to be replaced rather than supporting them since the result would be significantly worse just to support a really bad design decision of Progress. At a minimum, this would allow direct member access in the parent hierarchy, even if the methods had to be dynamically dispatched.
I'm not too surprised about the dynamic nature of method calls. On the other hand, using POLY also seems a somewhat rare case. Is there any reason that a method call with non-POLY arguments can't be directly dispatched?
Another issue with using Java inheritance: in Java, you can't reduce the visibility of a member, just upgrade it. In 4GL, you can have a public method in a base class and a protected version in the sub-class.
"On a state-reset AppServer, a given AppServer agent begins a fresh ABL session with each new client connection. Thus, each client connection to a state-reset AppServer re-initializes the static members of each class that is referenced."
When overloading methods, note that instance and static methods overload each other. In other words, the scope (presence or absence of the STATIC keyword) is not counted in distinguishing one overloaded method from another. For this reason, you cannot define an instance method and a static method in the same class hierarchy that both have the same signature. Such a combination causes ABL to raise a compile-time error.
I think so. You aren't supposed to be able to execute it directly and it only gets executed when garbage collected. This seems exactly the same as the Java finalizer concept. I think we can map to that unless there is some unexpected behavior we find during testing.
JVM specs don't guarantee which tread will execute the finalize() method - and if the DESTRUCTOR contains 4GL logic, this needs to be executed on the Conversation thread (at least). More, the destructor will be called when DELETE OBJECT is executed, and it can contain even UI statements.
We will have to be tracking object instances anyway. For example, SESSION:FIRST-OBJECT and SESSION:LAST-OBJECT allows one to walk the entire set of objects that are currently instantiated. This is insane of course, any business system of reasonable complexity will have massive numbers of objects created at any one point in time. Still, when the object would be removed from that list I guess we can also handle the destructor processing.
The object references maintained by the FIRST-OBJECT attribute and the NEXT-SIBLING property do not count as references for garbage collection. That is, if a class instance is referenced only on the session object chain, it is available for automatic garbage collection.
5. Please consider instantiation of objects via the NEW() function, NEW statement, DYNAMIC-NEW and Progress.Lang.Object:New(). Can we use Java .class instances for the NEW() function/NEW statement? The constructor invocation order and rules about invocation of super() seem to match Java (based on the docs). Considering that we have to maintain a chain of instantiated objects AND we need to duplicate error behavior, I think it is not safe to use the Java new operator directly. But if we can hard code .class references, that is a benefit.
6. What are the rules for destructor invocation? The documentation suggests it can occur from DELETE OBJECT, garbage collecton and when there is a failure during constructor invocation. Is this correct? Can you think of any reason we actually need the finalizer or can we drive everything off an explicit or implicit DELETE?
8. We need to implement a replacement for the Progress.* and OpenEdge.* built-in classes. I was planning a hand-coded Java implementation though I am open to the alternative (implement our own version of those as 4GL classes which we actually convert). What are your opinions on this and where do we keep the classes?
I'm working on the resolution of the converted Java class name information (simple class name, package name and fully qualified class name) for a given original source filename. In rev 11302, every CLASS_NAME, VAR_CLASS or other class reference is now has a source-file annotation with the original file name of the class being referenced.
My idea is that at parsing time, we have resolved the 4GL fully qualified names to some actual unambiguous source file. By mapping to that file's specific fully qualified Java class name, then everything else will be unambiguous. Do you see any flaws in my logic or unexpected results? I am assuming that the duck typing won't work, which is OK.
Stuff which uses 4GL-style class names may not work if we don't keep this mapping in an external file; for example, doing Progress.Lang.Class:GetClass("oo.Foo"). - of oo.Foo is in file abl/prj1/oo/Foo.cls, and converted Java file name is src/prj1/oo/Foo.java - we will need to know (at runtime) that oo.Foo in 4GL maps to prj1.oo.Foo in FWD. This is possible if you have the prj1/ entry on PROPATH.
3a8082e126