12

Is there a way to dynamically call an Objective C function from Python?

For example, On the mac I would like to call this Objective C function

[NSSpeechSynthesizer availableVoices]

without having to precompile any special Python wrapper module.

Mark Harrison
  • 297,451
  • 125
  • 333
  • 465

4 Answers4

25

As others have mentioned, PyObjC is the way to go. But, for completeness' sake, here's how you can do it with ctypes, in case you need it to work on versions of OS X prior to 10.5 that do not have PyObjC installed:

import ctypes
import ctypes.util

# Need to do this to load the NSSpeechSynthesizer class, which is in AppKit.framework
appkit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('AppKit'))
objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc'))

objc.objc_getClass.restype = ctypes.c_void_p
objc.sel_registerName.restype = ctypes.c_void_p
objc.objc_msgSend.restype = ctypes.c_void_p
objc.objc_msgSend.argtypes = [ctypes.c_void_p, ctypes.c_void_p]

# Without this, it will still work, but it'll leak memory
NSAutoreleasePool = objc.objc_getClass('NSAutoreleasePool')
pool = objc.objc_msgSend(NSAutoreleasePool, objc.sel_registerName('alloc'))
pool = objc.objc_msgSend(pool, objc.sel_registerName('init'))

NSSpeechSynthesizer = objc.objc_getClass('NSSpeechSynthesizer')
availableVoices = objc.objc_msgSend(NSSpeechSynthesizer, objc.sel_registerName('availableVoices'))

count = objc.objc_msgSend(availableVoices, objc.sel_registerName('count'))
voiceNames = [
  ctypes.string_at(
    objc.objc_msgSend(
      objc.objc_msgSend(availableVoices, objc.sel_registerName('objectAtIndex:'), i),
      objc.sel_registerName('UTF8String')))
  for i in range(count)]
print voiceNames

objc.objc_msgSend(pool, objc.sel_registerName('release'))

It ain't pretty, but it gets the job done. The final list of available names is stored in the voiceNames variable above.

2012-4-28 Update: Fixed to work in 64-bit Python builds by making sure all parameters and return types are passed as pointers instead of 32-bit integers.

Adam Rosenfield
  • 390,455
  • 97
  • 512
  • 589
  • 1
    Hello, this crashes on 10.6 - any idea why? – Ecir Hana Apr 28 '12 at 09:24
  • 4
    @Ecir: Thanks for the tip, I'm surprised the original code worked in the first place. The problem was that all of the pointers (class pointers and instance pointers) were getting truncated to 32 bits due to the way ctypes worked, which resulted in crashes. To fix that, I changed the code to set all of the result types and argument types to explicitly be pointers. – Adam Rosenfield Apr 29 '12 at 01:10
  • 1
    Seems mavericks no longer ships with pyobjc so this answer becomes relevant again! – boxed Nov 02 '13 at 15:33
10

Since OS X 10.5, OS X has shipped with the PyObjC bridge, a Python-Objective-C bridge. It uses the BridgeSupport framework to map Objective-C frameworks to Python. Unlike, MacRuby, PyObjC is a classical bridge--there is a proxy object on the python side for each ObjC object and visa versa. The bridge is pretty seamless, however, and its possible to write entire apps in PyObjC (Xcode has some basic PyObjC support, and you can download the app and file templates for Xcode from the PyObjC SVN at the above link). Many folks use it for utilities or for app-scripting/plugins. Apple's developer site also has an introduction to developing Cocoa applications with Python via PyObjC which is slightly out of date, but may be a good overview for you.

In your case, the following code will call [NSSpeechSynthesizer availableVoices]:

from AppKit import NSSpeechSynthesizer

NSSpeechSynthesizer.availableVoices()

which returns

(
    "com.apple.speech.synthesis.voice.Agnes",
    "com.apple.speech.synthesis.voice.Albert",
    "com.apple.speech.synthesis.voice.Alex",
    "com.apple.speech.synthesis.voice.BadNews",
    "com.apple.speech.synthesis.voice.Bahh",
    "com.apple.speech.synthesis.voice.Bells",
    "com.apple.speech.synthesis.voice.Boing",
    "com.apple.speech.synthesis.voice.Bruce",
    "com.apple.speech.synthesis.voice.Bubbles",
    "com.apple.speech.synthesis.voice.Cellos",
    "com.apple.speech.synthesis.voice.Deranged",
    "com.apple.speech.synthesis.voice.Fred",
    "com.apple.speech.synthesis.voice.GoodNews",
    "com.apple.speech.synthesis.voice.Hysterical",
    "com.apple.speech.synthesis.voice.Junior",
    "com.apple.speech.synthesis.voice.Kathy",
    "com.apple.speech.synthesis.voice.Organ",
    "com.apple.speech.synthesis.voice.Princess",
    "com.apple.speech.synthesis.voice.Ralph",
    "com.apple.speech.synthesis.voice.Trinoids",
    "com.apple.speech.synthesis.voice.Vicki",
    "com.apple.speech.synthesis.voice.Victoria",
    "com.apple.speech.synthesis.voice.Whisper",
    "com.apple.speech.synthesis.voice.Zarvox"
)

(a bridged NSCFArray) on my SL machine.

Barry Wark
  • 107,306
  • 24
  • 181
  • 206
  • 1
    The introduction link you shared is broken. Are you perhaps referring to this? https://opensource.apple.com/source/pyobjc/pyobjc-47/pyobjc/pyobjc-core-2.5.1/Doc/sphinx_build/html/tutorials/firstapp.html – kcstricks Jun 12 '20 at 20:03
4

Mac OS X from 10.5 onward has shipped with Python and the objc module that will let you do what you want.

An example:

from Foundation import *

thing = NSKeyedUnarchiver.unarchiveObjectWithFile_(some_plist_file)

You can find more documentation here.

Benno
  • 5,640
  • 2
  • 26
  • 31
3

You probably want PyObjC. That said, I've never actually used it myself (I've only ever seen demos), so I'm not certain that it will do what you need.

Stephen Canon
  • 103,815
  • 19
  • 183
  • 269