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