Usage

The Genero BDL reflection API allows you to implement generic code, to introspect program elements at runtime.

In programming, introspection is the ability to describe elements of a program at runtime. This feature allows you to implement common generic code to manipulate program elements the definition of which is not known at compile time.

Program elements such as variables, user-defined types, anonymous types, function references, methods and interfaces can be inspected by the reflection API.

The reflection API can also modify the value of program variables, and add/remove elements or arrays and dictionaries.

Importing the reflect package

In order to use the reflection API, you need to import the reflect package in your module.

The reflection API is provided as a C Extension module named reflect, containing a set of classes to manipulale program elements.

The reflect module must be imported at the beginning of your .4gl module(s) with:
IMPORT reflect

Exception handling

If needed, the errors thrown by the reflection API must be handled with a TRY / CATCH block:
DEFINE val reflect.Value
DEFINE rec RECORD ... END RECORD
TRY
    LET val = reflect.Value.valueOf( rec )
    ...
CATCH
    DISPLAY "ERROR: Could not use reflect.Value object for var"
    RETURN -1
END TRY
Important:

The exceptions thrown by the reflect.* API can only be caught with a TRY/CATCH block: If WHENEVER ERROR CONTINUE is active and the reflection API throws an exception, the program stops.

Manipulating variables: reflect.Value

Variables can be inspected and modified with the reflect.Value class.

reflect.Value objects must first be created with the reflect.Value class methods copyOf() or valueOf():
  • reflect.Value.copyOf( expression ) creates a reflect.Value object that references a copy of the expression passed as parameter. This is typically used to create a value from a literal, to be assigned to a reflect.Value object with the set() method.
  • reflect.Value.valueOf( variable ) creates a reflect.Value object that references the variable passed as parameter. This is used to describe and/or modify the underlying variable.

Once you have a reflect.Value object, you can get its corresponding type object with the getType() method, to inspect detailed information about the variable type.

Some reflect.Value methods are specific to a given type kind of the value object. It is for example only possible to call the appendArrayElement() method with a value object of the kind "ARRAY".

For a complete list of methods, see reflect.Value methods.

Inspecting types: reflect.Type

Types can be inspected with the reflect.Type class.

reflect.Type objects must first be created with the reflect.Type.typeOf() class method or from a reflect.Value object with the getType() method:
  1. reflect.Type.typeOf() can create a reflect.Type object directly from the expression or variable passed as parameter.
  2. reflect.Value.getType() returns the reflect.Type object representing the type of a existing reflect.Value object.

Each type belongs to a type kind. Some methods of the reflect.Type class apply to certain kind of types, not to all kind of types.

For a complete list of methods, see reflect.Type methods.

Inspecting methods: reflect.Method

Methods can be inspected with the reflect.Method class.

reflect.Method objects must first be created from a reflect.Type object, with the getMethod() method. The type object must represent a record-type with methods, or an interface.

For a complete list of methods, see The reflect.Method class.

From specific to generic code

The key to write generic code is that the "specific" language elements must be passed over to the code using the reflection API. For program variables, this is done with the reflect.Value.valueOf() class method.

To write a library with the reflection API, you can for example implement a function or method that takes a reflect.Value object as parameter, that will be registered locally. The "specific" code using the library can then pass its variables over for processing.

In the next example, the setVariable() method registers the variable to be processed by the find() method, of the t_find utility type:
IMPORT reflect
  
PUBLIC TYPE t_find RECORD
         val reflect.Value
     END RECORD

PUBLIC FUNCTION (self t_find) setVariable( v reflect.Value ) RETURNS ()
    IF v.getType().getKind() != "RECORD" THEN
        DISPLAY "ERROR: Can only process records"
        EXIT PROGRAM 1
    END IF
    LET self.val = v
END FUNCTION

PUBLIC FUNCTION (self t_find) find( pattern STRING ) RETURNS (STRING)
    DEFINE m reflect.Value
    DEFINE t reflect.Type
    DEFINE x, fc INTEGER
    LET t = self.val.getType()
    LET fc = t.getFieldCount()
    FOR x=1 TO fc
        LET m = self.val.getField(x)
        IF m.toString() LIKE pattern THEN
            RETURN t.getFieldName(x)
        END IF
    END FOR
    RETURN NULL
END FUNCTION
The main program:
IMPORT reflect
IMPORT FGL myutils
MAIN
    DEFINE r_cust RECORD
               pkey INTEGER,
               name VARCHAR(50),
               addr VARCHAR(200)
           END RECORD

    DEFINE f myutils.t_find
    
    LET r_cust.pkey = 111
    LET r_cust.name = "Mike Rean"
    LET r_cust.addr = "145 Sunset St."

    CALL f.setVariable( reflect.Value.valueOf(r_cust) )
    
    DISPLAY "Search 1: ", f.find( "xxx" )
    DISPLAY "Search 2: ", f.find( "Mike%" )
    DISPLAY "Search 3: ", f.find( "%Sunset%" )
    
END MAIN

Manipulating TEXT/BYTE data

TEXT and BYTE variable have methods to manipulate the LOB data, such as readFile().

To write generic code for TEXT/BYTE variables, use the reflect.Type.isAssignableFrom() method, to check if the corresponding reflect.Value object can be assigned to a TEXT or BYTE variable, assign to a local TEXT or BYTE variable with reflect.Value.assignToVariable(), then call required methods with that local variable:

IMPORT reflect
  
FUNCTION loadLobFromFile(v reflect.Value, fn STRING) RETURNS ()
    DEFINE tx TEXT, bt BYTE
    DEFINE t reflect.Type
    LET t = v.getType()
    CASE
    WHEN t.isAssignableFrom(reflect.Value.valueOf(tx).getType())
       CALL v.assignToVariable(tx)
       CALL tx.readFile(fn)
    WHEN t.isAssignableFrom(reflect.Value.valueOf(bt).getType())
       CALL v.assignToVariable(bt)
       CALL bt.readFile(fn)
    OTHERWISE
       DISPLAY "ERROR: Invalid reflect.Value object"
       EXIT PROGRAM 1
    END CASE
END FUNCTION

FUNCTION main()
    DEFINE tx TEXT
    DEFINE v_tx reflect.Value
    LOCATE tx IN MEMORY
    LET v_tx = reflect.Value.valueOf(tx)
    CALL loadLobFromFile(v_tx,"data.txt")
    DISPLAY "tx = ", tx
END FUNCTION

Manipulating DYNAMIC ARRAY elements

To append a new element to a DYNAMIC ARRAY, first append the element, then get the value object and set its value:
MPORT reflect

MAIN
    DEFINE a DYNAMIC ARRAY OF STRING
    DEFINE va, ve reflect.Value
    LET va = reflect.Value.valueOf(a)
    CALL va.appendArrayElement()
    LET ve = va.getArrayElement(1)
    CALL ve.set(reflect.Value.copyOf("new-value"))
    DISPLAY va.getArrayElement(1).toString()
END MAIN

Manipulating DICTIONARY elements

To append a new element to a DICTIONARY, first get an element with the key (this will automatically create the new element if it does not exist), then set the value of the element:
IMPORT reflect

MAIN
    DEFINE d DICTIONARY OF STRING
    DEFINE v reflect.Value
    LET v = reflect.Value.valueOf(d)
    CALL v.getDictionaryElement("key1").set(reflect.Value.copyOf("new-value"))
    DISPLAY v.getDictionaryElement("key1").toString()
END MAIN