| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/python | |
| 2 # Copyright 2014 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 # pylint: disable=W0104,W0106,F0401,R0201 | |
| 7 | |
| 8 import errno | |
| 9 import os.path | |
| 10 import sys | |
| 11 | |
| 12 import interface | |
| 13 | |
| 14 | |
| 15 def ScriptDir(): | |
| 16 return os.path.dirname(os.path.abspath(__file__)) | |
| 17 | |
| 18 | |
| 19 def RepoRoot(): | |
| 20 # Go up 4 levels, to get out of "mojo/nacl/sfi/nacl_bindings_generator". | |
| 21 return os.path.dirname(os.path.dirname(os.path.dirname( | |
| 22 os.path.dirname(ScriptDir())))) | |
| 23 | |
| 24 | |
| 25 def _AddThirdPartyImportPath(): | |
| 26 sys.path.append(os.path.join(RepoRoot(), 'third_party')) | |
| 27 | |
| 28 | |
| 29 _AddThirdPartyImportPath() | |
| 30 import jinja2 | |
| 31 | |
| 32 loader = jinja2.FileSystemLoader(ScriptDir()) | |
| 33 jinja_env = jinja2.Environment(loader=loader, keep_trailing_newline=True) | |
| 34 | |
| 35 | |
| 36 # Accumulate lines of code with varying levels of indentation. | |
| 37 class CodeWriter(object): | |
| 38 def __init__(self): | |
| 39 self._lines = [] | |
| 40 self._margin = '' | |
| 41 self._margin_stack = [] | |
| 42 | |
| 43 def __lshift__(self, line): | |
| 44 self._lines.append((self._margin + line).rstrip()) | |
| 45 | |
| 46 def PushMargin(self): | |
| 47 self._margin_stack.append(self._margin) | |
| 48 self._margin += ' ' | |
| 49 | |
| 50 def PopMargin(self): | |
| 51 self._margin = self._margin_stack.pop() | |
| 52 | |
| 53 def GetValue(self): | |
| 54 return '\n'.join(self._lines).rstrip() + '\n' | |
| 55 | |
| 56 def Indent(self): | |
| 57 return Indent(self) | |
| 58 | |
| 59 | |
| 60 # Context handler that automatically indents and dedents a CodeWriter | |
| 61 class Indent(object): | |
| 62 def __init__(self, writer): | |
| 63 self._writer = writer | |
| 64 | |
| 65 def __enter__(self): | |
| 66 self._writer.PushMargin() | |
| 67 | |
| 68 def __exit__(self, type_, value, traceback): | |
| 69 self._writer.PopMargin() | |
| 70 | |
| 71 | |
| 72 def TemplateFile(name): | |
| 73 return os.path.join(os.path.dirname(__file__), name) | |
| 74 | |
| 75 | |
| 76 # Wraps comma separated lists as needed. | |
| 77 # TODO(teravest): Eliminate Wrap() and use "git cl format" when code is checked | |
| 78 # in. | |
| 79 def Wrap(pre, items, post): | |
| 80 complete = pre + ', '.join(items) + post | |
| 81 if len(complete) <= 80: | |
| 82 return [complete] | |
| 83 lines = [pre] | |
| 84 indent = ' ' | |
| 85 for i, item in enumerate(items): | |
| 86 if i < len(items) - 1: | |
| 87 lines.append(indent + item + ',') | |
| 88 else: | |
| 89 lines.append(indent + item + post) | |
| 90 return lines | |
| 91 | |
| 92 | |
| 93 def GeneratorWarning(): | |
| 94 return ('// WARNING this file was generated by %s\n// Do not edit by hand.' % | |
| 95 os.path.basename(__file__)) | |
| 96 | |
| 97 | |
| 98 # Untrusted library which thunks from the public Mojo API to the IRT interface | |
| 99 # implementing the public Mojo API. | |
| 100 def GenerateLibMojo(functions, common_vars, out): | |
| 101 template = jinja_env.get_template('libmojo.cc.tmpl') | |
| 102 | |
| 103 code = CodeWriter() | |
| 104 | |
| 105 for f in functions: | |
| 106 for line in Wrap('%s %s(' % (f.return_type, f.name), f.ParamList(), ') {'): | |
| 107 code << line | |
| 108 | |
| 109 with code.Indent(): | |
| 110 code << 'struct nacl_irt_mojo* irt_mojo = get_irt_mojo();' | |
| 111 code << 'if (!irt_mojo)' | |
| 112 with code.Indent(): | |
| 113 code << 'abort();' | |
| 114 code << 'return irt_mojo->%s(%s);' % ( | |
| 115 f.name, ', '.join([p.name for p in f.params])) | |
| 116 | |
| 117 code << '}' | |
| 118 code << '' | |
| 119 | |
| 120 body = code.GetValue() | |
| 121 text = template.render( | |
| 122 body=body, | |
| 123 **common_vars) | |
| 124 out.write(text) | |
| 125 | |
| 126 | |
| 127 # Parameters passed into trusted code are handled differently depending on | |
| 128 # details of the parameter. ParamImpl instances encapsulate these differences | |
| 129 # and are used to generate the code that transfers parameters across the | |
| 130 # untrusted/trusted boundary. | |
| 131 class ParamImpl(object): | |
| 132 def __init__(self, param): | |
| 133 self.param = param | |
| 134 | |
| 135 # Declare whatever variables are needed to handle this particular parameter. | |
| 136 def DeclareVars(self, code): | |
| 137 raise NotImplementedError(type(self)) | |
| 138 | |
| 139 # Convert the untrusted representation of the parameter into a trusted | |
| 140 # representation, such as a scalar value or a trusted pointer into the | |
| 141 # untrusted address space. | |
| 142 def ConvertParam(self): | |
| 143 raise NotImplementedError(type(self)) | |
| 144 | |
| 145 # For this particular parameter, what expression should be passed when | |
| 146 # invoking the trusted Mojo API function? | |
| 147 def CallParam(self): | |
| 148 raise NotImplementedError(type(self)) | |
| 149 | |
| 150 # After invoking the trusted Mojo API function, transfer data back into | |
| 151 # untrusted memory. Overriden for Out and InOut parameters. | |
| 152 def CopyOut(self, code): | |
| 153 pass | |
| 154 | |
| 155 # Converting array parameters needs to be defered until after the scalar | |
| 156 # parameter containing the size of the array has itself been converted. | |
| 157 def IsArray(self): | |
| 158 return False | |
| 159 | |
| 160 | |
| 161 class ScalarInputImpl(ParamImpl): | |
| 162 def DeclareVars(self, code): | |
| 163 code << '%s %s_value;' % (self.param.base_type, self.param.name) | |
| 164 | |
| 165 def ConvertParam(self): | |
| 166 p = self.param | |
| 167 return ('ConvertScalarInput(nap, params[%d], &%s_value)' % | |
| 168 (p.uid + 1, p.name)) | |
| 169 | |
| 170 def CallParam(self): | |
| 171 return '%s_value' % self.param.name | |
| 172 | |
| 173 | |
| 174 class ScalarOutputImpl(ParamImpl): | |
| 175 def DeclareVars(self, code): | |
| 176 code << '%s volatile* %s_ptr;' % (self.param.base_type, self.param.name) | |
| 177 code << '%s %s_value;' % (self.param.base_type, self.param.name) | |
| 178 | |
| 179 def ConvertParam(self): | |
| 180 p = self.param | |
| 181 return ('ConvertScalarOutput(nap, params[%d], %s, &%s_ptr)' % | |
| 182 (p.uid + 1, CBool(p.is_optional), p.name)) | |
| 183 | |
| 184 def CallParam(self): | |
| 185 name = self.param.name | |
| 186 expr = '&%s_value' % name | |
| 187 if self.param.is_optional: | |
| 188 expr = '%s_ptr ? %s : nullptr' % (name, expr) | |
| 189 return expr | |
| 190 | |
| 191 def CopyOut(self, code): | |
| 192 name = self.param.name | |
| 193 if self.param.is_struct: | |
| 194 # C++ errors when you try to copy a volatile struct pointer. | |
| 195 # (There are no default copy constructors for this case.) | |
| 196 # memcpy instead. | |
| 197 copy_stmt = ('memcpy_volatile_out(%s_ptr, &%s_value, sizeof(%s));' % | |
| 198 (name, name, self.param.base_type)) | |
| 199 else: | |
| 200 copy_stmt = '*%s_ptr = %s_value;' % (name, name) | |
| 201 | |
| 202 if self.param.is_optional: | |
| 203 code << 'if (%s_ptr) {' % (name) | |
| 204 with code.Indent(): | |
| 205 code << copy_stmt | |
| 206 code << '}' | |
| 207 else: | |
| 208 code << copy_stmt | |
| 209 | |
| 210 | |
| 211 class ScalarInOutImpl(ParamImpl): | |
| 212 def DeclareVars(self, code): | |
| 213 code << '%s volatile* %s_ptr;' % (self.param.base_type, self.param.name) | |
| 214 code << '%s %s_value;' % (self.param.base_type, self.param.name) | |
| 215 | |
| 216 def ConvertParam(self): | |
| 217 p = self.param | |
| 218 return ('ConvertScalarInOut(nap, params[%d], %s, &%s_value, &%s_ptr)' % | |
| 219 (p.uid + 1, CBool(p.is_optional), p.name, p.name)) | |
| 220 | |
| 221 def CallParam(self): | |
| 222 name = self.param.name | |
| 223 expr = '&%s_value' % name | |
| 224 if self.param.is_optional: | |
| 225 expr = '%s_ptr ? %s : nullptr' % (name, expr) | |
| 226 return expr | |
| 227 | |
| 228 def CopyOut(self, code): | |
| 229 name = self.param.name | |
| 230 if self.param.is_optional: | |
| 231 code << 'if (%s_ptr) {' % (name) | |
| 232 with code.Indent(): | |
| 233 code << '*%s_ptr = %s_value;' % (name, name) | |
| 234 code << '}' | |
| 235 else: | |
| 236 code << '*%s_ptr = %s_value;' % (name, name) | |
| 237 | |
| 238 | |
| 239 class PointerInputImpl(ParamImpl): | |
| 240 def DeclareVars(self, code): | |
| 241 code << '%s %s_value;' % (self.param.base_type, self.param.name) | |
| 242 | |
| 243 def ConvertParam(self): | |
| 244 p = self.param | |
| 245 return ('ConvertPointerInput(nap, params[%d], &%s_value)' % | |
| 246 (p.uid + 1, p.name)) | |
| 247 | |
| 248 def CallParam(self): | |
| 249 return '%s_value' % self.param.name | |
| 250 | |
| 251 | |
| 252 class PointerInOutImpl(ParamImpl): | |
| 253 def DeclareVars(self, code): | |
| 254 code << 'uint32_t volatile* %s_ptr;' % (self.param.name,) | |
| 255 code << '%s %s_value;' % (self.param.base_type, self.param.name) | |
| 256 | |
| 257 def ConvertParam(self): | |
| 258 p = self.param | |
| 259 return ('ConvertPointerInOut(nap, params[%d], %s, &%s_value, &%s_ptr)' % | |
| 260 (p.uid + 1, CBool(p.is_optional), p.name, p.name)) | |
| 261 | |
| 262 def CallParam(self): | |
| 263 name = self.param.name | |
| 264 expr = '&%s_value' % name | |
| 265 if self.param.is_optional: | |
| 266 expr = '%s_ptr ? %s : nullptr' % (name, expr) | |
| 267 return expr | |
| 268 | |
| 269 def CopyOut(self, code): | |
| 270 assert not self.param.is_optional | |
| 271 name = self.param.name | |
| 272 if self.param.is_optional: | |
| 273 code << 'if (%s_ptr) {' % (name) | |
| 274 with code.Indent(): | |
| 275 code << 'CopyOutPointer(nap, %s_value, %s_ptr);' % (name, name) | |
| 276 code << '}' | |
| 277 else: | |
| 278 code << 'CopyOutPointer(nap, %s_value, %s_ptr);' % (name, name) | |
| 279 | |
| 280 | |
| 281 class ArrayImpl(ParamImpl): | |
| 282 def DeclareVars(self, code): | |
| 283 code << '%s %s;' % (self.param.param_type, self.param.name) | |
| 284 | |
| 285 def ConvertParam(self): | |
| 286 p = self.param | |
| 287 if p.base_type == 'void': | |
| 288 element_size = '1' | |
| 289 else: | |
| 290 element_size = 'sizeof(*%s)' % p.name | |
| 291 | |
| 292 return ('ConvertArray(nap, params[%d], %s, %s, %s, &%s)' % | |
| 293 (p.uid + 1, p.size + '_value', element_size, CBool(p.is_optional), | |
| 294 p.name)) | |
| 295 | |
| 296 def CallParam(self): | |
| 297 return self.param.name | |
| 298 | |
| 299 def IsArray(self): | |
| 300 return True | |
| 301 | |
| 302 | |
| 303 class ExtensibleStructInputImpl(ParamImpl): | |
| 304 def DeclareVars(self, code): | |
| 305 code << '%s %s;' % (self.param.param_type, self.param.name) | |
| 306 | |
| 307 def ConvertParam(self): | |
| 308 p = self.param | |
| 309 return ('ConvertExtensibleStructInput(nap, params[%d], %s, &%s)' % | |
| 310 (p.uid + 1, CBool(p.is_optional), p.name)) | |
| 311 | |
| 312 def CallParam(self): | |
| 313 return self.param.name | |
| 314 | |
| 315 | |
| 316 # We can handle extensible out structs just like we handle output arrays, except | |
| 317 # that the (input buffer) size is always in bytes. | |
| 318 class ExtensibleStructOutputImpl(ParamImpl): | |
| 319 def DeclareVars(self, code): | |
| 320 code << '%s %s;' % (self.param.param_type, self.param.name) | |
| 321 | |
| 322 def ConvertParam(self): | |
| 323 p = self.param | |
| 324 return ('ConvertArray(nap, params[%d], %s, %s, %s, &%s)' % | |
| 325 (p.uid + 1, p.size + '_value', '1', CBool(p.is_optional), p.name)) | |
| 326 | |
| 327 def CallParam(self): | |
| 328 return self.param.name | |
| 329 | |
| 330 def IsArray(self): | |
| 331 return True | |
| 332 | |
| 333 | |
| 334 def ImplForParam(p): | |
| 335 if p.IsScalar(): | |
| 336 if p.is_output: | |
| 337 if p.is_pointer: | |
| 338 return PointerInOutImpl(p) | |
| 339 elif p.is_input: | |
| 340 return ScalarInOutImpl(p) | |
| 341 else: | |
| 342 if p.is_always_written: | |
| 343 return ScalarOutputImpl(p) | |
| 344 else: | |
| 345 # Mojo defines that some of its outputs will not be set in specific | |
| 346 # cases. To avoid the complexity of determining if the output was set | |
| 347 # by Mojo, copy the output's current value (possibly junk) and copy it | |
| 348 # back to untrusted memory afterwards. | |
| 349 return ScalarInOutImpl(p) | |
| 350 else: | |
| 351 if p.is_pointer: | |
| 352 return PointerInputImpl(p) | |
| 353 else: | |
| 354 return ScalarInputImpl(p) | |
| 355 elif p.is_array: | |
| 356 return ArrayImpl(p) | |
| 357 elif p.is_struct: | |
| 358 if p.is_input and not p.is_output and p.is_extensible: | |
| 359 return ExtensibleStructInputImpl(p) | |
| 360 if p.is_output and not p.is_input and p.is_extensible: | |
| 361 return ExtensibleStructOutputImpl(p) | |
| 362 if not p.is_input and p.is_output and not p.is_extensible: | |
| 363 return ScalarOutputImpl(p) | |
| 364 assert False, p.name | |
| 365 | |
| 366 | |
| 367 def CBool(value): | |
| 368 return 'true' if value else 'false' | |
| 369 | |
| 370 | |
| 371 # A trusted wrapper that validates the arguments passed from untrusted code | |
| 372 # before passing them to the underlying public Mojo API. | |
| 373 def GenerateMojoSyscall(functions, common_vars, out): | |
| 374 template = jinja_env.get_template('mojo_syscall.cc.tmpl') | |
| 375 | |
| 376 code = CodeWriter() | |
| 377 code.PushMargin() | |
| 378 | |
| 379 for f in functions: | |
| 380 is_implemented = not f.broken_in_nacl | |
| 381 | |
| 382 impls = [ImplForParam(p) for p in f.params] | |
| 383 impls.append(ImplForParam(f.result_param)) | |
| 384 | |
| 385 code << 'case %d:' % f.uid | |
| 386 | |
| 387 if not is_implemented: | |
| 388 with code.Indent(): | |
| 389 code << 'fprintf(stderr, "%s not implemented\\n");' % f.name | |
| 390 code << 'return -1;' | |
| 391 continue | |
| 392 | |
| 393 code.PushMargin() | |
| 394 | |
| 395 code << '{' | |
| 396 | |
| 397 with code.Indent(): | |
| 398 num_params = len(f.params) + 2 | |
| 399 code << 'if (num_params != %d) {' % num_params | |
| 400 with code.Indent(): | |
| 401 code << 'return -1;' | |
| 402 code << '}' | |
| 403 | |
| 404 # Declare temporaries. | |
| 405 for impl in impls: | |
| 406 impl.DeclareVars(code) | |
| 407 | |
| 408 def ConvertParam(code, impl): | |
| 409 code << 'if (!%s) {' % impl.ConvertParam() | |
| 410 with code.Indent(): | |
| 411 code << 'return -1;' | |
| 412 code << '}' | |
| 413 | |
| 414 code << '{' | |
| 415 with code.Indent(): | |
| 416 code << 'ScopedCopyLock copy_lock(nap);' | |
| 417 # Convert and validate pointers in two passes. | |
| 418 # Arrays cannot be validated until the size parameter has been | |
| 419 # converted. | |
| 420 for impl in impls: | |
| 421 if not impl.IsArray(): | |
| 422 ConvertParam(code, impl) | |
| 423 for impl in impls: | |
| 424 if impl.IsArray(): | |
| 425 ConvertParam(code, impl) | |
| 426 code << '}' | |
| 427 code << '' | |
| 428 | |
| 429 # Call | |
| 430 getParams = [impl.CallParam() for impl in impls[:-1]] | |
| 431 callTarget = f.name | |
| 432 # Redirect to namespaced functions. | |
| 433 if callTarget.startswith("Mojo"): | |
| 434 callTarget = callTarget[:4] + 'SystemImpl' + callTarget[4:] | |
| 435 getParams = ['g_mojo_system'] + getParams | |
| 436 code << 'result_value = %s(%s);' % (callTarget, ', '.join(getParams)) | |
| 437 code << '' | |
| 438 | |
| 439 # Write outputs | |
| 440 code << '{' | |
| 441 with code.Indent(): | |
| 442 code << 'ScopedCopyLock copy_lock(nap);' | |
| 443 for impl in impls: | |
| 444 impl.CopyOut(code) | |
| 445 code << '}' | |
| 446 code << '' | |
| 447 | |
| 448 code << 'return 0;' | |
| 449 code << '}' | |
| 450 | |
| 451 code.PopMargin() | |
| 452 | |
| 453 body = code.GetValue() | |
| 454 text = template.render( | |
| 455 body=body, | |
| 456 **common_vars) | |
| 457 out.write(text) | |
| 458 | |
| 459 | |
| 460 # A header declaring the IRT interface for accessing Mojo functions. | |
| 461 def GenerateMojoIrtHeader(functions, common_vars, out): | |
| 462 template = jinja_env.get_template('mojo_irt.h.tmpl') | |
| 463 code = CodeWriter() | |
| 464 | |
| 465 code << 'struct nacl_irt_mojo {' | |
| 466 with code.Indent(): | |
| 467 for f in functions: | |
| 468 for line in Wrap('%s (*%s)(' % (f.return_type, f.name), | |
| 469 f.ParamList(), | |
| 470 ');'): | |
| 471 code << line | |
| 472 | |
| 473 code << '};' | |
| 474 | |
| 475 body = code.GetValue() | |
| 476 | |
| 477 text = template.render( | |
| 478 body=body, | |
| 479 **common_vars) | |
| 480 out.write(text) | |
| 481 | |
| 482 # IRT interface which implements the Mojo public API. | |
| 483 def GenerateMojoIrtImplementation(functions, common_vars, out): | |
| 484 template = jinja_env.get_template('mojo_irt.c.tmpl') | |
| 485 code = CodeWriter() | |
| 486 | |
| 487 for f in functions: | |
| 488 for line in Wrap('static %s irt_%s(' % (f.return_type, f.name), | |
| 489 f.ParamList(), | |
| 490 ') {'): | |
| 491 code << line | |
| 492 | |
| 493 # 2 extra parameters: message ID and return value. | |
| 494 num_params = len(f.params) + 2 | |
| 495 | |
| 496 with code.Indent(): | |
| 497 code << 'uint32_t params[%d];' % num_params | |
| 498 return_type = f.result_param.base_type | |
| 499 if return_type == 'MojoResult': | |
| 500 default = 'MOJO_RESULT_INVALID_ARGUMENT' | |
| 501 elif return_type == 'MojoTimeTicks': | |
| 502 default = '0' | |
| 503 else: | |
| 504 raise Exception('Unhandled return type: ' + return_type) | |
| 505 code << '%s %s = %s;' % (return_type, f.result_param.name, default) | |
| 506 | |
| 507 # Message ID | |
| 508 code << 'params[0] = %d;' % f.uid | |
| 509 # Parameter pointers | |
| 510 cast_template = 'params[%d] = (uint32_t)(%s);' | |
| 511 for p in f.params: | |
| 512 ptr = p.name | |
| 513 if p.IsPassedByValue(): | |
| 514 ptr = '&' + ptr | |
| 515 code << cast_template % (p.uid + 1, ptr) | |
| 516 # Return value pointer | |
| 517 code << cast_template % (num_params - 1, '&' + f.result_param.name) | |
| 518 | |
| 519 code << 'DoMojoCall(params, sizeof(params));' | |
| 520 code << 'return %s;' % f.result_param.name | |
| 521 | |
| 522 # Add body here. | |
| 523 code << "};" | |
| 524 code << "\n" | |
| 525 | |
| 526 # Now we've emitted all the functions, but we still need the struct | |
| 527 # definition. | |
| 528 code << 'struct nacl_irt_mojo kIrtMojo = {' | |
| 529 for f in functions: | |
| 530 with code.Indent(): | |
| 531 code << '&irt_%s,' % f.name | |
| 532 code << '};' | |
| 533 | |
| 534 body = code.GetValue() | |
| 535 | |
| 536 text = template.render( | |
| 537 body=body, | |
| 538 **common_vars) | |
| 539 out.write(text) | |
| 540 | |
| 541 | |
| 542 def OutFile(dir_path, name): | |
| 543 if not os.path.exists(dir_path): | |
| 544 try: | |
| 545 os.makedirs(dir_path) | |
| 546 except OSError as e: | |
| 547 # There may have been a race to create this directory. | |
| 548 if e.errno != errno.EEXIST: | |
| 549 raise | |
| 550 full_path = os.path.join(dir_path, name) | |
| 551 print full_path | |
| 552 return open(full_path, 'w') | |
| 553 | |
| 554 | |
| 555 def main(): | |
| 556 root_dir = RepoRoot() | |
| 557 | |
| 558 platform_dir = 'mojo/public/platform/nacl' | |
| 559 bindings_dir = 'mojo/nacl/sfi/nacl_bindings' | |
| 560 | |
| 561 full_platform_dir = os.path.join(root_dir, platform_dir) | |
| 562 full_bindings_dir = os.path.join(root_dir, bindings_dir) | |
| 563 | |
| 564 common_vars = dict( | |
| 565 generator_warning=GeneratorWarning(), | |
| 566 platform_dir=platform_dir, | |
| 567 platform_dir_header_path=platform_dir.replace("/", "_").upper(), | |
| 568 bindings_dir=bindings_dir, | |
| 569 ) | |
| 570 | |
| 571 mojo = interface.MakeInterface() | |
| 572 | |
| 573 out = OutFile(full_platform_dir, 'libmojo.cc') | |
| 574 GenerateLibMojo(mojo.functions, common_vars, out) | |
| 575 | |
| 576 out = OutFile(full_bindings_dir, 'mojo_syscall.cc') | |
| 577 GenerateMojoSyscall(mojo.functions, common_vars, out) | |
| 578 | |
| 579 out = OutFile(full_platform_dir, 'mojo_irt.h') | |
| 580 GenerateMojoIrtHeader(mojo.functions, common_vars, out) | |
| 581 | |
| 582 out = OutFile(full_bindings_dir, 'mojo_irt.c') | |
| 583 GenerateMojoIrtImplementation(mojo.functions, common_vars, out) | |
| 584 | |
| 585 if __name__ == '__main__': | |
| 586 main() | |
| OLD | NEW |