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.
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).
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.
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);
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
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) |
|
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). |
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 +=, -=, *=, /=, %=,
^=, |=, &=, <<=, >>=
.
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.
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]
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()
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.
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 |
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 |
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) |
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) |
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 |
default: |
An optional marker within a switch statement. If none of
the case s 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 |
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 break
s
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 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 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
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'
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
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; }
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()
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.
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
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 }
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 }
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.
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.
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.
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(); }
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(); } }
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.
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.
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.
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"; } } }
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.
Defines a symbol for use with the conditional directives below.
#define INTERNAL_BUILD
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;
.
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.
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".
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.
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.