Commits

Virgil Dupras committed 46cd34e

Added the ability to generate a python module from a python class interface definition. Added util.objcname and util.dontwrap. Improved init method autonaming.

  • Participants
  • Parent commits d28f6bd

Comments (0)

Files changed (12)

File demos/protocol/autogen/MyProtocol.m

-
-#define PY_SSIZE_T_CLEAN
-#import <Python.h>
-#import "structmember.h"
-#import "ObjP.h"
-
-#import <Cocoa/Cocoa.h>
-
-@protocol MyProtocol <NSObject>
-- (NSString *)getAnswer:(NSInteger)arg;
-@end
-
-typedef struct {
-    PyObject_HEAD
-    id <MyProtocol>objc_ref;
-} MyProtocol_Struct;
-
-static PyTypeObject MyProtocol_Type; /* Forward declaration */
-
-/* Methods */
-
-static void
-MyProtocol_dealloc(MyProtocol_Struct *self)
-{
-    [self->objc_ref release];
-    Py_TYPE(self)->tp_free((PyObject *)self);
-}
-
-
-static int
-MyProtocol_init(MyProtocol_Struct *self, PyObject *args, PyObject *kwds)
-{
-    PyObject *pRefCapsule = NULL;
-    if (!PyArg_ParseTuple(args, "|O", &pRefCapsule)) {
-        return -1;
-    }
-    
-    if (pRefCapsule == NULL) {
-        self->objc_ref = NULL; // Never supposed to happen
-    }
-    else {
-        self->objc_ref = PyCapsule_GetPointer(pRefCapsule, NULL);
-        [self->objc_ref retain];
-    }
-    
-    return 0;
-}
-
-
-
-static PyObject *
-MyProtocol_getAnswer_(MyProtocol_Struct *self, PyObject *args)
-{
-    PyObject *parg;
-    if (!PyArg_ParseTuple(args, "O", &parg)) {
-        return NULL;
-    }
-    NSInteger arg = ObjP_int_p2o(parg);
-    
-    NSString * retval = [self->objc_ref getAnswer:arg];
-    PyObject *pResult = ObjP_str_o2p(retval); return pResult;
-}
-
-
-static PyMethodDef MyProtocol_methods[] = {
- 
-{"getAnswer_", (PyCFunction)MyProtocol_getAnswer_, METH_VARARGS, ""},
-
-{NULL}  /* Sentinel */
-};
-
-static PyTypeObject MyProtocol_Type = {
-    PyVarObject_HEAD_INIT(NULL, 0)
-    "MyProtocol.MyProtocol", /*tp_name*/
-    sizeof(MyProtocol_Struct), /*tp_basicsize*/
-    0, /*tp_itemsize*/
-    (destructor)MyProtocol_dealloc, /*tp_dealloc*/
-    0, /*tp_print*/
-    0, /*tp_getattr*/
-    0, /*tp_setattr*/
-    0, /*tp_reserved*/
-    0, /*tp_repr*/
-    0, /*tp_as_number*/
-    0, /*tp_as_sequence*/
-    0, /*tp_as_mapping*/
-    0, /*tp_hash */
-    0, /*tp_call*/
-    0, /*tp_str*/
-    0, /*tp_getattro*/
-    0, /*tp_setattro*/
-    0, /*tp_as_buffer*/
-    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
-    "MyProtocol object", /* tp_doc */
-    0, /* tp_traverse */
-    0, /* tp_clear */
-    0, /* tp_richcompare */
-    0, /* tp_weaklistoffset */
-    0, /* tp_iter */
-    0, /* tp_iternext */
-    MyProtocol_methods,/* tp_methods */
-    0, /* tp_members */
-    0, /* tp_getset */
-    0, /* tp_base */
-    0, /* tp_dict */
-    0, /* tp_descr_get */
-    0, /* tp_descr_set */
-    0, /* tp_dictoffset */
-    (initproc)MyProtocol_init,      /* tp_init */
-    0, /* tp_alloc */
-    0, /* tp_new */
-    0, /* tp_free */
-    0, /* tp_is_gcc */
-    0, /* tp_bases */
-    0, /* tp_mro */
-    0, /* tp_cache */
-    0, /* tp_subclasses */
-    0  /* tp_weaklist */
-};
-
-static PyMethodDef module_methods[] = {
-    {NULL}  /* Sentinel */
-};
-
-static struct PyModuleDef MyProtocolDef = {
-    PyModuleDef_HEAD_INIT,
-    "MyProtocol",
-    NULL,
-    -1,
-    module_methods,
-    NULL,
-    NULL,
-    NULL,
-    NULL
-};
-
-PyObject *
-PyInit_MyProtocol(void)
-{
-    PyObject *m;
-    
-    MyProtocol_Type.tp_new = PyType_GenericNew;
-    if (PyType_Ready(&MyProtocol_Type) < 0) {
-        return NULL;
-    }
-    
-    m = PyModule_Create(&MyProtocolDef);
-    if (m == NULL) {
-        return NULL;
-    }
-    
-    Py_INCREF(&MyProtocol_Type);
-    PyModule_AddObject(m, "MyProtocol", (PyObject *)&MyProtocol_Type);
-    return m;
-}
-

File demos/pyprotocol/MyClass.h

+#import <Cocoa/Cocoa.h>
+
+@interface MyClass : NSObject {}
+- (NSString *)getAnswer:(NSInteger)arg;
+@end

File demos/pyprotocol/MyClass.m

+#import "MyClass.h"
+
+@implementation MyClass
+- (NSString *)getAnswer:(NSInteger)arg
+{
+    return [NSString stringWithFormat:@"The answer is %i.", arg];
+}
+@end

File demos/pyprotocol/main.m

+#import <Cocoa/Cocoa.h>
+#import "ObjP.h"
+#import "PyMain.h"
+#import "MyClass.h"
+
+int main(int argc, char *argv[])
+{
+    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+    Py_Initialize();
+    FILE* fp = fopen("main.py", "r");
+    PyRun_SimpleFile(fp, "main.py");
+    fclose(fp);
+    MyClass *callback = [[MyClass alloc] init];
+    PyObject *pCallback = ObjP_classInstanceWithRef(@"MyProtocol", @"MyProtocol", callback);
+    PyMain *foo = [[PyMain alloc] initWithCallback:pCallback];
+    Py_DECREF(pCallback);
+    [foo execute];
+    [foo release];
+    Py_Finalize();
+    [pool release];
+    return 0;   
+}

File demos/pyprotocol/main.py

+from objp.util import pyref
+
+class MyProtocol:
+    def getAnswer_(self, arg: int) -> str: pass
+
+class PyMain:
+    def __init__(self, callback: pyref):
+        self.callback = callback
+    
+    def execute(self):
+        print("Hello from Python. Let's do a callback on our class conforming to our protocol.")
+        print(self.callback.getAnswer_(42))

File demos/pyprotocol/waf

Binary file added.

File demos/pyprotocol/wscript

+#! /usr/bin/env python
+# encoding: utf-8
+
+top = '.'
+out = 'build'
+
+def options(opt):
+    opt.load('python')
+    opt.load('compiler_c')
+
+def configure(conf):
+    conf.load('compiler_c')
+    conf.load('python')
+    conf.check_python_version((3,2))
+    conf.check_python_headers()
+    conf.env.append_value('FRAMEWORK_COCOA', 'Cocoa')
+    conf.env.ARCH = ['i386', 'x86_64']
+
+def build(bld):
+    import objp.o2p
+    import objp.p2o
+    import main
+    objp.o2p.generate_objc_code(main.PyMain, 'autogen')
+    callbackspec = objp.o2p.spec_from_python_class(main.MyProtocol)
+    objp.p2o.generate_python_proxy_code_from_clsspec(callbackspec, 'autogen/MyProtocol.m')
+    bld(
+        features = 'c cshlib pyext',
+        source   = 'autogen/MyProtocol.m',
+        target   = 'MyProtocol')
+    bld.program(
+        features      = 'c cprogram pyembed',
+        target        = 'protocol',
+        source        = 'main.m autogen/PyMain.m autogen/ObjP.m MyClass.m',
+        use           = 'COCOA',
+        includes      = '. autogen',
+    )
+    bld(
+        rule = 'cp ${SRC} ${TGT}',
+        source = bld.path.make_node('main.py'),
+        target = bld.path.get_bld().make_node('main.py'),
+    )
+
+from waflib import TaskGen
+@TaskGen.extension('.m')
+def m_hook(self, node):
+    """Alias .m files to be compiled the same as .c files, gcc will do the right thing."""
+    return self.create_compiled_task('c', node)
+
+@TaskGen.feature('cshlib')
+@TaskGen.after('apply_obj_vars', 'apply_link')
+def kill_flag(self):
+    fl = self.link_task.env.LINKFLAGS
+    if '-bundle' in fl and '-dynamiclib' in fl:
+         fl.remove('-bundle')
+         self.link_task.env.LINKFLAGS = fl
+
+# -*- indent-tabs-mode: t -*-

File demos/simple/simple.py

+from objp.util import dontwrap
+
 class Simple:
     def hello_(self, name: str):
         print("Hello %s!" % name)
     def doubleNumbers_(self, numbers: list) -> list:
         return [i*2 for i in numbers]
     
+    @dontwrap
+    def foobar(self):
+        print("This method shouldn't be wrapped by objp because we tell it so.")

File objp/base.py

 
 TypeSpec = namedtuple('TypeSpec', 'pytype objctype o2p_code p2o_code')
 ArgSpec = namedtuple('ArgSpec', 'argname typespec')
-MethodSpec = namedtuple('MethodSpec', 'methodname argspecs returntype')
+MethodSpec = namedtuple('MethodSpec', 'pyname objcname argspecs returntype')
 ClassSpec = namedtuple('ClassSpec', 'clsname methodspecs is_protocol')
 
 TYPE_SPECS = [
         result = result.replace('%%{}%%'.format(placeholder), replacement)
     return result
 
+def get_objc_signature(methodspec):
+    methodname = methodspec.objcname
+    returntype = methodspec.returntype
+    name_elems = methodname.split(':')
+    assert len(name_elems) == len(methodspec.argspecs) + 1
+    returntype = returntype.objctype if returntype is not None else 'void'
+    result_elems = ['(%s)' % returntype, name_elems[0]]
+    for name_elem, arg in zip(name_elems[1:], methodspec.argspecs):
+        result_elems.append(':(%s)%s %s' % (arg.typespec.objctype, arg.argname, name_elem))
+    return ''.join(result_elems).strip()
+
 def copy_objp_unit(destfolder):
     if not op.exists(destfolder):
         os.makedirs(destfolder)
 import os.path as op
 import inspect
 
-from .base import PYTYPE2SPEC, tmpl_replace, copy_objp_unit, ArgSpec, MethodSpec, ClassSpec
+from .base import (PYTYPE2SPEC, tmpl_replace, copy_objp_unit, ArgSpec, MethodSpec, ClassSpec,
+    get_objc_signature)
 
 TEMPLATE_HEADER = """
 #import <Cocoa/Cocoa.h>
     return result;
 """
 
+def camelcase(s):
+    elems = s.split('_')
+    elems = [elems[0]] + [e.title() for e in elems[1:]]
+    return ''.join(elems)
+
 def internalize_argspec(name, argspec):
     # take argspec from the inspect module and returns MethodSpec
     args = argspec.args[1:] # remove self
     for arg in args:
         ts = PYTYPE2SPEC[ann[arg]]
         argspecs.append(ArgSpec(arg, ts))
-    if 'return' in ann:
-        returntype = PYTYPE2SPEC[ann['return']]
+    if name == '__init__':
+        # generate objcname from args and always have an object return type
+        returntype = PYTYPE2SPEC[object]
+        if argspecs:
+            argnames = [camelcase(arg.argname) for arg in argspecs]
+            argnames[0] = argnames[0].title()
+            objcname = 'initWith' + ':'.join(argnames) + ':'
+        else:
+            objcname = 'init'
     else:
-        returntype = None
-    return MethodSpec(name, argspecs, returntype)
-
-def get_objc_signature(methodspec, methodname=None, returntype=None):
-    if methodname is None:
-        methodname = methodspec.methodname
-    if returntype is None:
-        returntype = methodspec.returntype
-    name_elems = methodname.split('_')
-    assert len(name_elems) == len(methodspec.argspecs) + 1
-    returntype = returntype.objctype if returntype is not None else 'void'
-    result_elems = ['(%s)' % returntype, name_elems[0]]
-    for name_elem, arg in zip(name_elems[1:], methodspec.argspecs):
-        result_elems.append(':(%s)%s %s' % (arg.typespec.objctype, arg.argname, name_elem))
-    return ''.join(result_elems).strip()
+        if 'return' in ann:
+            returntype = PYTYPE2SPEC[ann['return']]
+        else:
+            returntype = None
+        objcname = name.replace('_', ':')
+    return MethodSpec(name, objcname, argspecs, returntype)
 
 def get_arg_c_code(argspecs):
     result = []
     result.append('NULL') # We have to add a NULL item in va_args in PyObject_CallMethodObjArgs
     return ', '.join(result)
 
-def get_objc_method_code(methodspec):
+def get_objc_method_code(clsspec, methodspec):
     signature = get_objc_signature(methodspec)
     tmpl_args = get_arg_c_code(methodspec.argspecs)
     if methodspec.returntype is not None:
         returncode = tmpl_replace(TEMPLATE_RETURN, type=ts.objctype, pyconversion=tmpl_pyconversion)
     else:
         returncode = TEMPLATE_RETURN_VOID
-    code = tmpl_replace(TEMPLATE_METHOD, signature=signature, pyname=methodspec.methodname,
-        args=tmpl_args, returncode=returncode)
-    sig = '- %s;' % signature
-    return (code, sig)
-
-def get_objc_init_code(methodspec, classname):
-    # our signature for init function is constructed based on arg names.
-    argnames = [arg.argname.title() for arg in methodspec.argspecs]
-    if argnames:
-        methodname = 'initWith' + '_'.join(argnames) + '_'
+    if methodspec.pyname == '__init__':
+        code = tmpl_replace(TEMPLATE_INIT_METHOD, signature=signature, classname=clsspec.clsname,
+            args=tmpl_args)
     else:
-        methodname = 'init'
-    signature = get_objc_signature(methodspec, methodname=methodname, returntype=PYTYPE2SPEC[object])
-    tmpl_args = get_arg_c_code(methodspec.argspecs)
-    code = tmpl_replace(TEMPLATE_INIT_METHOD, signature=signature, args=tmpl_args,
-        classname=classname)
+        code = tmpl_replace(TEMPLATE_METHOD, signature=signature, pyname=methodspec.pyname,
+            args=tmpl_args, returncode=returncode)
     sig = '- %s;' % signature
     return (code, sig)
 
     methods = inspect.getmembers(class_, inspect.isfunction)
     methodspecs = []
     for name, meth in methods:
+        if getattr(meth, 'dontwrap', False):
+            continue
         argspec = inspect.getfullargspec(meth)
         try:
+            if hasattr(meth, 'objcname'):
+                name = meth.objcname
             methodspec = internalize_argspec(name, argspec)
             methodspecs.append(methodspec)
         except AssertionError:
             print("Warning: Couldn't generate spec for %s" % name)
             continue
-    if not any(ms.methodname == '__init__' for ms in methodspecs):
+    if not any(ms.pyname == '__init__' for ms in methodspecs):
         # Always create a default init method.
-        methodspecs.insert(0, MethodSpec('__init__', [], None))
-    return ClassSpec(class_.__name__, methodspecs, False)
+        methodspecs.insert(0, MethodSpec('__init__', 'init', [], PYTYPE2SPEC[object]))
+    return ClassSpec(class_.__name__, methodspecs, True)
 
 def generate_objc_code(class_, destfolder, extra_imports=None, follow_protocols=None):
     clsspec = spec_from_python_class(class_)
     method_sigs = []
     for methodspec in clsspec.methodspecs:
         try:
-            if methodspec.methodname == '__init__':
-                code, sig = get_objc_init_code(methodspec, clsname)
-            else:
-                code, sig = get_objc_method_code(methodspec)
+            code, sig = get_objc_method_code(clsspec, methodspec)
         except AssertionError:
-            print("Warning: Couldn't generate code for %s" % methodspec.methodname)
+            print("Warning: Couldn't generate code for %s" % methodspec.pyname)
             continue
         method_code.append(code)
         method_sigs.append(sig)
 import re
 import os.path as op
-from .base import tmpl_replace, OBJCTYPE2SPEC, copy_objp_unit, ArgSpec, MethodSpec, ClassSpec
+from .base import (tmpl_replace, OBJCTYPE2SPEC, copy_objp_unit, ArgSpec, MethodSpec, ClassSpec,
+    get_objc_signature)
 
 TEMPLATE_UNIT = """
 #define PY_SSIZE_T_CLEAN
 
 """
 
+TEMPLATE_TARGET_PROTOCOL = """
+@protocol %%clsname%% <NSObject>
+%%methods%%
+@end
+"""
+
 TEMPLATE_INITFUNC_CREATE = """
 static int
 %%clsname%%_init(%%clsname%%_Struct *self, PyObject *args, PyObject *kwds)
                 argname = elem[2]
                 argtype = OBJCTYPE2SPEC[elem[1]]
                 args.append(ArgSpec(argname, argtype))
-        method_specs.append(MethodSpec(name, args, resulttype))
+        pyname = name.replace(':', '_')
+        method_specs.append(MethodSpec(pyname, name, args, resulttype))
     return ClassSpec(clsname, method_specs, is_protocol)
 
-def generate_python_proxy_code(header_path, destpath):
-    # The name of the file in destpath will determine the name of the module. For example,
-    # "foo/bar.m" will result in a module name "bar".
-    with open(header_path, 'rt') as fp:
-        header = fp.read()
-    clsspec = parse_objc_header(header)
+def generate_target_protocol(clsspec):
+    clsname = clsspec.clsname
+    signatures = ['- %s;' % get_objc_signature(methodspec) for methodspec in clsspec.methodspecs]
+    return tmpl_replace(TEMPLATE_TARGET_PROTOCOL, clsname=clsname, methods='\n'.join(signatures))
+
+def generate_python_proxy_code_from_clsspec(clsspec, destpath, objcinterface=None):
+    if objcinterface is None:
+        objcinterface = generate_target_protocol(clsspec)
     clsname = clsspec.clsname
     if clsspec.is_protocol:
         tmpl_objc_create = "NULL; // Never supposed to happen"
         objc_create=tmpl_objc_create)
     tmpl_methods = []
     tmpl_methodsdef = []
-    for methodname, args, resulttype in clsspec.methodspecs:
+    for pyname, objcname, args, resulttype in clsspec.methodspecs:
         tmplval = {}
-        tmplval['methname'] = methodname.replace(':', '_')
+        tmplval['methname'] = pyname
         if resulttype is None:
             tmplval['retvalassign'] = ''
             tmplval['retvalreturn'] = 'Py_RETURN_NONE;'
                 ts = arg.typespec
                 conversion.append('%s %s = %s;' % (ts.objctype, name, ts.p2o_code % ('p'+name)))
             tmplval['conversion'] = '\n'.join(conversion)
-            elems = methodname.split(':')
+            elems = objcname.split(':')
             elems_and_args = [elem + ':' + argname for elem, (argname, _) in zip(elems, args)]
             tmplval['methcall'] = ' '.join(elems_and_args)
             tmpl_methods.append(tmpl_replace(TEMPLATE_METHOD_VARARGS, clsname=clsname, **tmplval))
     modulename = op.splitext(op.basename(destpath))[0]
     typedecl = "id <%s>" % clsname if clsspec.is_protocol else "%s *" % clsname
     result = tmpl_replace(TEMPLATE_UNIT, clsname=clsname, modulename=modulename,
-        typedecl=typedecl, objcinterface=header, initfunc=tmpl_initfunc, methods=tmpl_methods,
+        typedecl=typedecl, objcinterface=objcinterface, initfunc=tmpl_initfunc, methods=tmpl_methods,
         methodsdef=tmpl_methodsdef)
     copy_objp_unit(op.dirname(destpath))
     with open(destpath, 'wt') as fp:
         fp.write(result)
+
+def generate_python_proxy_code(header_path, destpath):
+    # The name of the file in destpath will determine the name of the module. For example,
+    # "foo/bar.m" will result in a module name "bar".
+    with open(header_path, 'rt') as fp:
+        header = fp.read()
+    clsspec = parse_objc_header(header)
+    generate_python_proxy_code_from_clsspec(clsspec, destpath, header)
     

File objp/util.py

 pyref = object()
+
+def objcname(name):
+    def decorator(func):
+        func.objcname = name
+        return func
+    return decorator
+
+def dontwrap(func):
+    func.dontwrap = True
+    return func