The LanguageLink protocol

LanguageLink uses a custom protocol for communication between Glamorous Toolkit and the language server. In this page we give an overview of the protocol format.

The LanguageLink protocol uses either JSON or MsgPack, a binary serialization format similar to JSON. The relevant classes implementing the encodings are subclasses of PharoLinkAbstractMessageBroker Object subclass: #PharoLinkAbstractMessageBroker instanceVariableNames: 'messageCallbacks settings debugMode' classVariableNames: '' package: 'PharoLink-Platform' .

The encoded messages contain dictionaries describing commands and responses between Glamorous Toolkit and the language server. They are encoded as follows:

{
	'type' -> msgType. "explained later"
	'__sync' -> isSync. "depends on the type of the message"
	"... various other keys dependent on the message type"
} asDictionary
  

To understand the protocol better, we will have to discuss message types. The following types exist:

- ENQUEUE: Enqueues a command, usually an evaluation, on the language server. Asynchronous. - IS_ALIVE: sends a heartbeat. Synchronous, sent periodically. The answer is expected to be the string IS_ALIVE.

Server implementations can implement custom message types, but none are implemented by any of the standard language implementations.

The ENQUEUE command (implemented in LanguageLinkEnqueueCommandMessage LanguageLinkAsyncMessage subclass: #LanguageLinkEnqueueCommandMessage instanceVariableNames: 'commandId statements bindings' classVariableNames: '' package: 'PharoLink-Platform-Messages' ) is the central command for communication. It is sent for every code evaluation inside GT, whether triggered programatically or through a snippet.

ENQUEUE commands contain other keys in the message describing the evaluation request. The shape is as follows:

{
	'type' -> 'ENQUEUE'.
	'__sync' -> false.
	'commandId' -> 1234. "a unique identifier for the command"
	'statements' -> '1 + x'. "the code to run, language-specific"
	'bindings' -> {
		'x' -> 15
	} asDictionary "custom bindings to inject into the language"
} asDictionary
  

Bindings should usually be kept by the server, but can be injected from the GT side. This is useful for, e.g., keeping cross-language bindings, as is possible in the Python snippet.

Contrary to what one would probably expect from an endpoint like this, but true to its command name, this is not expected to return the answer directly, but rather enqueue that command for asynchronous evaluation. This setup is used to avoid any long blocking calls between the server and Glamorous Toolkit.

A server is expected to execute an enqueued command asynchronously, at some point returning the evaluation result by sending a message to an endpoint called EVAL owned by the GT instance.

Taking the example ENQUEUE statement above, we could respond to it like so if we were using the JSON encoder:

client := ZnClient new forJsonREST.

client url: 'http://localhost:', port , '/EVAL'.

client entity: {
	'type' -> 'EVAL'.
	'id' -> 1234. "the ID must match the command ID sent above"
	'value' -> '16'. "this is sent directly to the encoder used by the language; more details below"
	'__sync' -> false
} asDictionary
  

The port here may be injected in various ways, depending on how the server is set up, but it should match wahtever is set in the LanguageLinkSettings Object subclass: #LanguageLinkSettings instanceVariableNames: 'debugMode clientSocketAddress serverSocketAddress messageBroker messageBrokerStrategy serverProcessClass serverExecutable serverImage commandFactoryClass commandClass platform serializerClass deserializerClass parserClass serverDebugMode workingDirectory connectionExceptionHandler' classVariableNames: '' package: 'PharoLink-Platform' class used by the application (for some references refer to LanguageLinkSettings>>#jsDefaultSettings jsDefaultSettings ^ self new clientSocketAddress: (LanguageLinkSocketAddress ipOrName: 'localhost' port: (7000 + 99 atRandom)); serverSocketAddress: (LanguageLinkSocketAddress ipOrName: 'localhost' port: (6900 + 99 atRandom)); messageBrokerStrategy: LanguageLinkHttpMessageBroker; serverProcessClass: JSLinkPharoNodejsProcess; platform: JSLinkPharoPlatform new; commandFactoryClass: JSLinkCommandFactory; commandClass: LanguageLinkCommand; serializerClass: LanguageLinkSerializer; deserializerClass: JSLinkDeserializer; parserClass: JSParser; yourself , LanguageLinkSettings>>#pharoDefaultSettings pharoDefaultSettings | binary | binary := self headlessVmFilenameFrom: FileLocator vmBinary resolve. ^ self new serverSocketAddress: (LanguageLinkSocketAddress ipOrName: 'localhost' port: 6900 + 99 atRandom); messageBrokerStrategy: LanguageLinkMsgPackPharoBroker; connectionExceptionHandler: PharoLinkConnectionExceptionHandler new; serverProcessClass: PharoLinkPharoProcess; serverExecutable: binary; serverImage: FileLocator image resolve; platform: PharoLinkPharoPlatform new; commandFactoryClass: PharoLinkCommandFactory; commandClass: PharoLinkCommand; serializerClass: LanguageLinkSerializer; deserializerClass: PharoLinkDeserializer; parserClass: StParser; yourself , or PBPlatform>>#defaultSettings defaultSettings | basePortNumber | "Use 3 consecutive port numbers, makes it easier to listen using a port range in wireshark" basePortNumber := 7000 + 99 atRandom. ^ PBSettings new clientSocketAddress: (LanguageLinkSocketAddress ipOrName: 'localhost' port: basePortNumber); serverSocketAddress: (LanguageLinkSocketAddress ipOrName: 'localhost' port: basePortNumber + 1); debugSocketAddress: (LanguageLinkSocketAddress ipOrName: 'localhost' port: basePortNumber + 2); messageBrokerStrategy: self messageBrokerStrategy; platform: self; serverProcessClass: self processStrategy; commandFactoryClass: PBCommandFactory; commandClass: PBCommand; serializerClass: LanguageLinkSerializer; deserializerClass: PBDeserializer; parserClass: PythonParser; connectionExceptionHandler: PharoLinkConnectionExceptionHandler new; workingDirectory: self folderForApplication ).