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
asDIM point AS Point2D
, without any parameters. In such a case_create
is not called andx
andy
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 type | Meaning |
---|---|
INTEGER | 16 bit signed integer |
LONG | 32 bit signed integer |
QUAD | 64 bit signed integer |
BYTE | 8 bit unsigned integer |
WORD | 16 bit unsigned integer |
DWORD | 32 bit unsigned integer |
SINGLE | 32 bit floating point |
DOUBLE | 64 bit floating point |
EXTENDED | 80 bit floating point |
NUMBER | 80 bit floating point |
STRING | BSTR |
ASCIIZ | Null-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:
ARRAY ASSIGN
, reportedARRAY EXTRACT
, reportedARRAY FILL
, reportedARRAY SCAN
, reportedARRAY SORT
, reportedARRAY SUM
, reportedARRAY SHIFT
, reportedARRAY SHUFFLE
, reportedARRAY UNIQUE
, reportedJOIN$
, reported
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