{"id":5810,"library":"pyobjc-framework-metal","title":"PyObjC Metal Framework","description":"Wrappers for the “Metal” framework on macOS. PyObjC allows full-featured Cocoa applications to be written in pure Python, bridging Python and Objective-C. This specific package provides bindings for Apple's Metal framework, enabling GPU-accelerated computing and graphics. It is actively maintained with frequent updates tied to macOS SDK releases, currently at version 12.1.","status":"active","version":"12.1","language":"en","source_language":"en","source_url":"https://github.com/ronaldoussoren/pyobjc","tags":["macos","objc","metal","gpu","graphics","apple","accelerated-computing"],"install":[{"cmd":"pip install pyobjc-framework-metal","lang":"bash","label":"Install latest version"}],"dependencies":[{"reason":"Provides the core bridge between Python and Objective-C, essential for all PyObjC framework wrappers.","package":"pyobjc-core"}],"imports":[{"note":"The Metal framework bindings are accessed directly through the 'Metal' package.","symbol":"Metal","correct":"import Metal"}],"quickstart":{"code":"import Metal\nimport objc\nimport struct\n\ndef run_metal_kernel():\n    # 1. Get the default Metal device\n    device = Metal.MTLCreateSystemDefaultDevice()\n    if device is None:\n        print(\"Error: No Metal device found.\")\n        return\n\n    print(f\"Using Metal device: {device.name()}\")\n\n    # 2. Create a simple Metal shader (MSL) source\n    # This kernel adds two numbers\n    kernel_source = \"\"\"\n        #include <metal_stdlib>\n\n        kernel void add_numbers(\n            device const float *inA [[buffer(0)]],\n            device const float *inB [[buffer(1)]],\n            device float *out [[buffer(2)]],\n            uint id [[thread_position_in_grid]])\n        {\n            out[id] = inA[id] + inB[id];\n        }\n    \"\"\"\n\n    # 3. Create a library from the source\n    # Using newLibraryWithSource_options_error_ instead of newLibraryWithSource_options_error\n    # as PyObjC usually appends '_' to methods with Objective-C error pointers.\n    error_ptr = objc.nil\n    library = device.newLibraryWithSource_options_error_(kernel_source, objc.nil, error_ptr)\n\n    if library is None:\n        # Check if error_ptr now points to an actual error object\n        if error_ptr and error_ptr[0] is not objc.nil: # error_ptr is a C array of MTL_Error* in PyObjC\n            error_obj = error_ptr[0]\n            print(f\"Failed to create Metal library: {error_obj.localizedDescription()}\")\n        else:\n            print(\"Failed to create Metal library (unknown error).\")\n        return\n\n    # 4. Get the kernel function\n    function = library.newFunctionWithName_(\"add_numbers\")\n    if function is None:\n        print(\"Error: Failed to find kernel function 'add_numbers'.\")\n        return\n\n    # 5. Create a compute pipeline state\n    pipeline_state = device.newComputePipelineStateWithFunction_error_(function, objc.nil)\n    if pipeline_state is None:\n        print(\"Error: Failed to create compute pipeline state.\")\n        return\n\n    # 6. Prepare data\n    data_size = 10 * struct.calcsize('f') # 10 floats\n    input_a = [float(i) for i in range(10)]\n    input_b = [float(i * 2) for i in range(10)]\n    output_data = [0.0] * 10\n\n    # Create Metal buffers\n    buffer_a = device.newBufferWithBytes_length_options_(bytes(struct.pack('f'*10, *input_a)), data_size, Metal.MTLResourceStorageModeManaged)\n    buffer_b = device.newBufferWithBytes_length_options_(bytes(struct.pack('f'*10, *input_b)), data_size, Metal.MTLResourceStorageModeManaged)\n    buffer_out = device.newBufferWithLength_options_(data_size, Metal.MTLResourceStorageModeManaged)\n\n    # 7. Create a command queue\n    command_queue = device.newCommandQueue_()\n    if command_queue is None:\n        print(\"Error: Failed to create command queue.\")\n        return\n\n    # 8. Create a command buffer\n    command_buffer = command_queue.commandBuffer_()\n\n    # 9. Create a compute command encoder\n    compute_encoder = command_buffer.computeCommandEncoder_()\n    compute_encoder.setComputePipelineState_(pipeline_state)\n    compute_encoder.setBuffer_offset_atIndex_(buffer_a, 0, 0)\n    compute_encoder.setBuffer_offset_atIndex_(buffer_b, 0, 1)\n    compute_encoder.setBuffer_offset_atIndex_(buffer_out, 0, 2)\n\n    # 10. Dispatch threads\n    grid_size = Metal.MTLSizeMake(10, 1, 1)\n    thread_group_size = Metal.MTLSizeMake(min(10, pipeline_state.maxTotalThreadsPerThreadgroup()), 1, 1)\n    compute_encoder.dispatchThreads_threadsPerThreadgroup_(grid_size, thread_group_size)\n\n    compute_encoder.endEncoding_()\n\n    # 11. Commit and wait for completion\n    command_buffer.commit_()\n    command_buffer.waitUntilCompleted_()\n\n    # 12. Read results back to CPU (if using managed storage mode)\n    buffer_out.didModifyRange_(Metal.NSMakeRange(0, data_size))\n    result_bytes = buffer_out.contents().tobytes()\n    result = struct.unpack('f'*10, result_bytes)\n\n    print(\"Input A:\", input_a)\n    print(\"Input B:\", input_b)\n    print(\"Output (A+B):\", list(result))\n\n    expected_output = [input_a[i] + input_b[i] for i in range(10)]\n    if all(abs(r - e) < 1e-5 for r, e in zip(result, expected_output)):\n        print(\"✅ Output matches expected values.\")\n    else:\n        print(\"❌ Output does not match expected values.\")\n\nif __name__ == '__main__':\n    run_metal_kernel()","lang":"python","description":"This quickstart demonstrates how to execute a basic Metal compute kernel using `pyobjc-framework-metal`. It initializes a Metal device, compiles a simple shader to add two arrays of numbers, sets up input/output buffers, dispatches the compute command, and reads the results back."},"warnings":[{"fix":"Upgrade Python to 3.10 or later, or use an older PyObjC version compatible with your Python environment.","message":"PyObjC 12.0 dropped support for Python 3.9. PyObjC 11.0 dropped support for Python 3.8. Users should ensure they are on a supported Python version (>=3.10 for current 12.1).","severity":"breaking","affected_versions":">=11.0"},{"fix":"Review code interacting with Objective-C 'init' methods for potential reference counting issues, especially if manual memory management was assumed. Refer to Apple's ARC documentation.","message":"PyObjC 11.1 updated its behavior for initializer methods (those in the 'init' family) to align with `clang`'s Automatic Reference Counting (ARC) documentation. These methods now correctly steal a reference to 'self' and return a new reference, which may change memory management expectations for code relying on previous PyObjC behavior.","severity":"breaking","affected_versions":">=11.1"},{"fix":"Carefully test custom `__new__` and `__init__` implementations in Python subclasses of Objective-C classes, especially when upgrading from versions prior to 10.3.","message":"PyObjC 10.3 initially removed support for calling `__init__` when a user implements `__new__` in a Python subclass of an Objective-C class. While 10.3.1 reintroduced this capability for specific scenarios, developers should be aware that custom `__new__` implementations might interact unexpectedly with `__init__` due to underlying bridge changes.","severity":"gotcha","affected_versions":">=10.3"},{"fix":"Avoid relying on KVO for Python subclasses of `NSProxy`. If KVO-like behavior is needed, implement it manually within the Python class.","message":"Starting with PyObjC 12.1, Key-Value Observing (KVO) usage is automatically disabled for subclasses of `NSProxy` defined in Python. This change aims to prevent `SystemError` crashes that could occur in previous versions.","severity":"gotcha","affected_versions":">=12.1"},{"fix":"Rely on `pyobjc-framework-metal`'s abstraction for accessing Metal functionality. If direct `ctypes` loading is required, adapt paths to check `/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Metal.framework` or use `platform.mac_ver()` to determine the correct path dynamically.","message":"On macOS Sonoma (14) and later, the default path for `Metal.framework` has changed. This primarily affects low-level interactions that directly load the framework via `ctypes.CDLL` (e.g., `/System/Library/Frameworks/Metal.framework/Metal`), rather than through PyObjC's abstraction. Direct path lookups may fail.","severity":"gotcha","affected_versions":"macOS 14+"},{"fix":"If using Python 3.13+ with free-threading, test PyObjC interactions thoroughly and refer to the latest PyObjC documentation for compatibility and known issues.","message":"PyObjC 11.0 introduced experimental support for Python's free-threading (PEP 703) in Python 3.13, but PyObjC 10.3 explicitly stated it did not support free-threading in Python 3.13. This is an evolving area, and stability with free-threading may vary across versions and require careful testing.","severity":"gotcha","affected_versions":">=10.3"}],"env_vars":null,"last_verified":"2026-04-14T00:00:00.000Z","next_check":"2026-07-13T00:00:00.000Z"}