The eC compiler is released as part of the Ecere SDK under a revised BSD license.
eC is compiled and runs natively on the target platforms ensuring maximum performance. The language has been designed keeping C performance in mind.
eC is a superset of C and its ABI is fully compatible with C. Standard C programming can be used anywhere inside eC modules, and other object modules can be linked to eC code as if it was a standard C module.
While it is still possible to include header files to make use of code in C libraries or modules, eC does away with a need for header files while writing eC libraries or modules. Instead, it features an import mechanism making all constructs of imported modules available without requiring to write a separate file. Furthermore, there is no need for forward declaration (prototypes).
import "ecere" import "gameAI"
As an alternative to simple #define
macros not taking parameters, eC lets you define identifiers which resolve to expressions,
and can be exported from a module or library without headers.
public define Pi = Radians { 3.14159265 }; public define myColor = Color { 85, 85, 85 };
eC offers better control over what is visible when building an API library. Library developers don't need to worry about the private content of the class definition being seen by the end user, only what is declared public will be visible. eC provides three level of access control:
static :
Current source file only
private:
Private to library (.dll, .dylib or .so)
public :
Accessible wherever the module is accessible from
On the other hand, eC offers no restriction regarding member access between classes of the same access-level module, as we feel that is counter-productive and results in writing unnecessary accessor functions which negatively affect performance. Developers are instead encouraged to modularize using different source files and libraries to ensure the desired access level for encapsulation.
All variables and literals can be thought of as objects because all data types are treated in an object-oriented manner, and virtual methods are defined and can be overridden to control how data is (de)serialized, (de)stringified, sorted, visualized and edited. GUI common controls, distributed objects, Ecere Data Access, generics and containers all benefit from this.
class
data type, all data types including standard C types
are essentially classes, as pertaining to the type system.A distinctive feature of eC is its curly braces instantiation syntax, inspired from the syntax for list initialization in C. Instantiations can either be named (and are considered declarations):
Point p { 123, 23 };
or anonymous (and are expressions):
Point { 123, 23 }
If member names are not specified, the order of declaration of public members determine to which ones those values are assigned. eC also allows explicitly specifying to which instance members to assign, for greater clarity and convenience, omitting the dot required by the similar C99 syntax in list initializations. Specifiying the members is required for private members. Since Point declares x before y, the above is equivalent to:
Point { x = 123, y = 23 };
If a data type is expected, the class is inferred and can be omitted:
Point p; p = { 123, 23 };
The instantiation syntax is heavily used in eC and member assignment replaces the use of parameters for constructors, which never take any.
In addition to methods and data members, classes can have member properties, a useful way to encapsulate data. Properties are accessors which can cause code to be executed, but their syntactic usage is the same as regular data members. For classes used with an Object Designer (such as the GUI designer), they will show up in IDE property sheet and can be edited while providing instant visual feedback.
class Pen { Color color; public property Color color { get { return color; } set { color = value; } } } void demo() { Pen pen { }; pen.color = red; }
eC distinguishes between class
and struct
, unlike C++ where the only distinction is the default access mode.
One reason behind this is to avoid the need to think of handles for dynamic objects as pointers, and the added complexity and need to be familiar with them for writing basic object oriented programs.
Instances of a class
are always allocated on the heap, and thus the underlying implementation of a class instance is a pointer, but the developer does not need to think of them as such.
The '.'
operator is still used to refer to members rather than '->'
.
As a class is meant for more complex objects which may manage other dynamic objects or some other form of allocated
resources, it has more features which are not available in the simpler struct
:
Class methods can be virtual, and each instance has a virtual function table which can be proper to itself, or point to its class methods if it does not contain an override at the instance level. They can also set default values for any members of a class, and have constructors and destructors (which are always virtual).
An instance of a class will also have a pointer to the class itself (runtime type information) which is useful for reflection.
It has a reference count which serves for memory management.
class SampleClass { MemberClass m { }; int a; a = 123; virtual void vMethod() { PrintLn("vMethod default"); }; byte * data; MyClass() { data = new byte[100]; } ~MyClass() { delete data; } } SampleClass object { void vMethod() { PrintLn("object's vMethod"); } }; void demo() { Class c = object._class; incref object; // Call object's own implementation object.vMethod(); }
The struct
on the other hand are meant to be used as C structures.
They have no overhead (consists only of data members) and are allocated 'in place', e.g.
on the stack for local function variables, or as part of the bigger memory block of a containing class
.
They are useful for small self-contained data types, e.g. Point, objects we want allocated on the stack, objects to be stored contiguously in arrays.
They typically do not hold allocated memory. The same instantiation syntax is used for both class
and struct
.
Functions taking a struct
as a parameter will automatically see it passed with an extra indirection level without having to declare it as a pointer, unless declared
using the C syntax struct NameOfTheStruct
rather than simply NameOfTheStruct
.
This naturally discourages pushing big data blocks to the stack.
The developer still uses the '.'
operator to access members and must understand that it is the same object that was passed, and thus not perform unwanted modifications.
struct Point { int x, y; }; // Semicolon required for struct double length(Point p) { return sqrt(p.x * p.x + p.y + p.y); } void demo() { Point * points = new Point[100]; Point a { 3, 4 }; double l = length(a); // passed by address l = length({ 6, 8 }); delete points; }
It is possible to allocate a struct
dynamically on the heap using the new
operator, but this
then introduces an indirection level and the pointer notation must then be used. As an alternative, eC features a compromise between class
and struct
(class : struct
), allowing to strip classes of their overhead
information so that it contains only data members. Such a class will behave the same way
as a regular class, except it will not support reference counting, runtime type information,
or virtual functions (destructors will not be virtual either). It is typically used for
numerous objects for which we don't need the overhead of regular classes, but which are
allocated individually, such as those to be stored in linked lists.
Because of this, only some particular scenarios calls for the extra reference level and the ->
operator.
class Link : struct { Link prev, next; } void demo() { Link a { }; // On the heap Link b { }; a.next = b; // Still accessed with '.' b.prev = a; }
eC supports class inheritance. A class inherits all the members of a base class. If a method is virtual, invoking it on an instance will call the highest derivation level class, regardless of how the variable used for the call is typed.
class BaseClass { virtual void doStuff() { PrintLn("base"); } } class DerivedClass : BaseClass { void doStuff() { PrintLn("derived"); } } BaseClass a { }; DerivedClass b { }; void demo() { a.doStuff(); b.doStuff(); }
eC supports parametric polymorphism of classes with types. Since eC does not rely on header files, instantiating a generic class does not involve a separate compilation. Only the generic class definition is compiled and used for all parameterizations. Instead eC relies on a common representation for all types and its type system to take proper action based on a type parameter. The syntax for generic uses the angled brackets, as in C++. Complex cases with recursions are supported as well, as can be seen here:
class A<class T> { T test; } class B : A<B> { int a; }
eC provides containers (built using generics) for dynamic arrays (Array), link lists (List for use with an arbitrary type, LinkList for a type embedding links to avoid an extra indirection level), associative arrays (Map), as well as self balancing binary trees (AVLTree).
Array literals are supported and eC offers a for
syntax to iterate through elements, with an optional condition:
for(i : [ 0, 1, 2, 3 ]; i > 0) PrintLn(i);
A container' contents can be initialized with a clear syntax:
Map<String, int> wordCount { [ { "Foo", 1 }, { "Bar", 2 }, { "Cool", 3 }, { "Language", 4 } ] };
Associative arrays support the indexing operator:
const String s = "Bar"; wordCount[s]++;
The Iterator class has a concise iteration syntax, and containers can be used in a generic manner, regardless of the implementation:
void test(Container<int> c) { Iterator<int> i { c }; while(i.Next()) PrintLn(i.data); }
New container classes can be defined, and can be used to iterate through infinite collections, or through data contained outside the actual container class.
Another kind of data types in eC is unit types, meant to hold scalar values associated with a particular unit of measurement, e.g. Meters or Degrees. Types for different units for the same measurement can be related by simply defining a forward and reverse conversion to one default unit for that measurement. This has the advantage of great simplification for adding new related units, over manually overloading every type of operator; only a single pair of conversion must be defined. Still once these units are defined, operations between related units or conversion for assignment or passing as a parameter is automatically handled properly. (eC does not yet feature operator overloading, but there are plans to add it to support vector operations and string concatenation among other things).
// Declare Distance units to use doubles class Distance : double; // Meters as default (no conversion needed) class Meters : Distance { property Distance {} }; class Centimeters : Distance { // Conversion to Meter (100cm / m) property Meters { set { return value * 100; } get { return this / 100; } } };
These auomatic conversions are defined through conversion properties, which share a syntax similar to properties except the property does not define a name. Different kind of objects can be automatically converted from one type to another. If a conversion is loaded from a prebuilt library when compiling and applied to constants, the conversion is precomputed and thus does not affect the performance of the code (e.g. angles are always stored in Radians, but despite using a more familiar Degrees unit the compiler will precompute the Radians value).
class Feet : Distance { // Conversion to Meter (~3.28 feet / m) property Meters { set { return value * 3.28083f; } get { return this / 3.28083f; } } }; void demo() { Meters a; a = Feet { 12 } + Centimeters { 3 }; }
Enumerations in eC are context-sensitive, thus allowing to reuse a generic value for multiple enumerations (e.g. none
).
They support subclassing as well to add new values while inheriting values of an existing enumeration.
enum LineStyle { none, solid, dashed, dotted }; enum FillStyle { none, solid, hatch, stipple }; enum ExtFillStyle : FillStyle { bitmap, gradient }; void demo() { FillStyle fs = solid; LineStyle ls = none; int efs = ExtFillStyle::gradient; }
Another useful data type is bit collection classes, which defines a class-like type (where members are accessed with .
),
yet the entire value is stored in one scalar integer type. It is meant as an alternative to numerous #define
macros often seen in C headers definining how to mask a particular
bit or multi-bit values within a larger integer for flags collection. It also lends itself very well to things like Color classes and conversion from one color depth to another.
class Color : uint { public: // Specifying which bits to use byte r:8:16, g:8:8, b:8:0; } class Color565 : uint16 { public: byte r:5:11, g:6:5, b:5:0; property Color { set { return Color565 { value.r >> 3, value.g >> 2, value.b >> 3 }; } get { return Color {r<<3,g<<2,b<<3}; } } } Color565 orange16Bit = Color { 255, 128, 0 };
eC already takes care of many aspects of memory management through reference counting.
However, it does not automatically manage memroy allocated with the new
operator,
which must be manually freed with the delete
operator.
Furthermore, to avoid the overhead and recognizing the limits of reference counting,
reference count increments and decrements is left to the user, except for the initial
construction (at instantiation) and destruction of the object (when it goes out of scope).
At the moment this automatic behavior is also limited to global and member instances,
other instances currently start with a 0 reference count and must be manually destroyed with delete
.
We plan to improve on this to make the behavior more consistent. (See related tracked issue#513).
eC also provides its own memory error detection library to facilitate identifying and rapidly resolving the unavoidable typical memory problems associated with memory management in an non-managed environment such as leaks, free memory writes & reads and buffer overruns & underruns. It can be easily turned on during the debugging and will provide full call stack information as to where the problem is occuring. The Ecere IDE also provides integration with Valgrind for reporting memory leaks.
Just as one can import an eC library through the import
keyword, eC shared libraries can also be
dynamically imported at runtime. This is most useful as it provides a built-in plug-in architecture model.
The Ecere IDE for example makes use of this feature to support the integration with the Object Designer, such as the Form designer for the Ecere GUI.
Object oriented code written in eC is built around a reflective object model framework. All required information about the programming interfaces is readily and dynamically available. Classes can be instantiated and methods can be called using textual names. This also makes it ideal for automatically generating binding to other languages completely automatically. The unique features of the language offers a solid foundation on which to build a uniform Application Programming Interface using eC that will scale nicely to other languages, since eC was meant as a framework to easily build, publish and maintain classes. This powerful model can also be used in a distributed manner: it supports using objects across a network the same way as if they were local objects.