Skip to content

Calling native functions

The core power of rpcclient is calling native functions and working with raw memory through Symbol objects.

s = p.symbol(0x12345678)

# Symbol extends `int`
assert isinstance(s, int)

# write into this memory
s.poke(b'abc')

# peek (read) 20 bytes of memory
print(s.peek(20))

# jump to `s` as a function, passing (1, "string") as its args
s(1, "string")

# change the item size used for derefs
s.item_size = 1
s[0] = 1            # *(char *)s = 1
s[1] = 1            # *(((char *)s)+1) = 1

# Symbol inherits from int, so all int operations apply
s += 4

# back to 8 to store pointers
s.item_size = 8
s[0] = 1            # *(intptr_t *)s = 1

# calling a symbol returns a symbol
s[0] = p.symbol(0x11223344)()

# which file is this address loaded from?
print(p.symbol(0x11223344).filename)

Globalized symbols

Symbols already mapped into the running process are reachable through symbols.<name>. symbols is a SymbolsJar — a dict wrapper over all exported symbols:

x = symbols.malloc(20)

You can also use the bare name; the client checks for collisions and resolves it lazily:

x = malloc(20)
# equivalent to:
malloc = symbols.malloc
x = malloc(20)

ObjC support

On Darwin systems, the built-in Objective-C support is often easier:

# create CF/NS objects with cf()
some_cf_string = p.cf('some string')

# create a new NSMutableDictionary
a = NSMutableDictionary.new()
print(NSMutableDictionary.bundle_path)            # where the class is loaded from
a = p.objc_get_class('NSMutableDictionary').new() # the long form

# each darwin object is a DarwinSymbol with objc_call(...)
a.objc_call('setObject:forKey:', p.cf('value'), p.cf('key'))
a.cfdesc                                           # CFDescription

# convert to an ObjectiveCSymbol for richer introspection
a = a.objc_symbol
a.show()                                           # classes/objects properties
a.setObject_forKey_(p.cf('value2'), p.cf('key2'))  # auto-completed selectors

# back to native python (via plistlib; plist-serializable objects only)
a = a.py()

# load all frameworks for ObjC auto-completion (same as client -l -r)
p.load_all_libraries()

Autorelease pools

# create an autorelease pool context, drained at the end
pool_ctx = p.create_autorelease_pool_ctx()
# ... do stuff ...
pool_ctx.drain()

# same, as a context manager
with p.create_autorelease_pool_ctx() as pool_ctx:
    ...  # do stuff

# iterate over all currently existing autorelease pools and their objects
for pool in p.get_autorelease_pools():
    for obj in pool:
        ...  # do stuff with objects

Run loop and threading

On Apple targets the server pumps the main thread's run loop for the lifetime of the connection and serves your requests on a dedicated thread. Anything driven by the main run loop or the main dispatch queue - blocks dispatched to the main queue, run loop timers and sources, framework callbacks and notifications - keeps making progress while you work:

main_run_loop = p.symbols.CFRunLoopGetMain()
# the main run loop is up and waiting for work, not blocked serving you
p.symbols.CFRunLoopIsWaiting(main_run_loop)

There is no need to run a loop yourself; calling CFRunLoopRun() over RPC only hangs your own session, since it never returns.

Note that requests are served off the main thread. APIs that must run on the main thread need to be dispatched there explicitly.