VB:Tutorials:Building a Better Scripting Language by Using Dynamic Classes
From GPWiki
The wiki is now hosted by GameDev.NET at wiki.gamedev.net. All gpwiki.org content has been moved to the new server. However, the GPWiki forums are still active! Come say hello.
[edit] Building a Better Scripting Language by Using Dynamic Classesby MetalWarrior Sample Project - Simple Object Browser
[edit] IntroductionIn this tutorial I'm going to explain how to examine a class module at run-time in Visual Basic. When I say examine, I mean the ability to get information on all public members (properties and methods) of a class. This information includes the actual string name of the member, the type (property let, property get, property set, method), parameter data, return type data, and more. On top of all that, we're then going to look at how to dynamically call methods and set/retrieve properties, using this information. All of this is accomplished without knowing a thing about the class at design time. Crazy go nuts!
[edit] Part 1: Examining a Dynamic ClassYou may be saying, "what exactly is a dynamic class?" Stop talking to yourself. When we say "dynamic class", we're talking about that class having the ability to expose some or all of its members to other applications. In order to examine a class, it must be dynamic. So how do we make a class dynamic? We must make the class itself public. Those of you who have experience in this area may already see the problem with this: VB does not allow us to make a class public in a Standard EXE. Well, what to do? The solution is to turn our project into a standalone ActiveX EXE. This gives us the necessary flexibilty to create public classes. Now, if you're only going to examine classes from other applications (such as a separate ActiveX EXE or a DLL), then you don't have to worry about making your project an ActiveX EXE. But if you want to examine classes from within your own project - like we're going to do in the sample code - then ActiveX it is.
Set mobjClass = New clsTest As I'm sure most of you know, the variable name and class name are completely irrelevant. That's the beauty of this method, we don't have to know a thing about the class beforehand. Now that the class has been created, pass it to a TypeLib Information (TLI) function to get the info out of it. Set ClassInfo = TLI.InterfaceInfoFromObject(mobjClass) ClassInfo is an object of type InterfaceInfo. It's defined in the TypeLib reference, of course. TLI is a TLIApplication object, but you don't have to define or create it. It's already done in the reference, so just use it as if you had already dimmed as new. Or simply call the functions without the "TLI." at all. In the sample project I use both methods.
Set FilteredMembers = ClassInfo.Members.GetFilteredMembers FilteredMembers is a SearchResults object. SearchResults objects contain less information than Members, but they are useful for getting a subset of the entire Members collection. There are other ways to use them but they're beyond the scope of this tutorial. Check the further information section at the bottom.
Function GetMemberByID(objMembers As Members, memID As Long) As MemberInfo Dim i As Long For i = 1 To objMembers.Count If objMembers(i).MemberId = memID Then Set GetMemberByID = objMembers(i) Exit For End If Next i End Function Calling this function is very simple. Set ClassMember = GetMemberByID(ClassInfo.Members, FilteredMembers(i).MemberID) Notice the (i) on FilteredMembers. That's there because the best way to use this method is within a loop. As you're going through the FilteredResults, retrieve the unfiltered member for each SearchItem (the objects within a SearchResults collection).
FilteredMember.MemberID - The numerical ID of the member. This is useful to have, because it's faster to call a member by ID than by Name. Identical to ClassMember.MemberID. FilteredMember.InvokeKinds - Contains flags that let you know what type of member this is. More useful than ClassMember.InvokeKind because it combines all entries into one flag. More detail below. ClassMember.Name - See FilteredMember.Name ClassMember.MemberID - See FilteredMember.MemberID ClassMember.InvokeKind - The type of member (method/property let/property get/property set/etc). ClassMember.ReturnType - Object containing information about the return type of the function or property. ClassMember.Parameters - Collection of ParameterInfo objects, with information on parameters required to call a function/sub or property get. ClassMember.Value - Variant containing the current value of a property.
INVOKE_FUNC INVOKE_PROPERTYGET INVOKE_PROPERTYLET INVOKE_PROPERTYSET You can probably guess what they stand for. In a Members collection, there will be a separate entry for each InvokeKind of a member. For example, a property defined in the class like so: Public Xpos As Long will have two seperate MemberInfo objects in the Members collection. Both will have a .Name property of "Xpos", but one will have an InvokeKind of INVOKE_PROPERTYGET and one will have INVOKE_PROPERTYLET. This is because that property can be both retrieved and assigned (read/write) and it is not an object. Therefore, to save us some tricky looping and keeping track of things, it's easier to use the InvokeKinds property (note the "s") of the SearchItem object. We'll already have the object anyway, since we're going through the filtered list, so no extra coding needed. The GetFilteredMembers property combines all same-named entries in a Members collection into one object. As a result, the InvokeKinds property contains all combined InvokeKind properties. Here's an example of how we can use it. If FilteredMember.InvokeKinds And INVOKE_PROPERTYLET Then 'member is a variable property ElseIf FilteredMember.InvokeKinds And INVOKE_PROPERTYSET Then 'member is an object property ElseIf FilteredMember.InvokeKinds And INVOKE_FUNC Then 'member is a method End If Here's another reason this is handy: when a class has a property that's an object (like a reference to itself, or another class), that property will get three entries in the Members collection. That's right, three - INVOKE_PROPERTYGET, INVOKE_PROPERTYLET, and INVOKE_PROPERTYSET. You're probably wondering why the heck an object would have an INVOKE_PROPERTYLET. Well, I have no idea. Perhaps it's for accessing default properties, I don't know. The point is, once we've gotten the filtered members, that INVOKE_PROPERTYLET magically disappears, and that makes differentiating between variables and objects that much easier.
[edit] Part 2: Manipulating a Dynamic ClassUsing the Invoke set of functions included in the TypeLib Information library, we can perform all standard operations on a class using the string name or numeric ID of the classes members. By standard operations I mean calling a method and setting or retrieving the value of a property. Obviously if we code the name of the method or property directly in VB, these operations are a no-brainer. However, when creating a scripting language, we need an easy way to implement them using a string variable containing the name. Now, I know that some of you are thinking, "what do we need these for? We have CallByName!"
InvokeHook InvokeHookArray InvokeHookSub InvokeHookArraySub InvokeID Let's do a quick rundown of what they can do. All of the functions beginning with InvokeHook are used to call a method or property of a class using the name or id. The two with the word Sub in them are identical to their non-Sub cousins, except that they don't return a value. InvokeHook is very similar to CallByName. Here are the parameters: Function InvokeHook(Object As Object, NameOrID, InvokeKind As InvokeKinds, ParamArray ReverseArgList() As Variant) In use, it looks like this: hr = InvokeHook(mobjMyClassObject, strName, INVOKE_FUNC, 4, "blah", lngVariable) InvokeHook, like CallByName, requires that you know the number of parameters at design time, because you code them directly into the function call. The main difference in usage between InvokeHook and CallByName is that the ParamArray must be listed in reverse order. For example, the above call would look like this with CallByName: hr = CallByName(mobjMyClassObject, strName, vbMethod, lngVariable, "blah", 4) As mentioned above, most of the time having to code the parameters ahead of time in a ParamArray is very inconvenient. Unless you are going to have every function in your scripting language have the exact same number of parameters, you're not going to be able to use CallByName or InvokeHook. This is where InvokeHookArray comes in. It's usage is identical to InvokeHook, but instead of a ParamArray at the end, it takes an array of Variants. Function InvokeHookArray(Object As Object, NameOrID, InvokeKind As InvokeKinds, ReverseArgList() As Variant) Like InvokeHook, the arguments must be passed in reverse order, but this is a minor inconvenience. The above function call would be done this way with InvokeHookArray: varParams(2) = lngVariable varParams(1) = "blah" varParams(0) = 4 hr = InvokeHookArray(mobjMyClassObject, strName, INVOKE_FUNC, varParams) Note: When using InvokeHookArray, you lose the ability to pass arguments ByRef. See the sources in the further information section for a workaround to this.
Function InvokeID(Object As Object, Name As String) As Long And you use like so: lngID = InvokeID(mobjMyClassObject, strName) So what is this good for? Well as you may recall, I've mentioned a few times being able to call a member by ID instead of by name. Why would you want to do this? For speed and optimization, of course - those things that every game developer loses sleep over. Here's how it works. When you use an InvokeHook function with a string name (or CallByName), the function internally calls InvokeID to get the member ID, then calls the member using that ID. This is no biggie if we're only going to use InvokeHook once. But in a scripting language we have no idea what the scripts are going to want to do. They may form a loop and call the same function 100 times. We have no idea, so we're better off optimizing. All we have to do is run through all the member names that have been parsed from the script, get the member ID using InvokeID, and store that ID for later. Then, when executing the scripts, grab that ID and use it in place of the name.
Inspect Dynamic Objects article by Matthew Curland (PDF-format) Categories: VB | Tutorial |


