The open source OpenXR runtime
1#!/usr/bin/env python3
2# Copyright 2020-2023, Collabora, Ltd.
3# SPDX-License-Identifier: BSL-1.0
4"""Generate code from a JSON file describing the IPC protocol."""
5
6import argparse
7
8from ipcproto.common import (Proto, write_decl, write_invocation,
9 write_result_handler, write_cpp_header_guard_start,
10 write_cpp_header_guard_end, write_msg_struct,
11 write_reply_struct, write_msg_send)
12
13header = '''// Copyright 2020-2023, Collabora, Ltd.
14// SPDX-License-Identifier: BSL-1.0
15/*!
16 * @file
17 * @brief {brief}.
18 * @author Jakob Bornecrantz <jakob@collabora.com>
19 * @ingroup ipc{suffix}
20 */
21'''
22
23
24def write_send_definition(f, call):
25 """Write a ipc_send_CALLNAME_locked function."""
26 call.write_send_decl(f)
27 f.write("\n{\n")
28 f.write("\tIPC_TRACE(ipc_c, \"Sending " + call.name + "\");\n\n")
29
30 write_msg_struct(f, call, '\t')
31
32 write_msg_send(f, 'xrt_result_t ret', indent="\t")
33
34 f.write("\n\treturn ret;\n}\n")
35
36
37def write_receive_definition(f, call):
38 """Write a ipc_receive_CALLNAME_locked function."""
39 call.write_receive_decl(f)
40 f.write("\n{\n")
41 f.write("\tIPC_TRACE(ipc_c, \"Receiving " + call.name + "\");\n\n")
42
43 write_reply_struct(f, call, '\t')
44
45 f.write("\n\t// Await the reply")
46 func = 'ipc_receive'
47 args = ['&ipc_c->imc', '&_reply', 'sizeof(_reply)']
48 write_invocation(f, 'xrt_result_t ret', func, args, indent="\t")
49 f.write(";")
50 write_result_handler(f, 'ret', None, indent="\t")
51
52 for arg in call.out_args:
53 f.write("\t*out_" + arg.name + " = _reply." + arg.name + ";\n")
54
55 f.write("\n\treturn _reply.result;\n}\n")
56
57
58def write_call_definition(f, call):
59 """Write a ipc_call_CALLNAME function."""
60 call.write_call_decl(f)
61 f.write("\n{\n")
62
63 f.write("\tIPC_TRACE(ipc_c, \"Calling " + call.name + "\");\n\n")
64
65 write_msg_struct(f, call, '\t')
66 write_reply_struct(f, call, '\t')
67
68 f.write("""
69\t// Other threads must not read/write the fd while we wait for reply
70\tos_mutex_lock(&ipc_c->mutex);
71""")
72 cleanup = "os_mutex_unlock(&ipc_c->mutex);"
73
74 # Prepare initial sending
75 write_msg_send(f, 'xrt_result_t ret', indent="\t")
76 write_result_handler(f, 'ret', cleanup, indent="\t")
77
78 if call.in_handles:
79 f.write("\n\t// Send our handles separately\n")
80 f.write("\n\t// Wait for server sync")
81 # Must sync with the server so it's expecting the next message.
82 write_invocation(
83 f,
84 'ret',
85 'ipc_receive',
86 (
87 '&ipc_c->imc',
88 '&_sync',
89 'sizeof(_sync)'
90 ),
91 indent="\t"
92 )
93 f.write(';')
94 write_result_handler(f, 'ret', cleanup, indent="\t")
95
96 # Must send these in a second message
97 # since the server doesn't know how many to expect.
98 f.write("\n\t// We need this message data as filler only\n")
99 f.write("\tstruct ipc_command_msg _handle_msg = {\n")
100 f.write("\t .cmd = " + str(call.id) + ",\n")
101 f.write("\t};\n")
102 write_invocation(
103 f,
104 'ret',
105 'ipc_send_handles_' + call.in_handles.stem,
106 (
107 '&ipc_c->imc',
108 "&_handle_msg",
109 "sizeof(_handle_msg)",
110 call.in_handles.arg_name,
111 call.in_handles.count_arg_name
112 ),
113 indent="\t"
114 )
115 f.write(';')
116 write_result_handler(f, 'ret', cleanup, indent="\t")
117
118 f.write("\n\t// Await the reply")
119 func = 'ipc_receive'
120 args = ['&ipc_c->imc', '&_reply', 'sizeof(_reply)']
121 if call.out_handles:
122 func += '_handles_' + call.out_handles.stem
123 args.extend(call.out_handles.arg_names)
124 write_invocation(f, 'ret', func, args, indent="\t")
125 f.write(';')
126 write_result_handler(f, 'ret', cleanup, indent="\t")
127
128 for arg in call.out_args:
129 f.write("\t*out_" + arg.name + " = _reply." + arg.name + ";\n")
130 f.write("\n\t" + cleanup)
131 f.write("\n\treturn _reply.result;\n}\n")
132
133
134def generate_h(file, p):
135 """Generate protocol header.
136
137 Defines command enum, utility functions, and command and reply structures.
138 """
139 f = open(file, "w")
140 f.write(header.format(brief='Generated IPC protocol header', suffix=''))
141 f.write('''
142#pragma once
143
144#include "xrt/xrt_compiler.h"
145
146''')
147
148 write_cpp_header_guard_start(f)
149 f.write('''
150
151struct ipc_connection;
152''')
153
154 f.write('\ntypedef enum ipc_command')
155 f.write('\n{')
156 f.write('\n\tIPC_ERR = 0,')
157 for call in p.calls:
158 f.write("\n\t" + call.id + ",")
159 f.write("\n} ipc_command_t;\n")
160
161 f.write('''
162struct ipc_command_msg
163{
164\tenum ipc_command cmd;
165};
166
167struct ipc_result_reply
168{
169\txrt_result_t result;
170};
171
172''')
173
174 write_decl(f, return_type='static inline const char *',
175 function_name='ipc_cmd_to_str', args=['ipc_command_t id'])
176 f.write('\n{')
177 f.write('\n\tswitch (id) {')
178 f.write('\n\tcase IPC_ERR: return "IPC_ERR";')
179 for call in p.calls:
180 f.write('\n\tcase ' + call.id + ': return "' + call.id + '";')
181 f.write('\n\tdefault: return "IPC_UNKNOWN";')
182 f.write('\n\t}\n}\n')
183
184 f.write('#pragma pack (push, 1)')
185
186 for call in p.calls:
187 # Should we emit a msg struct.
188 if call.needs_msg_struct:
189 f.write('\nstruct ipc_' + call.name + '_msg\n')
190 f.write('{\n')
191 f.write('\tenum ipc_command cmd;\n')
192 for arg in call.in_args:
193 f.write('\t' + arg.get_struct_field() + ';\n')
194 if call.in_handles:
195 f.write('\t%s %s;\n' % (call.in_handles.count_arg_type,
196 call.in_handles.count_arg_name))
197 f.write('};\n')
198 # Should we emit a reply struct.
199 if call.out_args:
200 f.write('\nstruct ipc_' + call.name + '_reply\n')
201 f.write('{\n')
202 f.write('\txrt_result_t result;\n')
203 for arg in call.out_args:
204 f.write('\t' + arg.get_struct_field() + ';\n')
205 f.write('};\n')
206
207 f.write('#pragma pack (pop)\n')
208
209 write_cpp_header_guard_end(f)
210 f.close()
211
212
213def generate_client_c(file, p):
214 """Generate IPC client proxy source."""
215 f = open(file, "w")
216 f.write(header.format(brief='Generated IPC client code', suffix='_client'))
217 f.write('''
218#include "client/ipc_client.h"
219#include "ipc_protocol_generated.h"
220
221
222\n''')
223
224 # Loop over all of the calls.
225 for call in p.calls:
226 if call.varlen:
227 write_send_definition(f, call)
228 write_receive_definition(f, call)
229 else:
230 write_call_definition(f, call)
231
232 f.close()
233
234
235def generate_client_h(file, p):
236 """Generate IPC client header.
237
238 Contains prototypes for generated IPC proxy call functions.
239 """
240 f = open(file, "w")
241 f.write(header.format(brief='Generated IPC client code', suffix='_client'))
242 f.write('''
243#pragma once
244
245#include "shared/ipc_protocol.h"
246#include "ipc_protocol_generated.h"
247#include "client/ipc_client.h"
248
249''')
250 write_cpp_header_guard_start(f)
251 f.write("\n")
252
253 for call in p.calls:
254 if call.varlen:
255 call.write_send_decl(f)
256 f.write(";\n")
257 call.write_receive_decl(f)
258 else:
259 call.write_call_decl(f)
260 f.write(";\n")
261
262 write_cpp_header_guard_end(f)
263 f.close()
264
265
266def generate_server_c(file, p):
267 """Generate IPC server stub/dispatch source."""
268 f = open(file, "w")
269 f.write(header.format(brief='Generated IPC server code', suffix='_server'))
270 f.write('''
271#include "xrt/xrt_limits.h"
272
273#include "shared/ipc_protocol.h"
274#include "shared/ipc_utils.h"
275
276#include "server/ipc_server.h"
277
278#include "ipc_server_generated.h"
279
280''')
281
282 f.write('''
283xrt_result_t
284ipc_dispatch(volatile struct ipc_client_state *ics, ipc_command_t *ipc_command)
285{
286\tswitch (*ipc_command) {
287''')
288
289 for call in p.calls:
290 f.write("\tcase " + call.id + ": {\n")
291
292 f.write("\t\tIPC_TRACE(ics->server, \"Dispatching " + call.name +
293 "\");\n\n")
294
295 if call.needs_msg_struct:
296 f.write(
297 "\t\tstruct ipc_{}_msg *msg = ".format(call.name))
298 f.write("(struct ipc_{}_msg *)ipc_command;\n".format(call.name))
299
300 if call.varlen:
301 f.write("\t\t// No return arguments")
302 elif call.out_args:
303 f.write("\t\tstruct ipc_%s_reply reply = {0};\n" % call.name)
304 else:
305 f.write("\t\tstruct ipc_result_reply reply = {0};\n")
306
307 if call.in_handles:
308 # We need to fetch these handles separately
309 f.write("\t\tstruct ipc_result_reply _sync = {XRT_SUCCESS};\n")
310 f.write("\t\t%s in_%s[XRT_MAX_IPC_HANDLES] = {0};\n" % (
311 call.in_handles.typename, call.in_handles.arg_name))
312 f.write("\t\tstruct ipc_command_msg _handle_msg = {0};\n")
313 if call.out_handles:
314 f.write("\t\t%s %s[XRT_MAX_IPC_HANDLES] = {0};\n" % (
315 call.out_handles.typename, call.out_handles.arg_name))
316 f.write("\t\t%s %s = {0};\n" % (
317 call.out_handles.count_arg_type,
318 call.out_handles.count_arg_name))
319 f.write("\n")
320
321 if call.in_handles:
322 # Validate the number of handles.
323 f.write("\t\tif (msg->%s > XRT_MAX_IPC_HANDLES) {\n" % (call.in_handles.count_arg_name))
324 f.write("\t\t\treturn XRT_ERROR_IPC_FAILURE;\n")
325 f.write("\t\t}\n")
326
327 # Let the client know we are ready to receive the handles.
328 write_invocation(
329 f,
330 'xrt_result_t sync_result',
331 'ipc_send',
332 (
333 "(struct ipc_message_channel *)&ics->imc",
334 "&_sync",
335 "sizeof(_sync)"
336 ),
337 indent="\t\t"
338 )
339 f.write(";")
340 write_result_handler(f, "sync_result",
341 indent="\t\t")
342 write_invocation(
343 f,
344 'xrt_result_t receive_handle_result',
345 'ipc_receive_handles_' + call.in_handles.stem,
346 (
347 "(struct ipc_message_channel *)&ics->imc",
348 "&_handle_msg",
349 "sizeof(_handle_msg)",
350 "in_" + call.in_handles.arg_name,
351 "msg->"+call.in_handles.count_arg_name
352 ),
353 indent="\t\t"
354 )
355 f.write(";")
356 write_result_handler(f, "receive_handle_result",
357 indent="\t\t")
358 f.write("\t\tif (_handle_msg.cmd != %s) {\n" % str(call.id))
359 f.write("\t\t\treturn XRT_ERROR_IPC_FAILURE;\n")
360 f.write("\t\t}\n")
361
362 # Write call to ipc_handle_CALLNAME
363 args = ["ics"]
364
365 # Always provide in arguments.
366 for arg in call.in_args:
367 args.append(("&msg->" + arg.name)
368 if arg.is_aggregate
369 else ("msg->" + arg.name))
370
371 # No reply arguments on varlen.
372 if not call.varlen:
373 args.extend("&reply." + arg.name for arg in call.out_args)
374
375 if call.out_handles:
376 args.extend(("XRT_MAX_IPC_HANDLES",
377 call.out_handles.arg_name,
378 "&" + call.out_handles.count_arg_name))
379
380 if call.in_handles:
381 args.extend(("&in_%s[0]" % call.in_handles.arg_name,
382 "msg->"+call.in_handles.count_arg_name))
383
384 # Should we put the return in the reply or return it?
385 return_target = 'reply.result'
386 if call.varlen:
387 return_target = 'xrt_result_t xret'
388
389 write_invocation(f, return_target, 'ipc_handle_' +
390 call.name, args, indent="\t\t")
391 f.write(";\n")
392
393 # TODO do we check reply.result and
394 # error out before replying if it's not success?
395
396 if not call.varlen:
397 func = 'ipc_send'
398 args = ["(struct ipc_message_channel *)&ics->imc",
399 "&reply",
400 "sizeof(reply)"]
401 if call.out_handles:
402 func += '_handles_' + call.out_handles.stem
403 args.extend(call.out_handles.arg_names)
404 write_invocation(f, 'xrt_result_t xret', func, args, indent="\t\t")
405 f.write(";")
406
407 f.write("\n\t\treturn xret;\n")
408 f.write("\t}\n")
409 f.write('''\tdefault:
410\t\tU_LOG_E("UNHANDLED IPC MESSAGE! %d", *ipc_command);
411\t\treturn XRT_ERROR_IPC_FAILURE;
412\t}
413}
414
415''')
416
417 f.write('''
418size_t
419ipc_command_size(const enum ipc_command cmd)
420{
421\tswitch (cmd) {
422''')
423
424 for call in p.calls:
425 if call.needs_msg_struct:
426 f.write("\tcase " + call.id + ": return sizeof(struct ipc_{}_msg);\n".format(call.name))
427 else:
428 f.write("\tcase " + call.id + ": return sizeof(enum ipc_command);\n")
429
430 f.write('''\tdefault:
431\t\tU_LOG_E("UNHANDLED IPC COMMAND! %d", cmd);
432\t\treturn 0;
433\t}
434}
435
436''')
437
438 f.close()
439
440
441def generate_server_header(file, p):
442 """Generate IPC server header.
443
444 Declares handler prototypes to implement,
445 as well as the prototype for the generated dispatch function.
446 """
447 f = open(file, "w")
448 f.write(header.format(brief='Generated IPC server code', suffix='_server'))
449 f.write('''
450#pragma once
451
452#include "shared/ipc_protocol.h"
453#include "ipc_protocol_generated.h"
454#include "server/ipc_server.h"
455
456
457''')
458
459 write_cpp_header_guard_start(f)
460 f.write("\n")
461
462 # Those decls are constant, but we must write them here
463 # because they depends on a generated enum.
464 write_decl(
465 f,
466 "xrt_result_t",
467 "ipc_dispatch",
468 [
469 "volatile struct ipc_client_state *ics",
470 "ipc_command_t *ipc_command"
471 ]
472 )
473 f.write(";\n")
474
475 write_decl(
476 f,
477 "size_t",
478 "ipc_command_size",
479 [
480 "const enum ipc_command cmd"
481 ]
482 )
483 f.write(";\n")
484
485 for call in p.calls:
486 call.write_handler_decl(f)
487 f.write(";\n")
488
489 write_cpp_header_guard_end(f)
490 f.close()
491
492def generate_struct_names(file, p):
493 """Generate list of structures names.
494
495 Lists the structures used in the IPC protocol, this can be
496 used for tools such as pahole.
497 """
498 f = open(file, "w")
499 f.write("ipc_shared_memory\n")
500 types = set()
501 for call in p.calls:
502 for i in call.in_args + call.out_args:
503 if i.is_aggregate:
504 types.add(i.typename.split(" ")[-1])
505 for t in sorted(types):
506 f.write(t)
507 f.write("\n")
508 f.close()
509
510def main():
511 """Handle command line and generate a file."""
512 parser = argparse.ArgumentParser(description='Protocol generator.')
513 parser.add_argument(
514 'proto', help='Protocol file to use')
515 parser.add_argument(
516 'output', type=str, nargs='+',
517 help='Output file, uses the name to choose output type')
518 args = parser.parse_args()
519
520 p = Proto.load_and_parse(args.proto)
521
522 for output in args.output:
523 if output.endswith("ipc_protocol_generated.h"):
524 generate_h(output, p)
525 if output.endswith("ipc_client_generated.c"):
526 generate_client_c(output, p)
527 if output.endswith("ipc_client_generated.h"):
528 generate_client_h(output, p)
529 if output.endswith("ipc_server_generated.c"):
530 generate_server_c(output, p)
531 if output.endswith("ipc_server_generated.h"):
532 generate_server_header(output, p)
533 if output.endswith("structs.txt"):
534 generate_struct_names(output, p)
535
536
537if __name__ == "__main__":
538 main()