The jsii kernel API
This document describes the API for the @jsii/kernel module, which is to be used by all host libraries. It provides
the fundamental features necessary for host processes to interact with the original module's code.
Note
Currently, @jsii/kernel contains the bulk of the logic, however a separate @jsii/runtime package owns the
dialogue between the host and kernel processes. The @jsii/runtime is a very thin glue layer and it will
eventually be merged into @jsii/kernel.
Errors
Most of the calls described in this document may result in an error being raised. It is the responsibility of the host runtime library to deal with such errors correctly: action retries, propagate the error to the host app's code, and so on.
Error responses are signaled by the error key:
export interface ErrorResponse {
/** A simple message describing what happened. */
message: string;
/** Whenever possible, the stack trace of the error. */
stack?: string;
}
Where possible, the host runtime libraries should make sure to affix their own stack trace information where relevant to the kernel process's stack trace when such errors are propagated to host app's code, in order to offer as much relevant context information as possible.
Initialization - the hello message
The host library is responsible for spawning the node process that will run the original module's code. This node
process runs the @jsii/kernel application, and API messages are exchanged via the node processes' standard input and
output pipes.
Upon initialization, the @jsii/kernel process introduces itself to the host application by emitting a single JSON
message:
{
"hello": "@jsii/runtime@0.21.1"
}
Any additional key present on the hello message will be silently ignored by a host library that does not know how to
process it, ensuring forward compatibility of this message, if and when new attributes are added.
Note
In the future, this message may be augmented with additional keys to enable feature negotiation between the host
application and the @jsii/kernel.
Teardown - the exit message
The host library should send the exit message when it no longer needs the @jsii/kernel. This message enables the
@jsii/kernel to trigger clean-up operations, such as getting rid of temporary directories, in order to avoid littering
the temporary directory with leftover files.
The exit message has the following schema:
interface Exit {
/** The exit code the `@jsii/kernel` process should return. Typically `0`. */
readonly exit: number;
}
Important
Once the exit message has been sent, no more data should be sent through to the @jsii/kernel process. The
request stream should be closed as soon as the exit message was sent. Additional data may however be received from
the @jsii/kernel that is intended to the STDOUT or STDERR console streams.
General Kernel API
Once the hello handshake is complete, a sequence of request and responses are exchanged with the @jsii/kernel.
Requests take the form of JSON-encoded messages that all follow the following pattern:
interface Request {
/**
* This field discriminates the various request types.
*/
api:
| 'load' // Loading jsii assemblies into the Kernel
| 'naming' // Obtaining naming information for loaded assemblies
| 'stats' // Obtaining statistics about the Kernel usage
| 'create' // Creating objects
| 'del' // Destroying objects
| 'invoke'
| 'sinvoke' // Invoking methods (and static methods)
| 'get'
| 'sget' // Invoking getters (and static getters)
| 'set'
| 'sset' // Invoking setters (and static setters)
| 'begin'
| 'end'; // Asynchronous method invocation
// ... request-type specific fields ...
}
Loading jsii assemblies into the Kernel
Before any jsii type can be used, the assembly that provides it must be loaded into the kernel. Similarly, all
dependencies of a given jsii module must have been loaded into the kernel before the module itself can be loaded (the
@jsii/kernel does not perform any kind of dependency resolution).
Loading new assemblies into the @jsii/kernel is done using the load API:
interface LoadRequest {
/** The name of the assembly being loaded */
name: string;
/** The version of the assembly being loaded */
version: string;
/** The local path to the npm package for the assembly */
tarball: string;
// The discriminator
api: 'load';
}
The response to the load call provides some high-level information pertaining to the loaded assembly, which may be
used by the host app to validate the result of the operation:
interface LoadResponse {
/** The name of the assembly that was just loaded */
assembly: string;
/** The number of types the assembly declared */
types: number;
}
Once a module is loaded, all the types it declares immediately become available.
Obtaining naming information for loaded assemblies
In certain cases, host runtime libraries may need to obtain naming information from assemblies in order to determine
the translation table from a jsii fully-qualified name to a host-native name. This can be retrieved using the
naming call:
export interface NamingRequest {
/** The name of the assembly for which naming is requested */
assembly: string;
// The discriminator
api: 'naming';
}
In response to the naming call, the @jsii/kernel returns the configuration block for each language supported by the
named assembly:
export interface NamingResponse {
/** The naming information for the requested assembly. */
naming: {
/**
* For each language, provides the jsii configuration block. The content of
* this configuration block is specified by each language implementation.
*/
[language: string]: { [key: string]: any };
};
}
Obtaining statistics about the Kernel usage
The stats call can be used to obtain information about the current Kernel instance, which can be leveraged by unit
tests or in order to produce metrics for tracking the health of a long-running jsii app.
export interface StatsRequest {
// The discriminator
api: 'stats';
}
The response to the stats call contains synthetic information about the current state of the Kernel:
export interface StatsResponse {
/** The number of object reference currently tracked by the Kernel */
objectCount: number;
}
Creating objects
Most jsii workflows start with creating instances of objects. This can mean creating a pure JavaScript object, instantiating a sub-class of some JavaScript class, or even creating a pure-host instance that implements a collection of behavioral interfaces.
Creating objects is achieved using the create API, which accepts the following parameters:
interface CreateRequest {
/** The jsii fully qualified name of the class */
fqn: string;
/** Any arguments to provide to the constructor */
args?: any[];
/** Additional interfaces implemented in the host app */
interfaces?: string[];
/** Any methods or property overridden in the host app */
overrides?: Override[];
// The discriminator
api: 'create';
}
The response to the object call is a decorated ObjectReference object (which is a common parameter to other calls in
the @jsii/kernel API, used to refer to a particular instance):
interface ObjectReference {
/** A handle that uniquely idenfies an instance */
'$jsii.byref': string;
}
interface CreateResponse extends ObjectReference {
/** The list of interfaces implemented by the instance */
'$jsii.interfaces'?: string[];
}
The value of the '$jsii.byref' field of the ObjectReference type is formatted in the following way:
@aws-cdk/core.Stack@10003
└────────┬────────┘ └─┬─┘
│ └─ Opaque numeric identifier
└─ Object instance's base class' jsii fully qualified name
The first part of the reference identifier can have the special un-qualified value Object to denote the absence of a
jsii-known base class (for example when the object only implements a jsii interface).
Additional Interfaces
Sometimes, the host app will extend a jsii class and implement new jsii interfaces that were not present on the
original type. Such interfaces must be declared by providing their jsii fully qualified name as an entry in the
interfaces list.
Providing interfaces in this list that are implicitly present from another declaration (either because they are already
implemented by the class denoted by the fqn field, or because another entry in the interfaces list extends it) is
valid, but not necessary. The @jsii/kernel is responsible for correctly handling redundant declarations.
Danger
While declared interfaces may contain redundant declarations of members already declared by other interfaces or
by the type denoted by fqn, undefined behavior results if any such declaration is not identical to the others
(e.g: property foo declared with type boolean on one of the interfaces, and with type string on another).
Overrides
For any method that is implemented or overridden from the host app, the create call must specify an override
entry. This will inform the Kernel that calls to these methods must be relayed to the host app for execution, instead
of executing the original library's version.
An optional cookie string can be provided. This string will be recorded on the Javascript proxying implementation,
and will be provided to the host app with any callback request. This information can be used, for example, to
improve the performance of implementation lookups in the host app, where the necessary reflection calls would
otherwise be prohibitively expensive.
Override declarations adhere to the following schema:
interface MethodOverride {
/** The name of the overridden method */
method: string;
/** An arbitrary override cookie string */
cookie?: string;
}
interface PropertyOverride {
/** The name of the overridden property */
property: string;
/** An arbitrary override cookie string */
cookie?: string;
}
type Override = MethodOverride | PropertyOverride;
A note about callbacks
All @jsii/runtime calls that interact with object instances (that is, any call except for load, naming and
stats; as well as the del call since jsii does not support customized destructors or finalizers) may result in the
need to execute code that is defined in the host app, when the code path traverses a method or property that was
implemented or overridden in the host app.
Such cases will result in a callback request substituting itself to the response of the original call being made; execution of which will resume once the callback response is provided.
A callback request is sent from the @jsii/kernel's node process to the host app and has the following schema:
interface CallbackRequest {
/** Callback requests are identified by the presence of the `callback` key */
callback: Callback;
}
interface CallbackBase {
/** A unique identifier for this callback request */
cbid: string;
/** The object on which the callback must be executed */
objref: ObjectReference;
/** The callback cookie, if one was specified */
cookie?: string;
}
interface InvokeCallback extends CallbackBase {
/** The name of the host method to be invoked */
method: string;
}
interface GetCallback extends CallbackBase {
/** The name of the property to be read */
property: string;
}
interface SetCallback extends CallbackBase {
/** The name of the property to be written to */
property: string;
/** The value to be set */
value: any;
}
type Callback = InvokeCallback | GetCallback | SetCallback;
In order to fulfill the callback request, the host app may need to perform futher API calls (loading new assemblies,
creating new instances, invoking methods - including delegating to the super implementation, ...). Such calls will
behave as they otherwise would (including the possible introduction of further callback requests).
Once the host app has fulfilled the request, it must signal the result of that execution back to the @jsii/kernel
process by using the complete call:
interface CompleteBase {
/** The callback ID from the corresponding request */
cbid: string;
// The discriminator
api: `complete`;
}
interface CallbackSuccess extends CompleteBase {
/** The result of the execution (`undefined` if void) */
result: any;
}
interface CallbackFailure extends CompleteBase {
/** The error that was raised during fulfillment */
err: string;
}
type CompleteRequest = CallbackSuccess | CallbackFailure;
As discussed earlier, the response to the complete call is the result of resuming execution of the code path that
triggered the callback request. This may be another callback request, or the final result of that call.
The callbacks call may be used to determine the list of all outstanding callback requests:
interface CallbacksRequest {
// The discriminator
api: 'callbacks';
}
This call results in a list of outstanding callbacks:
interface CallbacksResponse {
/** The list of outstanding callback requests */
callbacks: Callback[];
}
Destroying Objects
Once the host app no longer needs a particular object, it can be discarded. This can happen for example when the
host reference to an object is garbage collected or freed. In order to allow the JavaScript process to also
reclaim the associated memory footprint, the del API must be used:
interface DelRequest {
/** The object reference that can be released */
objref: ObjectReference;
// The discriminator
api: 'del';
}
Danger
Failure to use the del API will result in memory leakage as the JavaScript process accumulates garbage in its
Kernel instance. This can eventually result in a Javascript heap out of memory error, and the abnormal termination
of the node process, and consequently of the host app.
Unimplemented
The existing host runtime libraries do not implement this behavior!
Question
There is currently no provision for the node process to inform the host app about object references it dropped.
This mechanism is necessary in order to support garbage collection of resources that involve host-implemented code
(in such cases, the host app must hold on to any instance it passed to JavaScript until it is no longer
reachable from there).
Upon successfully deleting an object reference, the @jsii/kernel will return an empty response object:
export interface DelResponse {}
Invoking methods (and static methods)
Invoking methods is done via the invoke call:
interface InvokeRequest {
/** The object reference on which a method is invoked */
objref: ObjectReference;
/** The name of the method being invoked */
method: string;
/** Any arguments passed to the method invocation */
args?: any[];
// The discriminator
api: 'invoke';
}
Static method invocations do not have a receiving instance, and instead are implemented by way of the sinvoke call:
interface StaticInvokeRequest {
/** The jsii fully qualified name of the declaring type */
fqn: string;
/** The name of the static method being invoked */
method: string;
/** Any arguments passed to the method invocation */
args?: any[];
// The discriminator
api: 'sinvoke';
}
Note that, unlike in certain programming languages such as Java, it is illegal to attempt invoking a static method
on the static type of some instance using the invoke call. All static invocations must be done using sinvoke.
Both the invoke and sinvoke calls result in the same response:
interface InvokeResponse {
/** The result of the method invocation. */
result: any;
}
When the method's return type is void, the result value should typically be undefined, however it may not be ??
(TypeScript may in certain circumstances allow returning a value from a void method): the host app should ignore
such values.
Asynchronous method invocation
Largely un-tested
Asynchronous operations are only partially supported in the various target languages, and is currently not widely used. As such, support is not as "battle-tested" as the rest of the jsii interoperability features, and customers may run into usability issues, unexpected bugs, or surprising behaviors when using async.
In particular, outstanding Promises may not be able to make progress as expected due to specific implementation
details of the @jsii/runtime event loop, which can result in deadlock situations.
The invoke call can only be used to invoke synchronous methods. In order to invoke asynchronous methods, the
begin and end calls must be used instead. Once the host app has entered a synchronous workflow (after it makes
an invoke call), and until it has completed that synchronous operation (after all callbacks have been handled and the
InvokeResponse has been received), no asynchronous operation may be initiated by the host app.
interface BeginRequest {
/** The object reference on which an asynchronous method is invoked */
objref: ObjectReference;
/** The name of the method being invoked */
method: string;
/** Any arguments passed to the method invocation */
args?: any[];
// The discriminator
api: 'begin';
}
Question
There is no static form of this call. Should there be one?
The begin call results in a promise being made:
interface BeginResponse {
/**
* An opaque string that uniquely idenfies the promised result of this
* invocation.
*/
promiseid: string;
}
Whenever the host app needs to obtain the promised value (possibly in a blocking way), it makes the corresponding
end call:
interface EndRequest {
/** The promiseid that was returned from the corresponding `begin` call. */
promiseid: string;
// The discriminator
api: 'end';
}
This will result in the promise being awaited and then resolved:
interface EndResponse {
/** The resolved value of the promise */
result: any;
}
Danger
All begin calls must be matched with an end call. Failure to do so may result in unhandled promise rejections
that might cause the application to terminate in certain environments.
Invoking getters (and static getters)
In order to obtain the value of properties, the get call is used:
interface GetRequest {
/** The object reference on which a poperty is read */
objref: ObjectReference;
/** The name of the property being read */
property: string;
// The discriminator
api: 'get';
}
When operating on static properties, or in order to obtain the value of enum constants, the sget API must be used
instead:
interface StaticGetRequest {
/** The jsii fully qualified name of the declaring type */
fqn: ObjectReference;
/** The name of the property being read */
property: string;
// The discriminator
api: 'sget';
}
Both the get and sget calls result in the same response:
interface GetResponse {
/** The value of the property. */
result: any;
}
Invoking setters (and static setters)
In order to change the value of a property, the set call is used:
interface SetRequest {
/** The object reference on which a poperty is written to */
objref: ObjectReference;
/** The name of the property being written to */
property: string;
/** The value that is written to the property */
value: any;
// The discriminator
api: 'set';
}
When operating on static properties, the sset API must be used instead:
interface StaticSetRequest {
/** The jsii fully qualified name of the declaring type */
fqn: ObjectReference;
/** The name of the property being written to */
property: string;
/** The value that is written to the property */
value: any;
// The discriminator
api: 'sset';
}
Both the set and sset calls result in the same response, which is an empty object:
interface SetResponse {}