// Copyright 2023 DeepMind Technologies Limited // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #import #import #import #import #import #import #import #import #import #import #import #import #import #import extern "C" { extern char **environ; // for execve // Wrap Objective-C Cocoa calls into C-style functions with default visibility, // so that we can dlsym and call them from Python via ctypes. __attribute__((used)) void mjpython_hide_dock_icon() { [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; } __attribute__((used)) void mjpython_show_dock_icon() { [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; } } // extern "C" namespace { // 16MiB is the default Python thread stack size on macOS as of Python 3.11 // https://bugs.python.org/issue18075 constexpr rlim_t kThreadStackSize = 0x1000000; struct { #define CPYTHON_FN(fname) decltype(&::fname) fname // go/keep-sorted start CPYTHON_FN(PyConfig_Clear); CPYTHON_FN(PyConfig_InitPythonConfig); CPYTHON_FN(PyConfig_SetBytesArgv); CPYTHON_FN(PyGILState_Ensure); CPYTHON_FN(PyGILState_Release); CPYTHON_FN(PyRun_SimpleStringFlags); CPYTHON_FN(Py_FinalizeEx); CPYTHON_FN(Py_InitializeFromConfig); CPYTHON_FN(Py_RunMain); // go/keep-sorted end #undef CPYTHON_FN } cpython; std::atomic_bool py_initialized = false; struct Args { int argc; char** argv; }; // The Python main thread (distinct from the macOS main thread, which executes the main function). void* mjpython_pymain(void* vargs) { Args* args = static_cast(vargs); PyGILState_STATE gil; // Initialize the Python interpreter. PyConfig config; cpython.PyConfig_InitPythonConfig(&config); cpython.PyConfig_SetBytesArgv(&config, args->argc, args->argv); cpython.Py_InitializeFromConfig(&config); cpython.PyConfig_Clear(&config); // Set up the condition variable to pass control back to the macOS main thread. gil = cpython.PyGILState_Ensure(); cpython.PyRun_SimpleStringFlags(R"( def _mjpython_make_cond(): # Don't pollute the global namespace. global _mjpython_make_cond del _mjpython_make_cond import threading global cond cond = threading.Condition() _mjpython_make_cond() )", nullptr); py_initialized.store(true); // Wait until GLFW is initialized on macOS main thread, set up the queue and an atexit hook // to enqueue a termination flag upon exit. cpython.PyRun_SimpleStringFlags(R"( def _mjpython_init(): # Don't pollute the global namespace. global _mjpython_init del _mjpython_init import atexit import threading # The mujoco.viewer module should only be imported after glfw.init() in the macOS main thread. with cond: cond.wait() import mujoco.viewer # Similar to a queue.Queue(maxsize=1), but where only one active task is allowed at a time. # With queue.Queue(1), another item is allowed to be enqueued before task_done is called. class _MjPythonImpl(mujoco.viewer._MjPythonBase): # Termination statuses NOT_TERMINATED = 0 TERMINATION_REQUESTED = 1 TERMINATION_ACCEPTED = 2 TERMINATED = 3 def __init__(self): self._cond = threading.Condition() self._task = None self._termination = self.__class__.NOT_TERMINATED self._busy = False def launch_on_ui_thread( self, model, data, handle_return, key_callback, show_left_ui, show_right_ui, ): with self._cond: if self._busy or self._task is not None: raise RuntimeError('another MuJoCo viewer is already open') else: self._task = ( model, data, handle_return, key_callback, show_left_ui, show_right_ui, ) self._cond.notify() def terminate(self): with self._cond: self._termination = self.__class__.TERMINATION_REQUESTED self._cond.notify() self._cond.wait_for( lambda: self._termination == self.__class__.TERMINATED) def get(self): with self._cond: self._cond.wait_for( lambda: self._task is not None or self._termination) if self._termination: if self._termination == self.__class__.TERMINATION_REQUESTED: self._termination = self.__class__.TERMINATION_ACCEPTED return None task = self._task self._busy = True self._task = None return task def done(self): with self._cond: self._busy = False if self._termination == self.__class__.TERMINATION_ACCEPTED: self._termination = self.__class__.TERMINATED self._cond.notify() mujoco.viewer._MJPYTHON = _MjPythonImpl() atexit.register(mujoco.viewer._MJPYTHON.terminate) with cond: cond.notify() _mjpython_init() )", nullptr); // Run the Python interpreter main loop. cpython.Py_RunMain(); // Tear down the interpreter. cpython.Py_FinalizeEx(); return nullptr; } } // namespace int main(int argc, char** argv) { const char* libpython_path = getenv("MJPYTHON_LIBPYTHON"); if (!libpython_path || !libpython_path[0]) { std::cerr << "This binary must be launched via the mjpython.py script.\n"; return EXIT_FAILURE; } // Enlarge the stack if necessary to match what Python normally expects to have when launching // a new thread. rlimit stack; if (getrlimit(RLIMIT_STACK, &stack)) { std::cerr << "getrlimit failed to query stack size with error code " << errno << " (" << std::strerror(errno) << ")\n"; std::cerr << "continuing anyway but crashes may occur if the stack is too small\n"; } else if (stack.rlim_cur < kThreadStackSize && stack.rlim_cur < stack.rlim_max) { auto rlim_old = stack.rlim_cur; stack.rlim_cur = std::min(kThreadStackSize, stack.rlim_max); if (setrlimit(RLIMIT_STACK, &stack)) { std::cerr << "setrlimit failed to increase stack size with error code " << errno << " (" << std::strerror(errno) << ")\n"; std::cerr << "continuing anyway with stack size " << rlim_old << " but crashes may occur\n"; } else { // re-exec the binary so that the new stack size takes effect std::uint32_t path_size = 0; _NSGetExecutablePath(nullptr, &path_size); std::vector path(path_size); if (_NSGetExecutablePath(path.data(), &path_size)) { std::cerr << "unexpected error from _NSGetExecutablePath, continuing anyway\n"; } else { execve(path.data(), argv, environ); } } } // Resolve libpython at runtime to prevent linking against the wrong dylib. The correct libpython // path is passed from a Python trampoline script, which ran inside the desired interpreter and // exec'd this binary. void* libpython = dlopen(libpython_path, RTLD_NOW | RTLD_GLOBAL); if (!libpython) { std::cerr << "failed to dlopen path '" << libpython_path << "': " << dlerror() << "\n"; return EXIT_FAILURE; } // Look up required CPython API functions from table of symbols already loaded into the process. #define CPYTHON_INITFN(fname) \ { \ cpython.fname = reinterpret_cast(dlsym(libpython, #fname)); \ if (!cpython.fname) { \ std::cerr << "failed to dlsym '" << #fname << "': " << dlerror() << "\n"; \ return EXIT_FAILURE; \ } \ } // go/keep-sorted start CPYTHON_INITFN(PyConfig_Clear); CPYTHON_INITFN(PyConfig_InitPythonConfig); CPYTHON_INITFN(PyConfig_SetBytesArgv); CPYTHON_INITFN(PyGILState_Ensure); CPYTHON_INITFN(PyGILState_Release); CPYTHON_INITFN(PyRun_SimpleStringFlags); CPYTHON_INITFN(Py_FinalizeEx); CPYTHON_INITFN(Py_InitializeFromConfig); CPYTHON_INITFN(Py_RunMain); // go/keep-sorted end #undef CPYTHON_INITFN // Package up argc and argv together to pass to pthread_create. Args args{argc, argv}; #define PTHREAD_CHECKED(func, ...) \ { \ int result = func(__VA_ARGS__); \ if (result) { \ std::cerr << #func << " failed with " << result << "(" << std::strerror(result) << ")\n"; \ return EXIT_FAILURE; \ } \ } // Configure the new thread with the correct stack size; pthread_attr_t pthread_attr; PTHREAD_CHECKED(pthread_attr_init, &pthread_attr); PTHREAD_CHECKED(pthread_attr_setstacksize, &pthread_attr, stack.rlim_cur); // Create a thread to be used as the "Python main thread". pthread_t pymain_thread; PTHREAD_CHECKED(pthread_create, &pymain_thread, &pthread_attr, &mjpython_pymain, &args); pthread_attr_destroy(&pthread_attr); #undef PTHREAD_CHECKED // Busy-wait until Python interpreter is initialized. while (!py_initialized.load()) {} // Initialize GLFW on the macOS main thread, yield control to Python main thread and wait for it // to finish setting up _MJPYTHON, then serve incoming viewer launch requests. PyGILState_STATE gil = cpython.PyGILState_Ensure(); cpython.PyRun_SimpleStringFlags(R"( def _mjpython_main(): # Don't pollute the global namespace. global _mjpython_main del _mjpython_main import ctypes # GLFW must be initialized on the OS main thread (i.e. here). import glfw import mujoco.viewer glfw.init() glfw.poll_events() ctypes.CDLL(None).mjpython_hide_dock_icon() # Wait for Python main thread to finish setting up _MJPYTHON global cond with cond: cond.notify() cond.wait() del cond while True: try: # Wait for an incoming payload. task = mujoco.viewer._MJPYTHON.get() # None means that we are exiting. if task is None: glfw.terminate() break # Otherwise, launch the viewer. model, data, handle_return, key_callback, show_left_ui, show_right_ui = task ctypes.CDLL(None).mjpython_show_dock_icon() mujoco.viewer._launch_internal( model, data, run_physics_thread=False, handle_return=handle_return, key_callback=key_callback, show_left_ui=show_left_ui, show_right_ui=show_right_ui, ) ctypes.CDLL(None).mjpython_hide_dock_icon() finally: mujoco.viewer._MJPYTHON.done() _mjpython_main() )", nullptr); cpython.PyGILState_Release(gil); // Tear everything down. pthread_join(pymain_thread, nullptr); dlclose(libpython); }