Contents
Description of entities, components, and interfaces (EDL, CDL, IDL)
All entities, components and interfaces used in the solution must be statically described using the EDL, CDL and IDL languages. The corresponding descriptions are saved in the *.edl
, *.cdl
and *.idl
files, respectively.
Description files are used when building a solution for the following purposes:
- Generating header files containing transport methods and types
- Building a security module
Entity-Component-Interface model
Each entity can provide one or more interaction interfaces. Multiple interfaces can be combined into a component. Components can be embedded into other components. All entities, components and interfaces used in the solution must be statically described.
In KasperskyOS, there are three types of static description files:
- An EDL file contains a description of the entity in the Entity Definition Language (EDL): its name, components and interfaces, and other information.
- A CDL file contains a description of the component in the Component Definition Language,hereinafter referred to as CDL. The component description includes its name, set of included components and implemented interfaces.
- An IDL file contains a description of the package containing one interface in the Interface Definition Language (IDL). The description includes the package name, a declaration of the interface included in it, and a declaration of the types and named constants.
To ensure flexibility, an entity can contain several instances of one component. Multiple entities can include instances of the same component.
An entity does not have to contain any component and does not have to implement an interface. If this is the case, it does not provide functionality that is available to other entities.
The Server entity includes instances of the Terminal and Serial components containing various implementations of the Console interface.
Page topEDL
Each entity in KasperskyOS must be described in the Entity Definition Language (EDL), in a separate file named <entity name>.edl
.
The name of an EDL file must match the name of the entity that it describes.
An EDL file contains the following sections – the order is important:
- Name of the entity. Required section beginning with the reserved word
entity
, which is followed by the entity name.The entity name must begin with an uppercase letter and must not contain an underscore (_).
- Security interface used by the entity. This section is optional. It must be added only if the entity uses a security interface. It is declared with the reserved word
security
, which should be followed by the full name of the interface. - List of implementations of interfaces that are provided by the entity. This is optional, and is described in the
interfaces
section. Each interface implementation is specified with a separate string in the following format:interfaces {
<interface implementation name>:<interface name>
}
An entity can contain several implementations of one interface. All implemented interfaces must be described in the IDL language in IDL files.
The interface implementation name must not contain an underscore (_).
- List of instances of components included in the entity. This is optional, and is described in the
components
section. Each component instance is specified with a separate string in the following format:components {
<component instance name>:<component name>
}
For each specified component, a separate file named
<component name>.cdl
needs to be created, containing a description of the component in the CDL language. Multiple instances of the same component can be added to an entity, and each instance can have a separate state (see the example ofUartDriver
entity below).The component instance name must not contain an underscore (_).
The interfaces
and components
sections are optional and are added only for server entities. Implementations of interfaces declared in the interfaces
section and implementations that are included in components from the components
section can be used by client entities.
EDL supports single-line comments and multi-line C++-style comments:
/* This is a comment
And this, too */
// Another comment
Examples of EDL files
At its simplest, the entity does not use a security interface and does not provide functionality to other entities, like the hello
entity from the hello example. An EDL description for such an entity contains only the reserved word entity
and the entity name.
Hello.edl
// Entity name: Hello
entity Hello
In the following example, the EFoo
entity contains a single interface implementation and does not use a security interface.
Efoo.edl
// Entity name: Efoo
entity Efoo
// The entity contains a named implementation of the IFool interface. Implementation name: foo.
interfaces {
foo : IFoo
}
In the following example, the UartDriver
entity contains two instances of the UartComp
component: one for each UART device.
The UartDriver
entity does not use a security interface.
UartDriver.edl
// Entity name: UartDriver
entity UartDriver
// uart0 and uart1 are the names of instances of the UartComp component
// that are responsible for two different devices
components {
uart0: UartComp
uart1: UartComp
}
CDL
Each component used in the solution must be described in the CDL language, in a separate <component name>.cdl
file.
The name of a CDL file must match the name of the component that it describes.
A CDL file includes the following sections:
- The name of the component. The reserved word
component
is placed before the component name.The component name must begin with an uppercase letter and must not contain an underscore (_).
- Security interface within this component. This section is optional. It must be added only if the component contains a security interface. It is declared with the reserved word
security
, which should be followed by the full name of the interface. - A list of interface implementations included in this component. Interfaces are declared in the
interfaces
section in which each interface implementation is specified with a separate string in the following format:interfaces {
<interface implementation name>:<interface name>
}
A component can contain several implementations of one interface. All implemented interfaces must be described in the IDL language in IDL files.
The interface implementation name must not contain an underscore (_).
- The list of instances of components embedded in this component. This section is optional. It should be added if the component contains embedded components. Components are declared in the
components
section in which each component instance is specified with a separate string in the following format:components {
<component instance name>:<component name>
}
For each specified component, a separate file named
<component name>.cdl
needs to be created, containing a description of the component in the CDL language. Multiple instances of the same component can be added to a component, and each instance can have a separate state.The component instance name must not contain an underscore (_).
CDL supports single-line comments and multi-line C++-style comments.
Examples of CDL files
At its simplest, the component contains a single interface implementation similar to the ping
component from the echo example.
Ping.cdl
/* Component name: Ping */
component Ping
/* The component contains a named implementation of the IPing interface. Implementation name: pingimpl.*/
components {
pingimpl: IPing
}
In the following example, the CoFoo
component contains implementations of two interfaces declared in two different packages named Foo
and Baz
(i.e. in the Foo.idl
and Bar.idl
files):
CoFoo.cdl
/* Component name: CoFoo */
component CoFoo
interfaces {
/* The component contains an implementation of the Foo interface. Implementation name: foo.*/
foo: Foo
/* The component contains three different implementations of the Bar interface. Names of the implementations: bar1, bar2 and bar3.*/
bar1: Bar
bar2: Bar
bar3: Bar
}
In the following example, the CoFoo
component contains a single interface implementation and an embedded component.
CoFoo.cdl
/* Component name: CoFoo */
component CoFoo
interfaces {
/* The component contains an implementation of the Foo interface. Implementation name: foo.*/
foo: Foo
}
components {
/* The component contains an instance of the CoBar component. Instance name: bar.*/
bar: CoBar
}
IDL
All interfaces implemented in a solution must be described in IDL files.
Only one interface can be declared in each IDL file.
The name of an IDL file must match the name of the interface that it describes.
An IDL file contains the following sections:
- Interface name.
The only mandatory section.
The interface name must begin with an uppercase letter and must not contain an underscore (_).
A name declaration has the following format:
package <name of the described interface>
An interface name can be composite. In this case, it is used to generate a path to the IDL file, for example:
/* Module.idl located at: a/b/c/Module.idl */
package a.b.c.Module
- Import external packages.
A package import statement has the following format:
import <name of external package>
It enables you to enter the following elements of an external package into the scope of an IDL file: named constants and user-defined types, including structures and variant types.
- Declaring structures, variant types, named constants, and synonyms.
- Declaration of interface methods.
An interface methods declaration has the following syntax:
interface {
<Declaration of methods>
}
The curly brackets contain declarations of methods that have the following syntax:
<Method name> (<arguments>)
The method name must not contain an underscore (_). It is recommended to begin the names of methods with an uppercase letter.
Arguments are divided into input (
in
) and output (out
) and are separated with a comma. Example interface declaration:interface {
// Declaration of Ping method
Ping(in UInt32 value, out UInt32 result);
}
It is not recommended to use
out
arguments to return codes of errors that occur when a request is being handled by the server entity. Instead, it is recommended to use specializederror
arguments. For more details, refer to Managing errors in IDL.
IDL supports single-line comments and multi-line C++-style comments.
Examples of IDL files
In the simplest case, an IDL description contains only a name and interface declaration:
Ping.idl
/* Ping is the interface name */
package Ping
/* Declaration of interface methods */
interface {
// Declaration of Ping method
Ping(in UInt32 value, out UInt32 result);
}
Example of a more complex IDL description:
Foo.idl
// Name declaration
package Foo
// Import statements
import Bar1
import Bar2
// Declaration of named constant
const UInt32 MaxStringSize = 1024;
// Declaration of synonym for user-defined type
typedef sequence <Char, MaxStringSize> String;
// Declaration of structure
struct BazInfo {
Char x;
array <String, 100> y;
sequence <sequence <UInt32, 100>, 200> y;
}
// Declaration of interface methods
interface {
Baz(in BazInfo a, out UInt32 b);
}
IDL data types
Primitive IDL data types
IDL supports the following primitive data types:
SInt8
,SInt16
,SInt32
,SInt64
(signed integer);UInt8
,UInt16
,UInt32
,UInt64
(unsigned integer);Handle
(handle).
Example declarations of primitive variable types:
SInt32 value;
UInt8 ch;
Handle ResID;
For primitive types (except the Handle
type), you can declare a named constant using the reserved word const
:
const UInt32 c0 = 0;
const UInt32 c1 = 1;
Let us now examine complex (composite) data types: union
(variant type), struct
(structure), array
and sequence
.
union type
The union
variant type, similar to the normal union in C, allows storing different types of values within one memory area. However, when transmitting an IPC message, the union
type is provided with an additional tag
field that lets you define which union field is used in the transmitted value. For this reason, the union
type is also called a union with a tag. Example declaration of a union
variable type:
union foo {
UInt32 bar;
UInt8 baz;
}
A variant type declaration is a top-level declaration.
struct type
Structures in the IDL language are analogous to structures in C. Example structure declaration:
struct BazInfo {
UInt64 x;
UInt64 y;
}
A structure declaration is a top-level declaration.
array type
An array
has a constant size that is defined during declaration:
array <ElementType, ElementsCount>
In IDL, arrays are anonymous and may be declared directly when declaring other complex types, and when declaring interfaces.
The reserved word typedef
is used to assign a name to a declared array type, for example:
typedef array <SInt8, 1024> String;
sequence type
A sequence
resembles a variable-sized array. When declaring a sequence, the maximum number of elements is specified. However, you can actually transmit fewer elements. In this case, only an amount of memory necessary for the transmitted elements will be used.
Similarly to arrays, sequences are anonymous and may be declared directly when declaring interfaces, and when declaring other complex types:
sequence <ElementType, ElementsCount>
Example declaration of a named sequence:
typedef sequence <SInt8, 1024> String;
Embedded complex types
Complex types can be used when declaring other complex types. Arrays
and sequences
can be declared directly inside another composite type:
struct BazInfo {
UInt64 x;
array <UInt8, 100> y;
sequence <sequence <UInt32, 100>, 200> z;
}
The union
or struct
types must be declared prior to use:
union foo {
UInt32 value1;
UInt8 value2;
}
struct bar {
UInt32 a;
UInt8 b;
}
struct BazInfo {
foo x;
bar y;
}
Type synonym declaration (typedef)
The reserved word typedef
declares a name that, within its scope, is a synonym for the specified type. For example:
typedef array <UInt32, 20> value;
The typedef struct
and typedef union
constructs are invalid. A new name for a structure or variant type can be entered only if they were previously declared:
union foo {
UInt32 value1;
UInt8 value2;
}
typedef foo bar;
A typedef
declaration does not introduce a new data type; it declares new names for already existing types.
Managing errors in IDL
It is not recommended to use out
arguments in an IDL description to return codes of logical errors occurring when a request is handled by the server entity to the client entity. Instead, it is recommended to use error
arguments.
A logical error is an error that occurs when the server entity is handling a request and must be returned to the client entity. For example, if a client entity attempts to open a non-existent file, the file system server returns a logical error. Internal errors of the server entity such as memory shortage or invalid statement execution are not logical errors.
Use of error
arguments enables the following:
- No need to forward all
out
arguments of methods in a response message from the server entity to the client entity if an error is returned. Instead, a response message will contain onlyerror
arguments if an error code needs to be returned. - Bind methods of security models not only to response forwarding events but also to error return events.
The IDL language and NK compiler provide two ways to use error
arguments:
- Simplified error handling
This is employed by the NK compiler by default.
When using simplified error handling, the methods of interfaces can have no more than one
error
argument of theUInt16
type. The values of the returnederror
argument of the method are packaged into a result code of the corresponding client interface method. - Extended error handling
Extended error handling requires the use of the
--extended-errors
parameter of the NK compiler.When this is employed, methods of an interface can have multiple
error
arguments of any type. The values of returnederror
arguments of a method are provided in the internal fields of the response message structure.
The types and macros for handling errors are provided in the file named /opt/KasperskyOS-Community-Edition-<version>/toolchain/include/nk/types.h
Simplified error handling
When employing simplified error handling, the methods of interfaces can have no more than one error
argument of the UInt16
type named status
.
In case of an error:
- Implementations of methods on the server entity side must use the
NK_ELOGIC_MAKE()
macro to package the error code and return its result. - The corresponding client interface methods return the error code (logical or transport) packaged into a value of the
nk_err_t
type. TheNK_ELOGIC_CHECK()
andNK_ETRANSPORT_CHECK()
macros are used to check it for logical errors and transport errors, respectively.
If the request is successfully handled:
- Implementations of methods on the server entity side must put the results into the response message fields corresponding to the
out
arguments and returnNK_EOK
. - The corresponding client interface methods also return
NK_EOK
.
For example, let's examine an IDL description of an interface containing one method:
Connection.idl
typedef UInt16 ErrorCode
// error code example
const ErrorCode ESendFailed = 10;
interface {
Send(in Handle handle,
in Packet packet,
out UInt32 sentLen,
// the Send method has one UInt16-type error argument named status
error ErrorCode status
);
}
Implementation of the server entity:
server.c
...
// Send method implementation
nk_err_t Send_impl(struct Connection *self,
const struct Connection_Send_req *req,
const struct nk_arena* req_arena,
struct Connection_Send_res* res,
struct nk_arena* res_arena)
{
if (...) {
// if the request is successfully handled, put the result into an out argument and return NK_EOK
res->sentLen = value;
return NK_EOK;
} else {
// if an error occurs, return the result of the NK_ELOGIC_MAKE macro
return NK_ELOGIC_MAKE(Connection_ESendFailed);
}
Implementation of the client entity:
client.c
// the values of the returned error argument of the method are packaged into a result code of the corresponding client interface method
nk_err_t ret = Connection_Send(ctx,
&req, &req_arena,
&res, NK_NULL);
if (ret == NK_EOK) {
// if there are no errors, res contains the values of out arguments
return res.sentLen;
} else {
if (NK_ELOGIC_CHECK(ret)) {
// handle the logical error
} else {
// handle the transport error
}
}
Extended error handling
When employing extended error handling, the methods of interfaces can have more than one error
argument of any type.
In case of an error:
- Implementations of methods on the server entity side must do the following:
- Use the
nk_err_reset()
macro to set the error flag in a response message. - Put error codes into the response message fields corresponding to
error
arguments. - return
NK_EOK
.
- Use the
- The corresponding client interface methods return the code of the transport error or
NK_EOK
if there is no transport error. Thenk_msg_check_err()
macro is used to check for logical errors in a response message. The values of returnederror
arguments of a method are provided in the internal fields of the response message structure.
If the request is successfully handled:
- Implementations of methods on the server entity side must put the results into the response message fields corresponding to the
out
arguments and returnNK_EOK
. - The corresponding client interface methods also return
NK_EOK
.
For example, let's examine an IDL description of an interface containing one method:
Connection.idl
typedef UInt16 ErrorCode
interface {
Send(in Handle handle,
in Packet packet,
out UInt32 sentLen,
// the Send method has one UInt16-type error argument named status
error ErrorCode status
);
}
The union
type generated by the NK compiler for the constant part of a response message for this method contains out
arguments and error
arguments:
Connection.idl.h
...
struct Connection_Send_res {
union {
struct {
struct nk_message base_;
nk_uint32_t sentLen;
};
struct {
struct nk_message base_;
nk_uint32_t sentLen;
} res_;
struct kl_Connection_Send_err {
struct nk_message base_;
nk_uint16_t status
} err_;
};
};
...
Implementation of the server entity:
server.c
...
// Send method implementation
nk_err_t Send_impl(struct Connection *self,
const struct Connection_Send_req *req,
const struct nk_arena* req_arena,
struct Connection_Send_res* res,
struct nk_arena* res_arena)
{
if (...) {
// if the request is successfully handled, put the result into an out argument and return NK_EOK
res->sentLen = value;
return NK_EOK;
} else {
// if an error occurs, call the nk_err_reset() macro to set the error flag in the response message
nk_err_reset(res)
// complete the fields of err arguments in the res->err_ structure
res->err_.status = 10;
// return NK_EOK
return NK_EOK;
}
Implementation of the client entity:
client.c
nk_err_t ret = Connection_Send(ctx,
&req, &req_arena,
&res, NK_NULL);
if (ret == NK_EOK) {
if (nk_msg_check_err(res)) {
// handle the logical error
// the values of returned error arguments of a method are provided in the internal fields of the response message structure
return res.err_.status;
} else {
// if there are no errors, res contains the values of out arguments
return res.sentLen;
}
} else {
// handle the transport error
}
Composite names of entities, components and interfaces
For entities, components and interfaces, you can use composite names containing one or more dots. In this case, the part of the name that comes after the last dot is called the short name.
For example, if the full name of an entity is kl.VfsEntity
, its short name is VfsEntity
. An interface with the full name main.security.Verify
has the short name Verify
.
If a name is not composite (for example, server
) – it is considered to be both the full name and the short name.
Names and paths of description files
The name of an EDL file must match the short name of the entity that it describes. In this case, the composite name is used to indicate the path to the EDL file. For example, kl.VfsEntity
must be described in the kl/VfsEntity.edl
file.
This same rule applies to components and interfaces: the name of a CDL file must match the short name of the component, and the name of an IDL file must match the short name of the interface. However, the composite name is used to indicate the path to a CDL file or IDL file. For example, the net.Updater
component must be described in the net/Updater.cdl
file, and the interface with the full name main.security.Verify
must be described in the main/security/Verify.idl
file.
The short names of entities, components, and interfaces must not contain an underscore (_). The underscore character is allowed in segments of a composite name.
Names of executable files for starting entities
When a solution is started, the Einit entity starts other entities as follows: in ROMFS (in the solution image), it runs the executable file with the name that matches the short name of the entity being started. For example, the Client
and net.Client
entities will be started from the executable file named Client
by default. To start an entity from an executable file with a different name when the solution is started, use the reserved word path
in the init description.
Full names in descriptions
Only the full names of entities, components and interfaces are used in EDL-, CDL- and IDL descriptions, and in the init description and security configuration. As an example, let us examine the already mentioned kl.VfsEntity
, net.Updater
component and main.security.Verify
interface.
Example EDL description of kl.VfsEntity
:
VfsEntity.edl
entity kl.VfsEntity
...
Example CDL description of the net.Updater
component:
Updater.cdl
component net.Updater
...
Example init description:
init.yaml
entities:
- name: kl.VfsEntity
...
Example security configuration:
security.psl
...
/* Declaration of "kl.VfsEntity" entity. */
use EDL kl.VfsEntity;
...
/* Configuration of requests for calling the Check method of any "Verify" interface implementations. */
request interface=main.security.Verify, method=Check { grant () }
...
Names of NK-generated methods and types
The names of generated types and methods are based on the full names of entities, components and interfaces. For example, the net.Updater
component will have the net_Updater_component
structure, the net_Updater_component_init
function, the net_Updater_component_dispatch
dispatcher, as well as the net_Updater_component_req
request structure and net_Updater_component_res
response structure.