Skip to content

Commit

Permalink
Merge pull request #3521 from mdboom/erfa/remove-cython
Browse files Browse the repository at this point in the history
Use Python/C API instead of Cython for ERFA wrappers
  • Loading branch information
embray committed Mar 6, 2015
1 parent cde7157 commit 4691592
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 117 deletions.
136 changes: 136 additions & 0 deletions astropy/_erfa/core.c.templ
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/* -*- mode: c -*- */

/* Licensed under a 3-clause BSD style license - see LICENSE.rst */

/* "core.c" is auto-generated by erfa_generator.py from the template
"core.c.templ". Do *not* edit "core.c" directly, instead edit
"core.c.templ" and run erfa_generator.py from the source directory to
update it. */


#include "Python.h"
#include "numpy/arrayobject.h"
#include "erfa.h"


#if PY_MAJOR_VERSION >= 3
#define PY3K 1
#else
#define PY3K 0
#endif


typedef struct {
PyObject_HEAD
NpyIter *iter;
} _NpyIterObject;


#define MODULE_DOCSTRING \
"This module contains the C part of the ERFA python wrappers.\n" \
"This implements only the inner iterator loops, while the heavy lifting\n" \
"happens in Python in core.py\n\n" \
"For more about the module and how to use it, see the ``core.py``\n" \
"docstrings."


{%- for func in funcs %}

static PyObject *Py_{{ func.pyname }}(PyObject *self, PyObject *args, PyObject *kwds)
{
{%- for arg in func.args_by_inout('in|inout|out|ret|stat') %}
{{ arg.ctype_ptr }} _{{ arg.name }};
{%- endfor %}
{%- if func.args_by_inout('stat')|length > 0 %}
int stat_ok = 1;
{%- endif %}
NpyIter *it = ((_NpyIterObject *)args)->iter;
char **dataptrarray = NpyIter_GetDataPtrArray(it);
NpyIter_IterNextFunc *iternext = NpyIter_GetIterNext(it, NULL);

do {
{%- for arg in func.args_by_inout('in|inout|out') %}
_{{ arg.name }} = (({{ arg.ctype }} *)(dataptrarray[{{ func.args.index(arg) }}])){%- if arg.ctype_ptr[-1] != '*' %}[0]{%- endif %};
{%- endfor %}

{{ func.args_by_inout('ret|stat')|map(attribute='name')|surround('_', ' = ')|join }}{{func.name}}({{ func.args_by_inout('in|inout|out')|map(attribute='name')|prefix('_')|join(', ') }});

{%- for arg in func.args_by_inout('ret|stat') %}
*(({{ arg.ctype_ptr }} *)(dataptrarray[{{ func.args.index(arg) }}])) = _{{ arg.name }};
{%- endfor %}

{%- for arg in func.args_by_inout('stat') %}
if (_{{ arg.name }}) {
stat_ok = 0;
}
{%- endfor %}
} while (iternext(it));

{%- if func.args_by_inout('stat')|length > 0 %}
if (stat_ok) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
{%- else %}
Py_RETURN_NONE;
{%- endif %}
}

{%- endfor %}

static PyMethodDef module_functions[] = {
{%- for func in funcs %}
{ "_" "{{ func.pyname }}", (PyCFunction)Py_{{ func.pyname }}, METH_O, NULL },
{%- endfor %}
{ NULL }
};

struct module_state
{
int _dummy;
};

#if PY3K

static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"_core",
MODULE_DOCSTRING,
sizeof(struct module_state),
module_functions,
NULL,
NULL,
NULL,
NULL
};

#define INITERROR return NULL

PyMODINIT_FUNC PyInit__core(void)

#else
#define INITERROR return

PyMODINIT_FUNC init_core(void)
#endif

{
PyObject *m;

#if PY3K
m = PyModule_Create(&moduledef);
#else
m = Py_InitModule3("_core", module_functions, MODULE_DOCSTRING);
#endif

if (m == NULL) {
INITERROR;
}

import_array();

#if PY3K
return m;
#endif
}
15 changes: 8 additions & 7 deletions astropy/_erfa/core.py.templ
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
# update it.

"""
This module uses Cython to wrap the ERFA library in numpy-vectorized
equivalents.
This module uses the Python/C API to wrap the ERFA library in
numpy-vectorized equivalents.

..warning::
This is currently *not* part of the public Astropy API, and may change in
Expand All @@ -25,11 +25,12 @@ want to give ten matricies (i.e., the ERFA input type is double[3][3]),
you would pass in a (10, 3, 3) numpy array. If the output of the ERFA
function is scalar, you'll get back a length-10 1D array.

Note that the Cython part of these functions are implemented in a separate
module (compiled as ``_core``), derived from the ``core.pyx`` file. Splitting
the wrappers into separate pure-python and Cython portions dramatically reduces
compilation time without notably impacting performance. (See issue [#3063] on the
github repository for more about this.)
Note that the C part of these functions are implemented in a separate
module (compiled as ``_core``), derived from the ``core.c`` file.
Splitting the wrappers into separate pure-python and C portions
dramatically reduces compilation time without notably impacting
performance. (See issue [#3063] on the github repository for more
about this.)
"""
from __future__ import absolute_import, division, print_function

Expand Down
90 changes: 0 additions & 90 deletions astropy/_erfa/core.pyx.templ

This file was deleted.

30 changes: 16 additions & 14 deletions astropy/_erfa/erfa_generator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
This module's main purpose is to act as a script to create new versions
of erfa.pyx when ERFA is updated (or this generator is enhanced).
of erfa.c when ERFA is updated (or this generator is enhanced).
`Jinja2 <http://jinja.pocoo.org/>`_ must be installed for this
module/script to function.
Expand Down Expand Up @@ -364,7 +364,7 @@ def surround(a_list, pre, post):
env.filters['postfix'] = postfix
env.filters['surround'] = surround

erfa_pyx_in = env.get_template('core.pyx.templ')
erfa_c_in = env.get_template('core.c.templ')
erfa_py_in = env.get_template('core.py.templ')

#Extract all the ERFA function names from erfa.h
Expand All @@ -378,7 +378,7 @@ def surround(a_list, pre, post):
with open(erfahfn, "r") as f:
erfa_h = f.read()

funcs = []
funcs = {}
section_subsection_functions = re.findall('/\* (\w*)/(\w*) \*/\n(.*?)\n\n',
erfa_h, flags=re.DOTALL|re.MULTILINE)
for section, subsection, functions in section_subsection_functions:
Expand All @@ -389,7 +389,7 @@ def surround(a_list, pre, post):
print_("{0}.{1}.{2}...".format(section, subsection, name))
if multifilserc:
# easy because it just looks in the file itself
funcs.append(Function(name, srcdir))
funcs[name] = Function(name, srcdir)
else:
# Have to tell it to look for a declaration matching
# the start of the header declaration, otherwise it
Expand All @@ -403,14 +403,16 @@ def surround(a_list, pre, post):
# argument names and line-breaking or
# whitespace
match_line = line[:-1].split('(')[0]
funcs.append(Function(name, srcdir, match_line))
funcs[name] = Function(name, srcdir, match_line)
break
else:
raise ValueError("A name for a C file wasn't "
"found in the string that "
"spawned it. This should be "
"impossible!")

funcs = list(funcs.values())

#Extract all the ERFA constants from erfam.h
erfamhfn = os.path.join(srcdir, 'erfam.h')
with open(erfamhfn, 'r') as f:
Expand All @@ -424,20 +426,20 @@ def surround(a_list, pre, post):
constants.append(Constant(name, value, doc))

print_("Rendering template")
erfa_pyx = erfa_pyx_in.render(funcs=funcs)
erfa_c = erfa_c_in.render(funcs=funcs)
erfa_py = erfa_py_in.render(funcs=funcs, constants=constants)

if outfn is not None:
outfnx = outfn + "x"
print_("Saving to", outfn, 'and', outfnx)
outfn_c = os.path.splitext(outfn)[0] + ".c"
print_("Saving to", outfn, 'and', outfn_c)
with open(outfn, "w") as f:
f.write(erfa_py)
with open(outfnx, "w") as f:
f.write(erfa_pyx)
with open(outfn_c, "w") as f:
f.write(erfa_c)

print_("Done!")

return erfa_pyx, erfa_py, funcs
return erfa_c, erfa_py, funcs

DEFAULT_ERFA_LOC = os.path.join(os.path.split(__file__)[0],
'../../cextern/erfa')
Expand All @@ -455,11 +457,11 @@ def surround(a_list, pre, post):
'erfa: "{0}"'.format(DEFAULT_ERFA_LOC))
ap.add_argument('-o', '--output', default='core.py',
help='The output filename. This is the name for only the '
'pure-python output, the Cython part will have the '
'same name but with an "x" appended.')
'pure-python output, the C part will have the '
'same name but with a ".c" extension.')
ap.add_argument('-t', '--template-loc',
default=DEFAULT_TEMPLATE_LOC,
help='the location where the "erfa.pyx.templ" '
help='the location where the "core.c.templ" '
'template can be found.')
ap.add_argument('-q', '--quiet', action='store_false', dest='verbose',
help='Suppress output normally printed to stdout.')
Expand Down
12 changes: 6 additions & 6 deletions astropy/_erfa/setup_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

SRC_FILES = glob.glob(os.path.join(ERFA_SRC, '*'))
SRC_FILES += [os.path.join(ERFAPKGDIR, filename)
for filename in ['core.py.templ', 'core.pyx.templ', 'erfa_generator.py']]
for filename in ['core.py.templ', 'core.c.templ', 'erfa_generator.py']]

GEN_FILES = [os.path.join(ERFAPKGDIR, 'core.py'), os.path.join(ERFAPKGDIR, 'core.pyx')]
GEN_FILES = [os.path.join(ERFAPKGDIR, 'core.py'), os.path.join(ERFAPKGDIR, 'core.c')]

def pre_build_py_hook(cmd_obj):
preprocess_source()
Expand All @@ -35,7 +35,7 @@ def pre_sdist_hook(cmd_obj):
def preprocess_source():
# Generating the ERFA wrappers should only be done if needed. This also
# ensures that it is not done for any release tarball since those will
# include core.py and core.pyx.
# include core.py and core.c.
if all(os.path.exists(filename) for filename in GEN_FILES):

# Determine modification times
Expand Down Expand Up @@ -65,7 +65,7 @@ def preprocess_source():
import jinja2
except:
log.warn("WARNING: jinja2 could not be imported, so the existing "
"ERFA core.py and core.pyx files will be used")
"ERFA core.py and core.c files will be used")
return

name = 'erfa_generator'
Expand All @@ -86,7 +86,7 @@ def preprocess_source():


def get_extensions():
sources = [os.path.join(ERFAPKGDIR, "core.pyx")]
sources = [os.path.join(ERFAPKGDIR, "core.c")]
include_dirs = ['numpy']
libraries = []

Expand Down Expand Up @@ -118,4 +118,4 @@ def requires_2to3():


def get_package_data():
return {'astropy._erfa': ['core.py.templ', 'core.pyx.templ']}
return {'astropy._erfa': ['core.py.templ', 'core.c.templ']}

0 comments on commit 4691592

Please sign in to comment.