Introduction

ThinBASIC offers various elemental numeric and string data types. These are very useful in number crunching and text manipulation.

Once you start describing real-world objects or more complex math problems, you may feel the need to somehow group the data together into more complex structures.

These structures are called user defined types (UDT) in thinBasic.

Syntax

ThinBASIC uses type / end type block to denote user defined type definition.

TYPE nameOfType [alignmentModifier] [EXTENDS baseType]
  element
  [otherElements]
  [dataHandlingfunctions]
END TYPE

Example

The definition of type looks complicated, but keep in mind only two parameters are mandatory:

  • name of type
  • at least one element

To give a specific example, if you want to represent a point in 2D, you could design the following UDT:

TYPE Point2D
  x AS SINGLE
  y AS SINGLE  
END TYPE

The name of the type is Point2D and it has two elements - x and y, each of SINGLE basic data type.

You can create a variable of user defined type the same way as you do with conventional data types:

DIM point AS Point2D

In order to write or read the data, you can use the dot notation:

point.x = 1
point.y = 2

msgBox 0, strFormat$("{1}, {2}", point.x, point.y) 

Arrays of user defined types are supported as well.

Elements

As the previous chapter mentioned, you need to specify at least one element for user defined type.

There are multiple types of elements available:

Simple elements

elementName AS elementType 

Element definition has two mandatory parts:

  • elementName, following the same naming rules as any other variable
  • elementType, which can be any numeric, string or user defined type

An example of UDT with simple elements follows:

TYPE Point2D
  x AS SINGLE
  y AS SINGLE  
END TYPE

Each element is specified on a new line.

Fixed array elements

An element can be also an array, in such a case you specify its subscripts in the brackets after the elementName:

TYPE Triangle
  point(3) AS Point2D
END TYPE

DIM t AS Triangle

t.point(1).x = 0 : t.point(1).y = 0
t.point(2).x = 2 : t.point(2).y = 0
t.point(3).x = 1 : t.point(3).y = 1

Element arrays defined this way can have up to 3 dimensions, with the number of items in each dimension separated by comma.

TYPE ConsoleScreen
  character(80, 25) AS STRING * 1
END TYPE

DIM cs AS ConsoleScreen

cs.character(1, 1) = "a"

Dynamic array elements

There is one single exception when UDT array does not have to be dimensioned at all. We call such arrays dynamic and their usage is strictly restricted to use in conjunction with UDT functions, which can (re)dimension them before first use.

TYPE Person
  firstName      AS STRING
  secondName     AS STRING

  luckyNumbers() AS LONG

  ...

END TYPE

Dynamic array element has then just empty brackets specified, to distinguish it from a simple element.

Limitation: You cannot create dynamic arrays of UDTs at the moment, only elemental numeric and string types can be used

Static elements

An element can be optionally marked as static. This means, that this field is shared between all the variables of a given type.

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
  
  STATIC description AS STRING
END TYPE

DIM a AS Point2D
a.x = 1 : a.y = 2
a.description = "simple 2D point"
msgBox strformat$("[{1}, {2}] is {3}", a.x, a.y, a.description)

DIM b AS Point2D
b.x = 3 : b.y = 4

msgBox strformat$("[{1}, {2}] is {3}", b.x, b.y, b.description)

Once you run this example, you can see that values of x and y are unique for each a, b variables.

However, the description of a has been promoted to b due to the shared, static nature of the element.

This connection works even in the other direction - if you change the static element in b now, it will be immediately reflected in a.

STATIC elements are the only type of element, which can be initialized in the declaration:

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
  
  STATIC description AS STRING = "simple 2D point"
END TYPE

Non-static elements are always initialized to 0 (for numeric elements) or "" (for string elements).

Memory representation

This chapter covers an advanced technical topic. Continue reading, if you plan to:

  • manipulate UDT variables via pointers
  • pass UDT variables to 3rd party DLLs

Otherwise, feel free to continue to the next chapter.

The order in which are members declared is also reflected in the binary representation of the variable in memory.

Let's demonstrate it on our exemplar UDT:

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
END TYPE

In this case, the element x is placed in memory first, and element y afterwards.

Continue reading to explore how to determine size, element offsets and sizes in memory.

Size

In order to determine the size of UDT in memory, always use dedicated functions.

SizeOf will tell you how much memory is occupied by UDT. You can use it both with UDT or UDT variable:

msgBox SizeOf(Point2D)

DIM point AS Point2D

msgBox SizeOf(point)

Both of these will return 8 in this case, because UDT has two single precision members, each occupying 4 bytes.

How can you get this detailed information for members? Use SizeOf directly on them:

msgBox strFormat$("Size of .x is: {1} bytes, .y {2} bytes",
                  SizeOf(point.x),
                  SizeOf(point.y))

Note: dynamic STRING always occupies 4 bytes in UDT, because it is internally represented via a pointer.

Element offset

The members are stored in the memory in the order they are defined.

This is very important because it means that swapping lines of member definition will completely change the memory representation.

In order to find element byte offset, please use the dedicated UDT_ElementOffset function:

msgBox strFormat$("Offset of .x is: {1} bytes, .y {2} bytes",
                  UDT_ElementOffset(point.x),
                  UDT_ElementOffset(point.y))

You can also use relevant UDT_ElementByte in order to find at which byte given element starts.

Element alignment

By default, the elements are tightly aligned in memory, one after each other.

This approach is memory efficient, yet in some cases, you need an alternative:

  • when in need of optimizing UDT element access speed from low-level code
  • for compatibility with another language

You can control the alignment via the alignment modifier. It can take 3 different values:

  • byte, for 1 byte alignment (default)
  • word, for 2 byte alignment
  • dword, for 4 byte alignment

In case you want to elements aligned to 4 byte steps, you might consider using DWORD modifier.

Let's have a look at this example:

TYPE RGBColor
  R AS BYTE
  G AS BYTE
  B AS BYTE  
END TYPE

By default, the size of RGBColor will be 3 bytes, as you can verify with SizeOf:

msgBox SizeOf(RGBColor)

3 elements, each 1 byte in size.

To apply 32bit alignment, use DWORD modifier:

TYPE DwordAlignedRGBColor DWORD
  R AS BYTE
  G AS BYTE
  B AS BYTE  
END TYPE

This means that R, G and B elements will still be of byte data type, however, their element offset will change to 4 byte jumps.

You can verify it easily:

DIM colorNormal  AS RGBColor

msgbox strFormat$("The offset of .R is {1}, .G is {2} and .B is {3}",
                  UDT_ElementOffset(colorNormal.r),
                  UDT_ElementOffset(colorNormal.g),
                  UDT_ElementOffset(colorNormal.b))

DIM colorAligned AS DwordAlignedRGBColor
                  
msgbox strFormat$("The offset of .R is {1}, .G is {2} and .B is {3}",
                  UDT_ElementOffset(colorAligned.r),
                  UDT_ElementOffset(colorAligned.g),
                  UDT_ElementOffset(colorAligned.b))

Extending existing UDT

Once designing a new type, you don't have to start from scratch.

Imagine our Point2D type, defined in the previous chapters.

It has two members, x and y.

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
END TYPE

Once you will need to create 3D point, the obvious approach would be to do it this way:

TYPE Point3D
  x AS SINGLE
  y AS SINGLE
  z AS SINGLE
END TYPE

The advantage of this copy-paste approach is that it is straightforward, but once you would add something to Point2D, it would need to be promoted to Point3D manually.

Let's have a look at two different approaches creating new type based on an existing one.

Extension

ThinBASIC offers the extends keyword to create new UDT by extending existing UDT.

In our case, creating Point3D would be as straightforward as:

TYPE Point3D EXTENDS Point2D
  z AS SINGLE
END TYPE

Now, the Point3D will have new z element, while the x and y will be inherited from Point2D.

Note: The elements of base UDT will be placed as first in the newly created UDT

Note: In case you would add a new element to base UDT, it will get promoted to UDT which extends it.

Note: In case you would reorder the elements in base UDT, they will get reordered in UDT which extends it.

Inclusion

Sometimes you wish to re-use the elements of an existing type, but you need them at a specific location.

Using the inclusion mechanism, you can specify where will the element be included.

In our case, creating Point3D would be as done as:

TYPE Point3D
  Point2D
  z AS SINGLE
END TYPE

In this case, x and y will be placed before z in the UDT memory.

Should you need to have them after z, just put the included type in different place:

TYPE Point3D
  z AS SINGLE
  Point2D  
END TYPE

Now z goes first, and x and y follow.

Adding functions to UDT

You can enhance the UDT with functions as well - this approach is appropriate anytime you need to perform the data manipulation of the UDT itself.

Imagine our Point2D example - you will most probably need to initialize x and y straight away, and you will most probably need to be able to get string representation of the variable.

Let's use this example to demonstrate the use of UDT functions.

The first fact you should know is that UDT functions do not take the UDT variable as input explicitly.

The data is always reachable via pre-defined ME variable, accessible in each UDT function.

There are two kinds of UDT functions:

  • user defined functions
  • functions with special meaning, currently _create and _destroy

User defined function within TYPE definition

You can define a function directly inside the type / end type block.

The same rules apply as for any other function, except the advantage of having ME as a way to reference UDT elements.

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
  
  FUNCTION setXY(x AS SINGLE, y AS SINGLE)
    ME.x = x
    ME.y = y
  END FUNCTION
  
  FUNCTION toString() AS STRING
    RETURN strFormat$("[{1}, {2}]", ME.x, ME.y)
  END FUNCTION
END TYPE

DIM point AS Point2D

point.setXY(1, 2)
msgBox point.toString()

The main advantage of this type of definition is that the type / end type contains everything the UDT can do.

User defined function outside TYPE definition

You can define the function body outside the type / end type block as well.

In such a case, please register the function via <functionName> AS FUNCTION within the type / end type first. This registration does not increase the memory footprint of the UDT, but it creates a logical link between UDT and function.

UDT function defined outside the type / end type block follows the same rules as any other function, except:

  • having ME as a way to reference UDT elements
  • need to prefix the function name with name of type and dot
TYPE Point2D
  x AS SINGLE
  y AS SINGLE

  setXY    AS FUNCTION
  toString AS FUNCTION  
END TYPE

FUNCTION Point2D.setXY(x AS SINGLE, y AS SINGLE)
  ME.x = x
  ME.y = y
END FUNCTION

FUNCTION Point2D.toString() AS STRING
  RETURN strFormat$("[{1}, {2}]", ME.x, ME.y)
END FUNCTION

DIM point AS Point2D

point.setXY(1, 2)
msgBox point.toString()

The main advantage of this type of definition is that you can spread the function across multiple source code files.

Functions with special purpose

ThinBASIC reserves the functions starting with underscore for special purpose.

There are currently two such functions provided:

_create

The optional _create function can be used for variable initialization.

The behaviour slightly differs depending on whether the function has parameters or not.

Note: This function cannot be called explicitly by name, it is used automatically.

Without parameters

The simplest form of _create does not take any parameters.

Such function is called automatically in case of:

  • declaration of a scalar variable of a given type

The typical use of _create without parameters is an assignment of the default element values:

TYPE PacMan
    R AS BYTE
    G AS BYTE
    B AS BYTE

    ' As we all now, PacMan is yellow by default :P
    FUNCTION _create()
        me.R = 255
        me.G = 255
        me.B = 0
    END FUNCTION
END TYPE

DIM player1 AS PacMan   ' player1 is yellow by default

DIM player2 AS PacMan   ' player2 is created yellow by default
player2.R = 0           ' ...but changed to green thanks to override below
player2.G = 255
player2.B = 0

Note: Should you create an array of given type, _create is not called at the moment.

With parameters

The advanced form of _create allows us advanced initialization.

For our case of Point2D it allows us to set x and y during the variable creation.

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
  
  FUNCTION _create(x AS SINGLE, y AS SINGLE)
    ME.x = x
    ME.y = y
  END FUNCTION
END TYPE

DIM point AS Point2D(1, 2)

Now x contains 1 and y is equal to 2.

As you can see, the _create function is not explicitly called, but any parameters passed to your UDT upon creation are passed to _create automatically.

Without _create, you would need to initialize the variable the hard way:

DIM point AS Point2D
point.x = 1
point.y = 2

Note: It is still possible to declare Point2D as DIM point AS Point2D, without any parameters. In such a case _create is not called and x and y will both have a value of zero.

_destroy

The optional _destroy function has two basic properties:

  • is automatically called once the variable goes out of scope
  • it does not take any parameters

Defining it in the context of Point2D has no meaning, but we can use it to illustrate how it works:

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
  
  FUNCTION _create(x AS SINGLE, y AS SINGLE)
    ME.x = x
    ME.y = y
    msgBox strFormat$("Point2D Variable is being created with x = {1}, y = {2}", ME.x, ME.y)
  END FUNCTION
  
  FUNCTION _destroy()
    msgBox "Point2D Variable is being released"
  END FUNCTION  
END TYPE

msgBox "Hello, I am about to call MyFunction"

MyFunction()

msgBox "Hello, I finished calling MyFunction"

FUNCTION MyFunction()
  DIM p AS Point2D(1, 2)
END FUNCTION

Passing UDT to function

UDT variables can be passed to any function - by value or by reference.

As the UDT's represent a group of multiple other types, their memory footprint can be larger.

As with all other variables, byRef is faster, while byVal ensures the value does not get altered.

General rules of thumb:

  • if changing the UDT variable from function is wanted or no issue, always use byRef
  • if you know you will not change the value in function, always use byRef
  • in all the other cases use byVal, it will ensure the UDT variable value will not change

Little example to illustrate the difference.

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
END TYPE

DIM point AS Point2D

Here we have our Point2D example again.

Imagine you want to change the element values via function. You can do it like:

Point2DSetXY(point, 1, 2)

SUB Point2DSetXY(byRef point AS Point2D, x AS SINGLE, y AS SINGLE)
  point.x = x
  point.y = y
END SUB

ByRef is wanted here, this function is explicitly designed to change the element values.

Another example:

msgBox Point2DAsString(point)

FUNCTION Point2DAsString(byRef point AS Point2D) AS STRING
  RETURN strFormat$("[{1}, {2}]", point.x, point.y)
END FUNCTION

ByRef is safe to use here, because we use it just to make the function call faster. The value of Point2D is not altered here, just read.

And last, but not least:

SINGLE halfSum = Point2DGetSumOfHalfs(point)

FUNCTION Point2DGetSumOfHalfs(byVal point AS Point2D) AS SINGLE
  point.x /= 2
  point.y /= 2
  
  RETURN point.x + point.y
END FUNCTION

ByVal is needed, as you need to modify the variable for purpose of the calculation, but do not want to change the original variable.

Appendix

Additional information on UDT.

UDTs and other languages

It is possible to use your UDTs with other languages, but you need to be careful.

In vast majority of cases, you will need to duplicate the UDT definition in given language.

UDT memory representation always contains just the elements, and that is what you can pass to 3rd party DLL.

Note: thinBASIC specifics, such as UDT FUNCTIONs or default values cannot be transfered.

Comparative data types

ThinBASIC uses BASIC convention for data type naming, which is incompatible with most non-BASIC languages.

The following table will help you with matching the correct thinBASIC type with the correct type in the targetted language:

Data typeMeaning
INTEGER16 bit signed integer
LONG32 bit signed integer
QUAD64 bit signed integer
BYTE8 bit unsigned integer
WORD16 bit unsigned integer
DWORD32 bit unsigned integer
SINGLE32 bit floating point
DOUBLE64 bit floating point
EXTENDED80 bit floating point
NUMBER80 bit floating point
STRINGBSTR
ASCIIZNull-terminated buffer of characters

With this knowledge, you may translate thinBASIC UDT:

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
END TYPE

...as the following in C:

struct Point2D {
  float x;
  float y;
};

...as the following in Rust:


#![allow(unused)]
fn main() {
struct Point2D {
  pub x: f32,
  pub y: f32,
}
}

Impact of inheritance

You need to keep in mind that many low level languages do not offer UDT inheritance equivalent.

In such case, you need to "flatten" the UDT to its final form:

With this knowledge, you may translate thinBASIC Point3D this way:

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
END TYPE

TYPE Point3D EXTENDS Point2D
  z AS SINGLE
END TYPE

...as the following in C:

struct Point3D {
  float x;
  float y;
  float z;
};

...as the following in Rust:


#![allow(unused)]
fn main() {
struct Point2D {
  pub x: f32,
  pub y: f32,
  pub z: f32,
}
}

List of known issues

As thinBASIC formal checking is currently under careful research, it might be useful for you to know about the current limitations of the language, which might otherwise confuse you.

SWAP

This useful command for swapping variable content is silently accepting UDT variable or member as a parameter, but it does not perform any action.

DANGER: - missing functionality silently ignored, reported

There is a workaround, which allows you to achieve the desired effect:

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
END TYPE

DIM a AS Point2D
a.x = 1

DIM b AS Point2D
b.x = 2

MSGBOX 0, "a: x=" + a.x + ", y=" + a.y + $CRLF + 
          "b: x=" + b.x + ", y=" + b.y, %MB_OK, "Before swap"

' SWAP a, b ' DOES NOT work at the moment
MEMORY_SWAP(varptr(a), varptr(b), sizeof(Point2D))

MSGBOX 0, "a: x=" + a.x + ", y=" + a.y + $CRLF + 
          "b: x=" + b.x + ", y=" + b.y, %MB_OK, "After swap"

Also, SWAP will trigger a confusing message when used on UDT elements directly, reported

Again, there is a workaround to get around this limitation:

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
END TYPE

DIM a AS Point2D
a.x = 1
a.y = 100

DIM b AS Point2D
b.x = 2
b.y = 200

MSGBOX 0, "a: x=" + a.x + ", y=" + a.y + $CRLF + 
          "b: x=" + b.x + ", y=" + b.y, %MB_OK, "Before swap"

'SWAP a.y, b.y ' DOES NOT work at the moment
MEMORY_SWAP(varptr(a.y), varptr(b.y), sizeof(a.y))

MSGBOX 0, "a: x=" + a.x + ", y=" + a.y + $CRLF + 
          "b: x=" + b.x + ", y=" + b.y, %MB_OK, "After swap"

Direct assignment

While this is not an issue, it is good to know about how this works.

When you perform the assignment of one UDT variable to another UDT variable, a copy of data is performed.

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
END TYPE

dim a as Point2D
a.x = 1

dim b as Point2D
b.x = 2

a = b

MSGBOX 0, a.x
MSGBOX 0, b.x

Please note thinBASIC currently allows you to assign two totally different types, as it performs a memory copy.

TYPE Point2D
  x AS SINGLE
  y AS SINGLE
END TYPE

TYPE Point3D
  x AS SINGLE
  y AS SINGLE
  z AS SINGLE
END TYPE

dim a as Point2D
a.x = 1

dim b as Point3D
b.x = 2

a = b

MSGBOX 0, a.x
MSGBOX 0, b.x

DANGER: - allows you to miss the accidental assignment of incompatible data types.

Arrays in UDT

The following functions do not work with UDT arrays yet:

You can workaround these limitations by overlaying virtual array over the UDT array.

Example:

TYPE MyType
  myArray() as int32
  
  function _Create()
    redim me.myArray(3)
    me.myArray(1) = 1, 2, 3
    
    ' This overlays local array over ME array,
    ' making ME.myArray data available for read/write
    int32 virtualMyArray(3) at varptr(me.myArray(1))
    
    int32 index = array scan virtualMyArray(), = 3
    
    msgbox 0, index
    
  end function
END TYPE

dim v as MyType
  • COUNTOF does not work, reported
    • you can workaround by using UBOUND instead

Multidimensional dynamic arrays in UDT

While static multidimensional arrays in UDT are fully supported with up to 3 dimensions, the dynamic UDT arrays currently work only with a single dimension.

If you dimension dynamic UDT array with more than 1 dimension, it will silently continue, but the indexing will not work correctly.

DANGER: - missing functionality silently ignored, reported

You can workaround this limitation by overlaying virtual array over the UDT array.

Example:

TYPE MyType
  myArray() as int32
  
  function _Create()
    redim me.myArray(3 * 2)                             ' -- Linear dimensioning
    
    int32 virtualMyArray(3, 2) at varptr(me.myArray(1)) ' -- Multidimensioning
    
    virtualMyArray(3, 1) = 1    ' Performs write to UDT data thanks to overlay
    virtualMyArray(1, 3) = 2    ' Performs write to UDT data thanks to overlay
    
    msgbox 0, virtualMyArray(3, 1)
    msgbox 0, virtualMyArray(1, 3)    
    
  end function
END TYPE

dim v as MyType