Contents > Language Reference

Language Reference

Basics Overview of the language.
Variables Places for storing data.
Functions Breaking a program into smaller pieces.
Expressions two = 1 + 1.
Constants and Enumerations Giving names to numbers.
Statements Making decisions and controlling program flow.
Pointers Addresses of variables.
Structures Aggregating data.
Objects Adding intelligence to structures.
Inheritance Specializing and extending objects.
Interfaces Defining an object contract.
Function Pointers Calling functions indirectly.
Event handlers Writing event handlers for forms and controls.
Creating and Using Gadgets Creating and using gadgets.
Debug Support Debugging-oriented langauge features.
Compiler Directives #define, #if, and related compiler directives.

Contents > Language Reference > Basics

Basics

The OrbC language is a simple language based on the style of C and C++. It is a procedural language with a limited implementation of objects. The language has a lot of useful features, but becomes extremely powerful when combined with the event handlers and library provided by the runtime.

A Basic Application

As a quick introduction to the language, lets dissect a very simple application. Let's assume you've created an application named "myApp". "myApp" has a main form named "myForm" which contains a text field named "nameField", and a button named "alertButton". This simple app could look something like this:

@app myApp {
  creator = "MyAp";
  name = "App Name";
  dbname = "AppName-MyAp";
}

@form myForm {
  id = 1000
  text = "My Form"
  x = 0, y = 0, w = 160, h = 160

  field nameField {
    id = 1001
    x = 10, y = 60, w = 140, h = 12
  }
  
  button alertButton {
    id = 1002
    x = 60, y = 80, w = 40, h = 12
    text = "My Button"
  }
}

// a global variable to hold the salutation.
string text;

// the handler for when the app is started
handler myApp.onstart() {
  // load the main form
  myForm.load();
}

// a function to build the text for the alert
string buildText(string name) {
  return "Hello, " + name;
}

// the handler for the button, which displays the alert
handler alertButton.onselect() {
  text = buildText(nameField.text);
  alert(text);
}

As you can see, there is no main function such as you would see in a C/C++ application. There is no single entry into the application. Instead, all the code in your application is called by the runtime through handlers. When an application is started, the first handler to be called is the onstart handler of you application. Once the handler completes, control is returned to the OrbC runtime. The runtime waits for a UI event to occur and calls into the appropriate handler in your application if one is defined (such as the alertButton.onselect handler above).

Keywords

The keywords are described through the rest of the documentation. As a reference, they are: if, else, for, while, do, return, break, continue, switch, case, default, string, int, float, char, bool, void, object, struct, sizeof, typeof, new, delete, handler, const, enum, include, true, false, null, funcptr, interface, virtual, library, debug, debuglog, assert.

The following are reserved words, but are not yet used by the language. You may not use these words as names in your application: static, tassert.

Comments

A comment is text in a source file that the compiler ignores. Comments are used by the developer to explain what the code does in plain English (or any other languge you prefer). There are two types of comments: single-line and multi-line. A single line comment begins with two slashes (//) - everything following the slashes on the current line is a comment. A multi-line comment starts with /* and ends with */. Multi-line comments cannot be nested.

// this is a comment

/* this is a
multi-line comment */
/* it is /* not valid */ to nest comments*

Contents > Language Reference > Variables

Variables

Variables are the items in a program that store data. There are five basic types of variables in the OrbC language:

Type Name Example
integer (32-bit, signed) int 1, 2, 5, -789, 452349
floating point (32-bit) float -1.2, 3.141592, 5.7e-4
characters (8-bit, signed) char 'a', 'b', '#', '4', 56
strings string "Bob" "Megan" "Hello"
boolean bool true, false

Each variable has a name and is of a pre-determined type. Variables are declared like this:
variable-type name[, name2...];

Here are a few examples:

int myInteger, row, column; // defines three integer variables named "myInteger", "row", and "column"
string name;
float pi;
char c, last, first;

Variables must have names conforming to a few rules: 1. the name must be 31 characters or less. 2. the name may only contain letters, numbers, and the underscore character (_) 3. the name must not begin with a number. A variable should not have the same name as a method - although it is legal to do so, this will prevent the application from being able to call the function from any location where the variable is visible.

It is also possible to have an array of values. An array is a list of values that are stored in one variable. Arrays are declared like normal variables except that the variable name is followed by '[size]' where size is the number of items that the variable can hold. A declaration might look like this:

int values[10];
string names[7];

Of course, arrays and normal variables can be declared together:

int row, values[10], column;
string name, colors[8];

You can also give default values to the variables, however these initial values must be simple literals:

int nine = 9, eight = 8, zero;
string days[7] = { "Sun", "Mon", "Tues" };

In the case of arrays, the initial values must be in braces and separated by commas. If the number of values in the initializer list is less than the length of the array, then the uninitialized elements have default values. For the days array above, the last 4 members of days are the empty string ("").


Contents > Language Reference > Functions

Functions

A function is a group of statements which is the basic unit of a program. A function has a return type, may have parameters, and may have local variables. The return type is the type (such as int) of the value the function returns - when used in an expression, the body of the function is executed and its return value is used as part of the expression. (If a function does not return a value, its type is void, and the function cannot be used in an expression.)
return-type func-name
([param-type param-name,...]) { local-variables statements }

Function names follow the same rules as variable names - 1. the name must be 31 characters or less. 2. the name may only contain letters, numbers, and the underscore character (_) 3. the name must not begin with a number. All function names must be unique, and you may not declare a function with the same name as one of the built-in functions.

Statements are discussed later, but for now, here are a few examples:

int area(int width, int height) {
   return width * height;
}

float square(float x) {
   return x * x;
}

int five() {
   return 5;
}

void sayHello() {
   alert("Hello");
}

Local variables are variables that can only be seen inside a function. In fact, they only exist while the function is executing. In other words, function A cannot access the variable inside of function B, and vice versa. When a function returns, the memory used by the local variables is cleaned up, and they are no longer accessible. This leads to the topic of variable scope. All variables have a scope - either global, local, or struct/object member. You can define a local variable (or parameter) with the same name as a global variable - when using the variable name inside the function, the local variable will be accessed/modified, but the global variable will be inaccessible.

Parameters are local variables whose value is provided by the calling function. In the function above, area, there are two parameters - width and height. width and height are local variables, but there value is initialized elsewhere. To invoke area with a width of 5 and a height of 4, you would type:

area(5, 4);

Parameters (except struct and object parameters) are passed by value, which means that if you pass a variable into a function as a parameter, the function is not able to modify the original variables. For example:

void six(int x) {
  x = 6; // only sets the local variable
}

void test() {
  int y = 9;
  six(y);
  // y is still 9
}

Structs and objects, however, are passed by reference. We'll discuss these later.


Contents > Language Reference > Expressions

Expressions

An expression is something in OrbC that evaluates to a value. An expression can be a literal, a variable, a function call (when the function returns a value), or any combination of these connected by operators.

A literal is any value that is directly entered into a source file, such as: 5, 5.4, 'j', "hello".

A value stored in a variable can be accessed by simply typing its name: (e.g. myInteger). However, if the variable is an array, each value in the array must be accessed individually by index. The valid indices for an array with n elements are 0 to n-1. So, an array declared like this:

string names[4];

can be accessed like this:

names[0] = "first name";
names[1] = "second name";
names[2] = "third name";
names[3] = "fourth name";

A function call consists of the name of a function followed by parentheses containing all the parameters for the function. The value of a function call is the value returned by the function.

a = area(4, 5);
square(9.2);
clear();
text(20, 30, "Game Over");

A function can only be called after it is defined. If you want to call a function before defining it, you can use a function prototype. A prototype of a function is a global line (not within another function) which states the return type, name, and parameters of a function followed by a semicolon:

int area(int x, int y);
float square(float); // the use of variable names is optional in a declaration

These three basic elements can be combined with operators:

5 + 7 - area(12, 34);
square(5) * pi;
"Hello, " + "World";

Of course, function calls can have expressions in them as well:

area(x+3, y*9);
area(8 * square(4), 7);

Assignment

Variable assignment is actually just another form of expression. The value of the entire assignment expression is equal to the value being assigned. Assignment is done in one of two ways--for a normal variable:
name = expression

and for an array:
name[index-expression] = expression

Here are a few examples:

int myInt, numbers[3];
string myString;
...
myInt = 8;
myString = "Animaniacs";
numbers[0] = myInt + 5;
numbers[2] = numbers[0] * 8;

However, since OrbC is loosely typed, any type of value can be assigned to any type of variable and the value will be automatically converted:

myString = 95; // The value of myString is now "95"
numbers[1] = "78"; // The value of numbers[1] is now 78;
numbers["2"] = "2"; // Another neat trick. numbers[2] is now 2

Operators

The following table gives the list of operators and their associativity in order of precedence, lowest first. In other words, when two operators are in an expression, the operator lowest in the table is evaluated first. However, if part of an expression is enclosed in parens, that part is evaluated first.

Operator

Assoc

Description

=

right assigns the value of the expression on the right to the variable on the left. Evaluates to the expression on the right.

||

left logical 'or', evaluates to 0 if false, 1 if true

&&

left logical 'and'

|

left bitwise 'or'

^

left bitwise 'xor'

&

left bitwise 'and'

== != < <= > >=

left relational operators. == (equal), != (not equal), <= (less than or equal), >= (greater than or equal). These evaluate to 1 if the expression is true, 0 otherwise
<< >> left bitwise shift operators. The operands must be int or char.

+ -

left addition, subtraction (subtraction cannot be used with a string argument)

* / %

left multiplication, division, modulus (cannot be used with strings, nor can modulus be used with floats)

- ! ++ -- ~ * [] () & . -> sizeof() typeof()

left - (negation), ! (logical 'not'), ++ (increment), -- (decrement), ~ (bitwise neg), [] (array subscript, string character accessor), & (address of ), . (the struct accessor), and -> (the struct dereference accessor). sizeof (size of a type/variable), typeof (type string of a type/variable).

Compound Assignment

To save time and space, you can use compound assignment operators. These operators perform a binary operation on the left and right side of the expression, and then assign the value back to the left side. For example:

x += 2;
myArray[i] *= 3;

are equivalent to:

x = x + 2;
myArray[i] = myArray[i] * 3;

Compound assignment operators have the same precedence as the basic assignment operator (=). Available compound operators are +=, -=, *=, /=, %=, ^=, |=, &=, <<=, >>=.

Shortcut Logic

When using the && and || operators, the compiler uses shortcut logic, meaning it only evaluates the sub-expression that it needs to in order to get the correct result. For example:

bool true_func() { return true; }
bool false_func() { return false; }

void test() {
  if (true_func() || false_func()) {
    ...
  }
  if (false_func() && true_func()) {
    ...
  }
}

In the first if statement false_func() would not be called. Since the first half of the || expression is true, the whole expression must be true so the second half is not evaluated. In the same way, in the second if expression true_func() is never executed because the compiler knows that the entire expression is false regardless of the second half.

String Character Accessor

To get or set an individual character within a string variable, use stringVariable[index]. The index of the first character is 0. You will produce a runtime error if you attempt to access a character that is past the end of the string. Example:

string str = "bob";
...
alert(str[1]); // Displays the second letter of str
str[1] = 'X'; // changes str from "bob" to "bXb"

Note: the string character accessor cannot be used with literals, nor can the address of the resulting character be taken. In other words, the following expressions are not valid: &str[i], "hello"[i]

Increment / Decrement

The ++ and -- operators are special in that they must be placed before or after a variable and modify the value of the variable. The ++ increments the value of a variable by one, while the -- decrements by one. The caveat is that if the ++/-- is placed in front of the variable, the expression evaluates to the value of the variable after it is incremented/decremented. If it is placed after the variable, the expression evaluates to the variable's previous value. Example:

int myInt;
...
myInt = 8;
alert(++myInt); // Displays "9"

myInt = 8;
alert(myInt++); // Displays "8", but myInt is now 9

sizeof() and typeof()

sizeof() and typeof() are operators that give information about a type. They can be used either on the name of a variable or the name of a type. sizeof() evaluates to the number of memory values required for the type - which is 1 for the five basic types and for pointers, but becomes more interesting when used on structs and objects. typeof() evaluates to a string describing the memory structure of a type. Some of the built-in functions (such as DBRecord.read) require these type strings to convert OrbC structures into native data to pass to the operating system.

struct Person {
  string name;
  int age;
};

void test() {
  Person bob;
  int size;
  string type;
  size = sizeof(Person); // size = 2
  size = sizeof(bob); // size = 2
  type = typeof(Person); // type = "si";
}

Note: When either of these operators is applied to an array variable, only information about the type is supplied - the size is NOT multiplied by the number of elements in the array. However, if an array is contained in a struct/object, the operators return information about the entire struct/object including the full size of the array.

Automatic Conversion and Casting

Just like in assignments statements, automatic conversion takes place in every part of an expression. If the two arguments to an operator are of different types, one of the arguments will be promoted to the less strict type. The promotion order is char to int to float to string. So in the expression:

"Result is: " + 5;

The literal 5 is first promoted to a string, and the two strings are concatenated to "Result is: 5". This may have some undesirable side effects. For example, if you want to display an expression and result, you might do something like this:

alert("5 + 7 = " + 5 + 7); // Displays "5 + 7 = 57"

This probably wasn't the desired outcome. Instead, you would want the expression evaluated first, then concatenated to the string. The parentheses can be used to accomplish this:

alert("5 + 7 = " + (5 + 7)); // Displays "5 + 7 = 12"

One problem remains. Suppose you want to find the floating point value of a fraction of two integers.

alert("7 / 5 = " + (7 / 5)); // Displays "7 / 5 = 1"

This output is because both arguments are integers, so the result is also an integer. To solve this, we can cast one of them to a float:

alert("7 / 5 = " + ((float)7 / 5)); // Displays " 7 / 5 = 1.4"

This forces the integer 7 to a floating point number before dividing it by 5.


Contents > Language Reference > Constants and Enumerations

Constants and Enumerations

It is often useful to give names to important constants. This makes source code more readable. You can do this by defining a constant using the const keyword, or by creating an enumeration using the enum keyword. A constant is defined the same way a variable is, but begins with the const keyword. A constant can only be created for the five simple types. For example:

const float pi = 3.141592;
const string version = "1.0";

Enumerations are a way of creating a group of related integer contants. Each enumeration name is put in a list, and can be given an explicit value (such a four, below). If an explicit value is not given, the compiler will provide a default value - the value for each name is 1 greater than the previous name, and 0 for the first name. To clarify:

enum { zero, one, two, four = 4, five };

Note: constants and enumerations are treated by the compiler at a low level. Because of this, whenever the compiler sees the name of a constant or enum, it treats it as if it was the defined value. If the programmer attempts to define a variable or function with the same name as a constant/enum, the compiler will give an error saying that an identifier was expected.


Contents > Language Reference > Statements

Statements

Statements are the individual parts that make up the body of a function. The following are the available statements:

Statement Description
return; Returns immediately from the current function. If the function has a return type other than void, the next form of return must be used.
return expr; Returns immediately from the current function, returning the value of the expression expr. This may only be used if the function is declared with a return type other than void.
if (expr) stmt Evaluates the expression expr, if its result is true (non-zero, non-null pointer, or non-empty string), the statement stmt is executed, otherwise stmt is skipped, and execution continues
if (expr) stmtA
else stmtB
Evaluates the expression expr, if its result is true (non-zero, non-null pointer, or non-empty string), the statement stmtA is executed, otherwise stmtB is executed
while (expr) stmt The expression expr is evaluated. If it is true (non-zero, non-null pointer, or non-empty string), stmt is executed. The loop then begin again, evaluating expr and executing stmt until expr is no longer true. This means that stmt will never execute if expr is initially false
do stmt
while (expr)
The same as while except that the statement stmt is executed before expr is evaluated. This guarantees that stmt will executed at least once
for (init;cond;iter)
stmt
The initializer expression init is first evaluated. The condition expression cond is evaluated. If it is true, stmt is executed and the iterator expression iter is evaluated continuing the loop, otherwise the the for loop ends. Note: init is evaluated only once.
break; Immediately exits from the directly enclosing while/do/for loop or switch statement..
continue; Immediately jumps to the beginning of the the directly enclosing while/do/for loop. In a for loop, the iter expression is evaluated, followed by the cond expression and possibly the stmt. In a while loop, the cond expression is evaluated again, and possibly the stmt.
switch (expr)
{ stmts }
Evaluates the expression expr. If stmts contains a case statement with a matching value, the code immediately following the case statement is executed until either a break statement or the end of the switch statement is reached. If no matching case statement is found and a default statement exists in the switch, the code immediately following the default statement is executed until either a break statement or the end of the switch statement is reached. If no case statement matches the expr, and no default statement is present, everything in the switch statement is skipped, and execution continues after the final closing brace. expr must evaluate to an int, char, or string.
case constant:

A marker within a switch statement. The constant must be either a char (case 'a':), an int (case 3:), or a string (case "apple":) If the constant matches the expr in the switch statement, then the code immediately following this marker is run, until a break statement or the end of the switch statement is reached.

default: An optional marker within a switch statement. If none of the cases in the switch statement match the switch expr, the code immediately following this marker is executed, until a break statement or the end of the switch statement is reached.
{ statements } A brace followed by a list of statements, followed by another brace is considered a single statement
expression; An expression followed by a semicolon is also considered to be a statement

Statement Examples

return

Let's visit a previous example function to see how return works.

int five() {
   return 5;
}

Since the return value of the function five is always 5, we can use the function any place we would normally put the literal 5.

alert("Five is " + five()); // Prints "Five is 5"

Also, since return causes the function to exit immediately, we could do this:

int five() {
   return 5;
   alert("This won't display");
}

and we would have the same effect.

if

void lessThan5(int x) {
   if (x < 5)
      alert("Less than five");
   alert("Hello");
}

If this function is called with a number less than 5, "Less than five" will be displayed followed by the word "Hello", otherwise, only the word "Hello" is displayed.

if ... else

void lessThan5(int x) {
   if (x < 5)
      alert("Less than five");
   else
      alert("Greater than or equal to five");
}

If this function is called with a parameter less than 5, "Less than five" is displayed, otherwise "Greater than or equal to five" is displayed.

while

void count() {
   int x;

   x = 5;
   while (x > 0) {
      alert(x);
      x = x - 1;
   }
}

This bit of code will display the numbers from 5 to 1 counting backwards. Notice that braces were placed around the two lines of code in the while loop to make them act as a single statement.

do ... while

void count() {
   int x;

   x = 6;
   do {
      x = x - 1; // could also be x--
      alert(x);
   } while (x > 0);
}

This bit of code (similar to the previous example) will display the numbers from 5 to 0 counting backwards. The zero is displayed in this case because the expression x > 0 is not evaluated until after the loop

for

void output() {
   string list[4];
   int index;

   list[0] = "Zero";
   list[1] = "One";
   list[2] = "Two";
   list[3] = "Three";

   for (index = 0 ; index < 4 ; index++)
      alert(list[index]);
}

This example will display "Zero", "One, "Two", "Three". When we dissect it we see that the array list is initialized first. We then reach the for loop. First, the initializer is evaluated, setting index to 0. Next, the condition is evaluated index < 4, which is true, so the body of the loop executes, displaying "Zero". The iterator expression is then evaluated, increasing index by one. This continues until index is equal to 4, at which point the loop exits without executing the body again.

break

void count() {
   int x;

   x = 5;
   while (x > 0) {
      if (x == 1)
         break;
      alert(x);
      x = x - 1;
   }
}

In this slightly more complex piece of code, the counting goes on as it normally would, displaying "5", "4", "3", 2". However, when x reaches 1, break is executed, breaking out of the while loop early, before the 1 gets displayed.

continue

void count() {
   int x;

   x = 6;
   while (x > 1) {
      x--; // Do the subtraction first
      if (x == 3)
         continue;
      alert(x);
   }
}

In this clearly contrived example, the output is "5421". When x reaches 3, the continue is executed, jumping to the beginning of the loop, skipping over the alert.

switch, case, default

void which_number(int x) {
   switch (x) {
      case 1:
         alert("x == 1\n");
         break;
      case 2:
      case 3:
         alert("x == 2 or x == 3\n");
         break;
      case 8:
         alert("x == 8\n");
      case 10:
         alert("x == 8 or x == 10\n");
         break;
      default:
         alert("x is not 1,2,3,8, or 10\n");
   }
}

The which_number function is passed a value, and will display a fact or two about it. If the value is 1, case 1 is executed and breaks to the end of the switch. If the value is 2 or 3, the code following case 3: is executed. If the value is 8, both "x=8" and "x=8 or x=10" is displayed because there is no break before case 10:, this is called "fall-through". If none of the cases match the the value of x, the code following default: is executed.


Contents > Language Reference > Pointers

Pointers

Note: Pointers are an advanced topic, which should be dealt with after the user is familiar with all the other programming concepts.

All variables are stored at some address in memory. A pointer is a variable which holds the address of another variable - using the pointer the value of the original variable can be indirectly accessed (a process called "dereferencing"). A pointer can be used with only one value type (int, string, struct, etc.) - for example an integer pointer can only be used to access an integer. A pointer is declared in the same way as a normal variable with the addition of an asterisk (*) after the type name.

There are two primary operators which are used with pointers, * and &. The * operator dereferences the pointer. A dereferenced pointer acts just like the data to which it points. The & operator returns the address of a given variable. To illustrate:

int* p, q; // Note: unlike in C/C++, this declares p and q to BOTH be pointers
int i;

void test() {
  i = 5;
  p = &i;    // Assign the address of 'i' to the pointer 'p'
             // now, typing '*p' is the same as typing 'i'
  alert(*p); // Display the value of 'i'
  *p = 7;    // Assign 7 to 'i'
  q = p;     // Assign the value of 'p', which is the address of 'i', to 'q'
             // now, typing '*q' is the also the same as typing 'i'

  // Things not to do
  p = 8;     // BAD! Don't assign a literal value to a pointer
  *i = 9;    // BAD! Don't try to dereference a non-pointer
}

When initially declared, the pointers will have the value null (which is equivalent to 0), which means they point to nothing and cannot be dereferenced.

Pointers and arrays

Pointers and arrays are fairly similar. Pointers can use the [] operator, and an array variable (when not used with []) results in the address of the first element. For example:

int array[5];
int* p;

void test() {
  p = array; // Assign the address of the first element of
             // 'array' to 'p'
  *p = 7;    // Assign 7 to array[0]
  p[1] = 8;  // Assign 8 to array[1]
}

This enables the pointers to arrays to be passed as function parameters. This also allows the user to implement their own version of two-dimensional arrays (multi-dimensional arrays will be implemented in a future version). By creating an array of pointers, each of which is a pointer to an array (or part of one), a two-dimensional array can be simulated.

int array[100];
int* twod[10]; // after init(), this can be treated
               // like at 10x10 matrix

void init() {
  int i;
  for (i=0;i<10;i++)
    twod[i]=array + i*10; // Pointer arithmetic
}

void test() {
  int x, y;
  init();
  for (x=0;x<10;x++)
    for (y=0;y<10;y++)
      twod[x][y]=x * y; // Sets array[x*10 + y] = x*y
}

Pointer arithmetic

Pointer values can used in a limited number of expression. You can add and subtract from a pointer (and, thus, can use the increment and decrement operators as well). When you add 1 to a pointer, the pointer points to the next value in memory. Similarly, when you subtract 1 from a pointer, the pointer points to the previous value in memory. When you add to or subtract from a pointer to a structure/object, the pointer is changed by the number of structures specified, rather than the number of memory values (see example below). Caution should be used when using pointer arithmetic, because dereferencing an invalid memory location will cause an error in the applet.

struct Person {
  string name;
  int age;
};
...
Person people[10];
Person* pPerson;
pPerson = &people[4]; // pPerson now points to the 4th person
pPerson++;            // pPerson now points to the 5th person

Dynamically allocating memory (new and delete)

The OrbC language allows you to dynamically allocate memory at runtime. Two of the most common uses of this are allocating an array whose length cannot be determined before the program is run, and creating structures and objects (discussed soon).

To allocate a new block of memory, use the new keyword. The new keyword takes the name of the type to allocate and returns a pointer to the allocated memory (or null if the allocation fails). For example:

int* pi;
string* ps;
pi = new int;
ps = new string;
*pi = 6;
*ps = "Hello";

The new keyword can also create arrays by specifying the number of desired elements in brackets after the type name:

int count = 10;
int* parray;
string* psarray;
parray = new int[10]; // allocate an array of 10 integers
psarray = new string[count * 2]; // allocates count*2 strings
if (parray != null) { // since dynamic memory may run out,
  parray[3] = 45;     // you should check that new succeeded
}                     // before using the pointer!
if (psarray) { // this is the same as (psarray != null)
  psarray[12] = "twelve";
}

Once the memory is no longer needed, it must be freed using the delete operator. The delete operator takes a pointer to memory previously allocated by new. The pointer could have been allocated either as a single value, or as an array - if the pointer is null, it will safely do nothing.

delete pi; // delete the integer pointed to by pi
delete parray; // deletes the entire array pointed to by parray
delete null; // does nothing

Note: The runtime also provides two methods for dealing with less structured memory allocations: malloct, and free.


Contents > Language Reference > Structures

Structures

A structure is a user defined data type that is a collection of other data types. For example, a Person data type can be defined as a string containing the person's name, and an integer representing the person's age like this:

struct Person {
  string name;
  int age;
};

After being declared, Person can be used like the built-in data types (e.g. int, string). To access the data in a structure, use the dot (.) operator like this:

Person bob; // create a Person variable named 'bob'
bob.name = "Bob"; // assign "Bob" to the name member of 'bob'
bob.age = 26;

When using a pointer to a struct, you must use the -> operator, rather than the . operator, to access the data in the structure, like this:

Person* pBob; // create a Person pointer name 'pBob'
pBob = new Person; // allocate a new Person object, and assign the address to 'pBob'
pBob->name = "Bob"; // set the name member of the Person that 'pBob' points to
pBob->age = 18;

Structs may also contain other structs (or objects) as members. When a struct contains another struct, multiple . operators must be used. For example:

struct City {
  int population;
  Person mayor;
};
City hudson;
...
hudson.mayor.age++; // happy birthday

Structs can be initialized like other data types, but must be enclosed in braces and each member must be initialized in the order in which they were declared, like this:

Person bob = { "Bob", 72 };
City hudson = { 45000, { "Mayor John", 48 } };

Structures can be copied just like other variables, by using the assignment operator (=):

Person bob = { "Bob", 54 }, andy = { "Andy", 32 };
bob = andy; // copies all the data from 'andy' to 'bob'
alert(bob.name); // displays "Andy"

Unlike other variable types, structure and object parameters are passed by reference. When a structure is passed to a function, the entire structure is not copied. Instead, when the structure parameter is accessed, the original structure is accessed, not a local copy. For example:

struct Person {
  string name;
}

void bob(Person person) {
  person.name = "bob"; // this affects the original variable 'andy'
}

void test() {
  Person andy = { "Andy" };
  bob(andy); // pass a reference to 'andy'
  // andy.name is now "Bob"
}

Contents > Language Reference > Objects

Objects

An object is a structure with functions defined that operate on it. These functions are called methods. An object is declared in the same way as a structure, with the addition of method declarations. Method declarations are identical to function declarations, but are inside the object. The following block of code declares a new object type called MyObject:

object MyObject {
  int data;
  int add(int x);
};

An instance of an object is then created in the same way as a struct, by declaring a variable of the new object's type, and the object's members can be accessed like struct members:

MyObject mine; // create a MyObject variable named 'mine'
mine.data = 4; // assign 4 to the data member of 'mine'

Methods

A method is defined like a function, but with the name of the object type before the method name, like this:

int MyObject.add(int x) {
  data += x;
  return data;
}

Within the context of a method, all the members of the object can be accessed as if they are local variables, like data above. However, if a local variable is defined with the same name as an object member, you must use this, below, to access the member. Given the above definition of MyObject.add:

MyObject mine = { 4 }; // initialize mine.data to 4
mine.add(3); // adds 3 to mine.data

Just as with pointers to structures, accessing members and methods of an object through a pointer must be done with the -> operator

MyObject* pMine;
pMine = new MyObject; // allocate a new object
pMine->data = 9;
pMine->add(4); // use the -> to call methods as well

this

Within an object method, this is a special keyword that represents the current object. (Unlike in C++, this is a reference to the object rather than a pointer). this can be used to pass the current object to another function as a parameter, but is not required to access the members or methods of the object.

void doSomething(MyObject myo) { }

int MyObject.add(int x) {
  doSomething(this); // call the doSomething method, using the current object as a parameter
  return data;
}

this may also be used to access an object member that is hidden by a local variable:

int MyObject.add(int x) {
  int data; // local variable
  data = 5; // changes the value of the local variable, not the member
  this.data += x; // adds x to the member 'data'
  return this.data;
}

_init, _destroy, _copy

There are three special methods that can be defined for an object. Calls to these methods are generated automatically by the compiler, and they cannot be called directly by the developer. These methods are optional, and default implementations will be generated by the compiler if they are required and the developer has not defined them.

void _init()

This method is called automatically when an object is being created. When an object is created with the new keyword, this method is called upon successful creation. If the object is a global variable, this method is called before the application starts. If the object is a local variable, this method is called upon entry into the containing function. If the object contains other objects with _init defined, those objects are initialized before the containing object.

void _destroy()

This method is called automatically when an object is being deleted. When an object is deleted with the delete keyword, this method is called upon successful deletion. If the object is a global variable, this method is called after the application exits. If the object is a local variable, this method is called just before the containing function returns. If the object contains other objects with _destroy defined, those objects are destroyed after the containing object.

void _copy(object-type source)

This method is called automatically when an object is being copied - when an object is assigned to another object or when an object is returned from a function. If this method is not defined, the compiler may generate one (if it needs to do so). If the method is defined, the developer is required to manually copy all the important members (inherited members and member objects with _copy() methods are automatically copied by the compiler before the method body begins). When defined, an assignment such as a = b would generate a call which would look like a._copy(b).

Here is an example to demonstrate how these methods work:

object Person {
  _init();
  _destroy();
  _copy(Person);
  string name;
  int age;
};

void test() {
  Person a, b; // allocates stack space, calls a._init() and b._init()
  Person* p;
  
  a.name = "Jon";
  b = a; // generates call to b._copy(a)
  p = new Person; // allocates heap space, calls p->_init()
  *p = a; // generates call to p->_copy(a)
  delete p; // calls p->_destroy(), frees heap space
} // calls b._destroy() and a._destroy()

Objects vs. Structs

A struct and an object are very similar - they both support methods and members. However, an object cannot inherit from a struct and only objects may inherit from other objects and interfaces.


Contents > Language Reference > Inheritance

Inheritance

Object inheritance allows you to define a new object type by extending and specializing an existing object type.

Extending

When you create a derived object from a base object, the derived object inherits all the methods and members of the base object type. A derived type can only have one base type - this is called single inheritance. As in other object-oriented languages, a base class can have its own base class, allowing a deep inheritance tree (e.g. Lion inherits from Cat which inherits from Animal). To declare such a derived object type, you must follow the name of the derived type by a colon and the name of the base type:

// base object
object Tree {
  string name;
  void grow();
};

// derived object
object FruitTree : Tree {
  string fruit;
};

In the example above, the Tree is an object that represents a real-world tree - it has a name (such as "maple") and a method that makes it grow. The FruitTree is a special kind of Tree which also has the name of the fruit (such as "apple"). Because the FruitTree inherits from Tree, it has all the same methods and members as a Tree. Thus, a FruitTree can be used like this:

FruitTree appleTree;
appleTree.name = "Apple tree"; // name is inherited from Tree
appleTree.fruit = "apple";
appleTree.grow(); // grow() is also inherited from Tree

Pointers

Because a derived object has all the methods and members as its base, a pointer to the derived type can be used any place that a pointer to the base is expected.

// this function expects a pointer to a Tree
// a pointer to any object derived from a Tree will also work
void GrowATree(Tree* tree) {
  tree->grow();
}

void MakeATree() {
  FruitTree* appleTree;
  appleTree = new FruitTree;
  GrowATree(appleTree); // passing a FruitTree to a function expecting a Tree
}

Specializing

You can specialize the behavior of a derived object by using virtual methods. A virtual method is a method that is defined for a base class and (optionally) overrided in derived classes to provide a specialized implementation. When a virtual method is called through a base object pointer, the specialized version of the method is called based on the object's actual type. To create a virtual method, use the virtual keyword in the base class (the method in the derived class is automatically virtual). An example makes this more clear:

object Animal {
  virtual string speak();
};
string Animal.speak() { return "(silence)"; }

object Dog : Animal {
  string speak();
};
string Dog.speak() { return "woof"; }

object Cat : Animal {
  string speak();
};
string Cat.speak() { return "meow"; }

object Lion : Cat { // a derived object can inherit from another derived object
  string speak();
};
string Lion.speak() { return "roar"; }

void TalkToTheAnimals(Animal* animals, int count) {
  int i;
  for (i = 0; i < count; i++) {
    // although 'animals' is an array of Animal pointers,
    // at runtime the actual type pointed to by each
    // pointer is determined (e.g. Dog, Cat), and the
    // correct method is called (e.g Dog.speak())
    alert(animals[i]->speak());
  }
}

void MakeAnimals() {
  Animal* animals[4];
  animals[0] = new Animal;
  animals[1] = new Dog;
  animals[2] = new Cat;
  animals[3] = new Lion;
  TalkToTheAnimals(animals, 4); // displays (silence), woof, meow, and roar
}

Often when specializing a method from a base object, it is useful to call the base object's implementation. This can be done using the base keyword. For example, a BigDog might want to make the same sound as a Dog, only louder (in all UPPERCASE). This is accomplished like this:

object BigDog : Dog {
  string speak();
};
string BigDog.speak() {
  string sound;
  sound = base.speak(); // calls Dog.speak(), returning woof
  return strupr(sound); // return WOOF
}

Special methods (_init, _destroy, _copy)

In derived classes, the special methods have some special behavior. The _init() method automatically calls the base _init() method as well as the _init() methods of all member objects before entering the method body. The _destroy() method automatically calls the base _destroy() method as well as the _destroy() method of all member objects after exiting the method body. All _destroy() methods are automatically virtual. The _copy() method automatically calls the base _copy() method as well as the _copy() method of all member objects before entering the method.

Objects vs. Structs

A struct and an object are very similar - they both support methods and members. However, an object cannot inherit from a struct and only objects may inherit from other objects and interfaces.


Contents > Language Reference > Interfaces

Interfaces

An interface is a way of specifying how to interact with an object without needing to know the type of the object. An interface allows otherwise unrelated object to expose a common set of methods. For example, both an orchestra and a CD player can play a song. However, there is little similarity otherwise and these objects should not derive from a common base object. The solution it to have these objects implement a common interface.

Declaring an interface

An interface is declared with the interface keyword and looks similar to an object declaration. An interface contains a list of methods, but cannot have member variables. An interface can inherit from other interfaces. All methods in an interface are virtual. Each object that claims to implement an interface must provide an implementation for each method that is part of the interface. For example:

interface ISongPlayer {
  void playSong();
};
// an interface cannot have an implementation

object CDPlayer : ISongPlayer {
  void eject();
  void playSong();
};
void CDPlayer.eject() { alert("ejecting CD"); }
// provide an implementation for the ISongPlayer method
void CDPlayer.playSong() { alert("playing a CD track"); }

object Orchestra : ISongPlayer {
  void playSong();
  void practice();
};
// provide an implementation for the ISongPlayer method
void Orchestra.playSong() { alert("playing lots of instruments"); }
void Orchestra.practice() { alert("practicing"); }

In this example the ISongPlayer interface represents the commonality between the Orchestra and CDPlayer, specifically the ability to play a song. The CDPlayer and Orchestra each have other functionality as well.

Using an interface

An interface must be used through a pointer. Because an interface does not represent any specific object, an interface cannot be instantiated directly. An interface is used just like a pointer to an object, but the interface pointer can only be used to call methods defined on that interface (plus any of the base interfaces that the interface inherits from). For example:

void PlayASong(ISongPlayer* player) {
  player->playSong(); // call a method on the interface
}

void Concert() {
  Orchestra orchestra;
  orchestra.practice();
  // because Orchestra implements the ISongPlayer interface, we can
  // pass a pointer to the orchestra to PlayASong, which is expecting
  // an ISongPlayer pointer
  PlayASong(&orchestra);
}

Contents > Language Reference > Function Pointers

Function Pointers

A function pointer is a variable which contains the address of a function. When the address of a function is assigned to such a variable, it can be used just like the function. To use a function pointer, you must first declare a function pointer type using the funcptr keyword.

funcptr CallBack string(string, int);

In the above declaration, a new type named CallBack is declared. The second half of the declaration is the function signature, which states that the CallBack type can be used to point to any function which returns an string, and takes a string and an int as its arguments. In order to use the new type, you must declare a variable of that type and assign a matching function address to it:

string display(string str, int x) {
  return str + " = " + x;
}

CallBack cb;
cb = display;

Once assigned, the function pointer can be used as if it were the function it points to. The following two lines do the same thing:

result = display("value", 5);
result = cb("value", 5);

Contents > Language Reference > Event handlers

Event handlers

An OrbC application responds to user interaction through event handlers. Whenever a button is pressed, a form is opened, or dozens of other things happen, an event is generated. If the application has defined a handler for that event, that handler is executed.

For each user interface object (form, button, etc.) in your application, the IDE automatically creates an object to represent it. Each type of control has a corresponding object type - a button is of type UIButton, form is UIForm, etc. See the API documentation for more details and supported events. For example, if you created a button named 'helloButton', the compiler would create a UIButton object named 'helloButton'.

An event handler is similar to an object method, but is defined for an instance of a UI object. A handler does not have a return value, and takes no parameters - the event information can be retrieved using the Event object, see API documentation for details. When one of the events occurs that is supported by the object, the handler is executed. Using 'helloButton' as an example, you could define a handler for the onselect event (which is the only event supported by a button) like this:

handler helloButton.onselect() {
  alert("You pressed the 'hello' button!");
}

Just as with object methods, event handlers can access the UI object's members directly (like the x and y coordinate):

handler helloButton.onselect() {
  string coord;
  coord = "(" + x + "," + y + ")"; // x and y provided by UIButton object
  alert("helloButton is at " + coord);
}

Unlike object methods, handlers cannot be called directly - only the runtime can call them.


Contents > Language Reference > Creating and Using Gadgets

Creating and Using Gadgets

The Palm OS provides many useful types of UI controls, such as buttons, lists, and text fields. However, many times an application needs to interact with the user in a way these controls to do not provide. For example, a chart control would be very useful for displaying collected data and a graph control would make displaying trends easy. Using gadgets it is possible to create these and many other controls that can then be reused in multiple forms or applications.

A gadget is defined by creating a struct which provides some or all the default event handlers. A gadget must have a UIGadget as its first member, and must define methods to handle one or more of the gadget events (onopen, onclose, ondraw, onpendown, onpenmove, onpenup). These methods have the same name as the event handler, take no parameters, and return nothing (void). For example:

struct Chart {     // NOTE: As of version 3 this must be a struct
  UIGadget gadget; // rather than an object
  void onopen();
  void onclose();
  void ondraw();
  
  Draw draw;
};

void Chart.onopen() {
  draw.attachGadget(gadget); // a draw struct can attach to a gadget when it opens
}

void Chart.onclose() {
}

void Chart.ondraw() {
  draw.begin();
  draw.line(clrFG, 0, 0, gadget.w-1, gadget.h-1);
  draw.end();
}

When these methods are defined, they are used as the default event handlers for gadgets of this type. The UIGadget struct (called 'gadget' above) is used to access the location, size, and visibility properties of the gadget (see ondraw above).

To use a gadget, create an instance of one on a form using the IDE. After a Chart gadget is added to a form, the IDE automatically creates a Chart struct using the name you have specified, myChart for example. To customize this struct, you may override the default handlers that the gadget type provides. However, if you define a handler for myChart, the default handler will not be called unless you explicitly call it. For example:

handler myChart.ondraw() {
  ondraw(); // call the default Chart supplies
  draw.begin();
  draw.line(clrFG, gadget.w-1, 0, 0, gadget.h-1);// draw an intersecting line
  draw.end();
}

Custom Events

It is often useful for a gadget to create its own events. For example, a tic-tac-toe gadget could define an onusermove event that would fire when the user clicked on the gadget in a valid position. Implementing custom events in your gadget requires two steps. First, declare the event handler in your struct using the handler keyword:

struct TicGadget {
  // A UIGadget must always be the first member of a gadget
  UIGadget gadget;
  
  // default event handlers
  void onpenup();
  
  // custom events
  handler onusermove;
  
  // actions from app
  void newGame();
  ...

Second, call the event handler. The event handler is called like a normal function, however, if the user of the gadget has not defined a handler, the call does nothing. Like other handlers, custom event handlers do not take arguments. Unlike other handlers, a custom handler cannot pass event arguments in the Event structure (since its properties are read-only). Instead, the data needed by the event handler should be member variables in your gadget struct.

void TicGadget.onpenup() {
  if (isValidMove(event.x, event.y) {
    onusermove();
  }
}

Embedding Bitmaps in Your Gadget

If your gadget uses bitmaps, it is useful to automatically include those in the application without requiring the user to explicitly add them to the project. To do this, add a bitmap declaration to your gadget declaration file. A bitmap declaration looks like this:

@bitmap Bitmap yourBitmapName {
  id = 2000
  image1 = "gadget_bitmap_file_1.bmp"
  image8 = "gadget_bitmap_file_8.bmp"
}

As you can see, a bitmap declaration does not follow the same syntax rules as other language elements. In particular, there is no semicolon a the end of the block, nor are there any within the block.

The bitmap declaration contains two properties - the id and the image names. The id is the resource id that will be used to store and reference the bitmap by the OS. This id must not conflict with other bitmap ids in the application, so you should choose a high number (but <9000). The image files associated with a bitmap are specified using the imagex properties, where x is the bit-depth - you may specify image1, image2, image4, image8, and/or image16. High density images can also be specified using imageh1, imageh2, imageh4, imageh8, and/or imageh16. You may specify any combination of bit-depths. All normal density images specified must be the same size; high-density images must be exactly twice the size of normal images.


Contents > Language Reference > Debug Support

Debug Support

OrbC provides several features that aid developers in debugging code:

Most of the features are only available when the project is built in debug mode. These features generate no code in non-debug builds, which means you can keep all your debugging code in place without degrading the performance/experience of your shipping code. In addition to the features below, when building a debug build, the DEBUG preprocessor symbol will be defined.

Assertions

An assertion tests an assumption that a developer makes when writing code, displaying an error if the specified condition is not true. Assertions do not generate any code in non-debug builds. Using assertions liberally is a great way to ensure that otherwise difficult bugs are found early in the development cycle. An assertion looks very similar to function call using the assert keyword:

void setAge(int age) {
  assert(age > 0);
  // do something with age
}

The function above takes an age as a parameter. Age cannot be negative, so this function expects the parameter to always be positive. If a number less than 1 is ever passed to this function, the code above will cause an error message to be displayed containing "assert(age > 0)" and the call stack causing the error.

Debug Logging

Logging can be effectively used to track the state of an application. Debug logging works in conjunction with the emulator or simulator to create a file on your computer containing output from your OrbC application. Logging code is not generated for non-debug builds.

To write a line to the log, use the debuglog keyword:

void myButton.onselect() {
  debuglog("myButton pressed, myCheckbox.checked=" + myCheckbox.checked);
}

When the first debuglog statement is encountered, the file "\orbforms_log.txt" is opened in the root of the drive that was used to launch the emulator. Each debuglog statement creates a new line in the log file.

Debug-only Code Blocks

In addition to logging and assertions, you can create blocks of code that are only compiled into debug builds. To do this, create a block using the debug keyword. In the following example, a status label is used for verbose data:

void loadData(Stream* stream) {
  Rectangle rect;
  int count, i;
  count = stream->readInt();
  for (i=0; i<count; i++) {
    stream->read(&rect, typeof(rect), 1);
    debug {
      // display in a status label how many rects have been read
      // (this info it too verbose for end users, so do not
      // show this in non-debug builds)
      labelStatus.text = i + " rects";
    }
  }
}

Runtime Functions

The following functions can be very useful in debugging. These are available in debug builds and non-debug builds.


Contents > Language Reference > Compiler Directives

Compiler Directives

OrbC supports basic C-style compiler directives for conditional compilation. Conditional compilation allows you to have code in your source files that may or may not be compiled, based on defined symbols. OrbC also supports basic replacement macros.

#define symbol

Defines a symbol for use with the conditional directives below.

#define INTERNAL_BUILD

#define symbol replacementText

Defines a symbol for source code replacement. When the compiler comes across symbol in a source file, it treats it as though it actually read replacementText.

#define COLOR "green"
#define DIVIDEBY3 / 3

int x;
string func() {
  x = 12 DIVIDEBY3;
  return COLOR;
}

The code above will return "green", because the compiler replaces COLOR with "green". In the the first line of the function, x is assigned 4, because the compiler sees x = 12 / 3;.

#if symbol, #endif

These directives surround blocks of code. Code between an #if directive and the matching #endif will be compiled if the specified symbol was previously defined.

#define GREEN

string func() {
  string color = "red";
#if GREEN
  color = "green";
#endif
  return color;
}

In the code above, func will return "green". Because the symbol GREEN was defined, the code in the middle of the function will be compiled.

string func() {
  string color = "red";
#if GREEN
  color = "green";
#endif
  return color;
}

In the code above, func will return "red". The code in the middle of the function was not compiled, because GREEN had not been defined. The function was compiled as if the line color = "green"; did not exist.

#if symbol, #else, #endif

The #else directive can be used between an #if and an #endif, with the expected behavior.

#define GREEN

string func() {
  string color;
#if GREEN
  color = "green";
#else
  color = "red";
#endif
  return color;
}

The code above returns "green", because GREEN is defined. Had GREEN not been defined, func would return "red".

#if !symbol

Using the ! operator before the symbol will test for the symbol not being defined.

string func() {
  string color;
#if !RED
  color = "green";
#else
  color = "red";
#endif
  return color;
}

The code above returns "green" because the symbol RED has not been defined.

#undef symbol

Undefines the specified symbol.

#define GREEN
#undef GREEN

string func() {
  string color = "red";
#if GREEN
  color = "green";
#endif
  return color;
}

The code above will return "red". Although GREEN was defined, it was undefined before the #if directive.