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 Promise
s 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 {}