I do not wish to leave the discussion group with the notion that the foregoing is the preferred use-case for library(protobufs). It is not. What we’ve been discussing pertains to means and methods for inter-working with foreign wire-streams that have been generated by other systems and languages that are utilizing Google’s tools.
For those of you who are writing in Prolog for Prolog, you can expect much more from library(protobufs). But much more is expected from you. Think of library(protobufs) as a toolkit, and not as a turnkey service.
The following screenshot depicts an HMI for a very-large distributed application. This system has National Language Support for U.S. English (usa), French (fra), German (ger), and Danish (dan):
The National Language chosen for the HMI is selected by way of an environment variable that’s provided in the user’s log-in profile. Several National Languages may be supported on the same machine simultaneously.
The HMI application is a SWIPL PCE application for those who don’t recognize it. The HMI has no built-in knowledge about the structure of the menu tree that it will present, or what the fkeys will do. The HMI application only knows how to render the UI widgets, the splash photo, and the fkeys, and how to ask for a menu tree.
The HMI needs two things: ‘fkeys’ for the function keys, and ‘fmenus’ for the menu item hierarchy.
For this illustration, we’re only going to be looking at the ‘fkeys’:
In the Prolog structure illustrated below, a list of ‘fkey’ structures is provided. Each ‘fkey’ structure provides a ‘moniker’ and a list of ‘fno’ structures. Each ‘fno’ structure provides a UI widget association, ‘f1’ through ‘f10’, some fly-over help text for that widget, and a list of actions that should be performed when that widget is pressed/selected, etc.
main_menu(usa, Menu) :-
Menu = [
[fkey(fkeyversion,
[ fno(f1, '$s2r31: 3.1.16$', action([func(['MmiFkeyHelp'])]))
]),
fkey(fkey1,
[ fno(f1,'Help',action([func(['MmiFkeyHelp'])])),
fno(f3,'Redraw',action([func(['MmiFkeyRedraw'])])),
fno(f4,'Print screen',action([func(['MmiFkeyPrnscr'])]))
]),
fkey(fkey2,
[ fno(f1,'Help',action([func(['MmiFkeyHelp'])])),
fno(f2,'Main menu',action([func(['NmGoMain'])])),
fno(f3,'Redraw',action([func(['MmiFkeyRedraw'])])),
fno(f4,'Print screen',action([func(['MmiFkeyPrnscr'])])),
fno(es,'Return',action([exit]))
])
],
[
% FMenu parts (removed for simplicity)
]
].
And equivalent ‘.proto’ file looks like this:
syntax = "proto2";
message MMIActionClause {
repeated string func = 130;
optional bool exit = 131;
}
message MMIFnoClause {
required string moniker = 121;
required string help = 122;
repeated MMIActionClause action = 123;
}
message MMIFKeyClause {
required string moniker = 110;
repeated MMIFnoClause fno = 120;
}
enum Language {
USA = 1;
FRA = 2;
GER = 3;
DAN = 4;
}
message MMIMessage {
repeated MMIFKeyClause fkey = 100;
// repeated MMIMenuClause fmenu = 200;
}
message MMIRequest {
required Language nls = 1;
required MMIMessage fkeys = 2;
}
NOTE: As originally designed, SWIPL’s library(protobufs) is stands-alone. It does not require any tools, libraries, or source code artifacts, to be provided by others in order to be used effectively on SWIPL. (I will admit however, that constructing equivalent ‘.proto’ files for your grammars, and using ‘protoc’ as a validation tools turn out to be really handy.)
Getting back to the illustration:
The menu trees are dispensed by a server process running out on the cluster somewhere. For each member of a ‘language’ enumeration, the server replies with the associated protobuf wire-stream that has been pre-built when the server is launched. The interaction between the HMI and the server is by way of a synchronous transaction over an IPC channel (connectionless socket, TIPC sockets in this case).
The wire-stream transaction between HMI client and server looks like this:
client_sent([8,1]), to(name(20007,10,0))
… about 900 micro-seconds later…
client_rcvd([8,1,18,232,2,162,6,57,242,6,11,102,107,101,121,118,101,114,115,105,111| ...]),
length(365), from(port_id('<1.1.2:2252316681>'))
Now, all you have to do is reconstitute the ‘main_menu’ structure that we saw earlier. “But wait!”, you’d say, “The structure doesn’t resemble anything that library(protobufs) knows anything about!” And you’d be right. We have provide some extensions that protobuf_message/2 can use to serialize your own structures.
Somewhere on both sides we’d have to provide the following hooks:
%
% Hooks into protobufs
%
protobufs:message_sequence(Type, Tag, Value) -->
{ my_message_sequence(Type, Value, Proto) },
protobufs:message_sequence(embedded, Tag, Proto).
%
% fkey, fno, and action clause message sequence predicates
%
% See: mmi.proto
%
my_message_sequence(fkey, fkey(Moniker, FNoList), Proto) :-
Proto = protobuf([atom(110, Moniker),
repeated(120, fno(FNoList) )
]).
my_message_sequence(fno, fno(Moniker, Help, action(Actions)), Proto) :-
Proto = protobuf([atom(121, Moniker),
atom(122, Help),
repeated(123, action(Actions))
]).
my_message_sequence(action, func(FuncList), Proto) :-
Proto = protobuf([ repeated(130, atom(FuncList))]).
my_message_sequence(action, exit, Proto) :-
Proto = protobuf([ boolean(131, true) ]).
And we’d have to register the enumeration:
%
% Register an enumeration 'language' with protobufs
%
protobufs:language(Key, Value) :-
nth1(Value, [ usa, fra, ger, dan], Key).
NOTE: We are defining these things in ‘protobufs’ name space, not our own. library(protobufs) can’t see them otherwise.
Now, protobuf_message/2 can treat your own structures as first-class entities that can be ‘repeated’ and so forth. When protobuf_message/2 is presented with the wire-stream returned by the server, it yields a list that contains the ‘fkey’ structures, exactly as we expect it to appear:
[ fkey(
fkeyversion,
[ fno(
f1,
'$s2r31: 3.1.16$',
action(
[ func(
[ 'MmiFkeyHelp'
])
]))
]),
fkey(
fkey1,
[ fno(
f1,
'Help',
action(
[ func(
[ 'MmiFkeyHelp'
])
])),
fno(
f3,
'Redraw',
action(
[ func(
[ 'MmiFkeyRedraw'
])
])),
fno(
f4,
'Print screen',
action(
[ func(
[ 'MmiFkeyPrnscr'
])
]))
]),
fkey(
fkey2,
[ fno(
f1,
'Help',
action(
[ func(
[ 'MmiFkeyHelp'
])
])),
fno(
f2,
'Main menu',
action(
[ func(
[ 'NmGoMain'
])
])),
fno(
f3,
'Redraw',
action(
[ func(
[ 'MmiFkeyRedraw'
])
])),
fno(
f4,
'Print screen',
action(
[ func(
[ 'MmiFkeyPrnscr'
])
])),
fno(
es,
'Return',
action(
[ exit
]))
])
]
If you’ve done your home-work, then tt’s safe to assume that if protobuf_message/2 succeeded, then the structure (Reply) that was returned is both valid and well-formed, for the request that you provided and for the grammar that you specified. The HMI client can use this structure directly to construct the appropriate UI elements.
Protobuf_message/2:
- is the “Guardian at the Gate”
- for constants, it will generate a wire-stream for encode
- for constants, it will unify with a wire-stream on decode
The client side might look like this:
%
% Here's the Client Side.
%
% tipc_client/1 is semidet.
%
tipc_client(NLS) :-
must_be(constant, NLS),
Request = protobuf([enum(1, language(NLS))]),
Payload = protobuf([repeated(100, fkey(Fkeys))]),
Reply = protobuf([enum(1, language(NLS)),
embedded(2, Payload)]),
protobuf_message(Request, UpMsg), % Request is valid/well-formed, if true
tipc_server_address(Addr),
tipc_service_exists(Addr), % at least one server is available, if true
format('client_sent(~w), to(~p)~n', [ UpMsg, Addr]),
tipc_socket(S, dgram)
~> tipc_close_socket(S),
tipc_send(S, UpMsg, Addr, []),
% Rendezvous with the Server
tipc_receive(S, DownMsg, From, [as(codes)]), !,
length(DownMsg, Len),
format('client_rcvd(~w), length(~d), from(~p)~n', [DownMsg, Len, From]),
protobuf_message(Reply, DownMsg), % Reply is valid/well-formed, if true
print_term(Fkeys, [indent_arguments(2)]).
The worker loop on server side might look like this:
process_requests(S) :-
Request = protobuf([enum(1, language(NLS))]),
Reply = protobuf([enum(1, language(NLS)),
codes(2, Codes)]),
repeat,
tipc_receive(S, UpMsg, Peer, [as(codes)]),
format('server_rcvd(~w), from(~p)~n', [UpMsg, Peer]),
protobuf_message(Request, UpMsg),
once(fkey_msg(NLS, Codes)),
protobuf_message(Reply, DownMsg),
writeln(server_replies(DownMsg)),
tipc_send(S, DownMsg, Peer, []),
fail.
Finally, if our National Language selection is French, the we’d see:
Regards.