Skip to content

Commit 999c0e7

Browse files
committed
utils: add functionality to generate bindings
This currently exists in two places 1) Bindtool (longevity TBD) which calls blocktool to parse the public header file in the include directory 2) Modtool - binding of headers added to add and bind. rm, update, info, etc still TODO
1 parent a4e6d4d commit 999c0e7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1782
-307
lines changed

gr-utils/CMakeLists.txt

+3
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ add_subdirectory(blocktool)
8484
endif(ENABLE_GR_BLOCKTOOL)
8585

8686
if(ENABLE_GR_MODTOOL)
87+
add_subdirectory(bindtool)
8788
add_subdirectory(modtool)
8889
endif(ENABLE_GR_MODTOOL)
8990

91+
92+
9093
endif(ENABLE_GR_UTILS)

gr-utils/bindtool/CMakeLists.txt

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copyright 2019 Free Software Foundation, Inc.
2+
#
3+
# This file is part of GNU Radio
4+
#
5+
# GNU Radio is free software; you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation; either version 3, or (at your option)
8+
# any later version.
9+
#
10+
# GNU Radio is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with GNU Radio; see the file COPYING. If not, write to
17+
# the Free Software Foundation, Inc., 51 Franklin Street,
18+
# Boston, MA 02110-1301, USA.
19+
20+
include(GrPython)
21+
22+
GR_PYTHON_INSTALL(FILES
23+
__init__.py
24+
DESTINATION ${GR_PYTHON_DIR}/gnuradio/bindtool
25+
)
26+
27+
########################################################################
28+
# Add subdirectories
29+
########################################################################
30+
add_subdirectory(core)
31+
add_subdirectory(templates)
32+

gr-utils/bindtool/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Tool to generate python bindings from headers
2+
3+
May eventually be absorbed into modtool or blocktool

gr-utils/bindtool/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .core.generator import BindingGenerator

gr-utils/bindtool/core/CMakeLists.txt

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
include(GrPython)
2+
3+
GR_PYTHON_INSTALL(FILES
4+
__init__.py
5+
base.py
6+
generator.py
7+
# types.py
8+
DESTINATION ${GR_PYTHON_DIR}/gnuradio/bindtool/core
9+
)

gr-utils/bindtool/core/__init__.py

Whitespace-only changes.

gr-utils/bindtool/core/base.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
3+
4+
class BindTool(object):
5+
6+
target_bindings = 'pybind11'
7+
8+
def __init__(self):
9+
pass

gr-utils/bindtool/core/generator.py

+268
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
#
2+
# Copyright 2020 Free Software Foundation, Inc.
3+
#
4+
# This file is part of GNU Radio
5+
#
6+
# SPDX-License-Identifier: GPL-3.0-or-later
7+
#
8+
#
9+
10+
from .base import BindTool
11+
from gnuradio.blocktool import BlockHeaderParser, GenericHeaderParser
12+
13+
import os
14+
import pathlib
15+
import json
16+
from mako.template import Template
17+
from datetime import datetime
18+
19+
20+
class BindingGenerator:
21+
22+
def __init__(self, prefix, namespace, prefix_include_root, output_dir="", addl_includes="", match_include_structure=False):
23+
"""Initialize BindingGenerator
24+
prefix -- path to installed gnuradio prefix (use gr.prefix() if unsure)
25+
namespace -- desired namespace to parse e.g. ['gr','module_name']
26+
module_name is stored as the last element of namespace
27+
prefix_include_root -- relative path to module headers, e.g. "gnuradio/modulename"
28+
29+
Keyword arguments:
30+
output_dir -- path where bindings will be placed
31+
addl_includes -- comma separated list of additional include directories (default "")
32+
match_include_structure --
33+
If set to False, a bindings/ dir will be placed directly under the specified output_dir
34+
If set to True, the directory structure under include/ will be mirrored
35+
"""
36+
37+
self.header_extensions = ['.h', '.hh', '.hpp']
38+
self.addl_include = addl_includes
39+
self.prefix = prefix
40+
self.namespace = namespace
41+
self.module_name = namespace[-1]
42+
self.prefix_include_root = prefix_include_root
43+
self.output_dir = output_dir
44+
self.match_include_structure = match_include_structure
45+
46+
pass
47+
48+
def gen_pydoc_h(self, header_info, base_name):
49+
current_path = os.path.dirname(pathlib.Path(__file__).absolute())
50+
tpl = Template(filename=os.path.join(
51+
current_path, '..', 'templates', 'license.mako'))
52+
license = tpl.render(year=datetime.now().year)
53+
54+
tpl = Template(filename=os.path.join(current_path, '..',
55+
'templates', 'pydoc_h.mako'))
56+
return tpl.render(
57+
license=license,
58+
header_info=header_info,
59+
basename=base_name,
60+
prefix_include_root=self.prefix_include_root,
61+
)
62+
63+
64+
def gen_pybind_cc(self, header_info, base_name):
65+
current_path = os.path.dirname(pathlib.Path(__file__).absolute())
66+
tpl = Template(filename=os.path.join(
67+
current_path, '..', 'templates', 'license.mako'))
68+
license = tpl.render(year=datetime.now().year)
69+
70+
tpl = Template(filename=os.path.join(current_path, '..',
71+
'templates', 'generic_python_cc.mako'))
72+
return tpl.render(
73+
license=license,
74+
header_info=header_info,
75+
basename=base_name,
76+
prefix_include_root=self.prefix_include_root,
77+
)
78+
79+
def write_pydoc_h(self, header_info, base_name, output_dir):
80+
81+
doc_pathname = os.path.join(
82+
output_dir, 'docstrings', '{}_pydoc_template.h'.format(base_name))
83+
84+
try:
85+
pybind_code = self.gen_pydoc_h(
86+
header_info, base_name)
87+
with open(doc_pathname, 'w+') as outfile:
88+
print("Writing binding code to {}".format(doc_pathname))
89+
outfile.write(pybind_code)
90+
return doc_pathname
91+
except Exception as e:
92+
print(e)
93+
return None
94+
95+
96+
def write_pybind_cc(self, header_info, base_name, output_dir):
97+
98+
binding_pathname_cc = os.path.join(
99+
output_dir, '{}_python.cc'.format(base_name))
100+
101+
try:
102+
pybind_code = self.gen_pybind_cc(
103+
header_info, base_name)
104+
with open(binding_pathname_cc, 'w+') as outfile:
105+
print("Writing binding code to {}".format(binding_pathname_cc))
106+
outfile.write(pybind_code)
107+
return binding_pathname_cc
108+
except Exception as e:
109+
print(e)
110+
return None
111+
112+
def write_json(self, header_info, base_name, output_dir):
113+
json_pathname = os.path.join(output_dir, '{}.json'.format(base_name))
114+
with open(json_pathname, 'w') as outfile:
115+
json.dump(header_info, outfile)
116+
117+
def read_json(self, pathname):
118+
with open(pathname, 'r') as fp:
119+
header_info = json.load(fp)
120+
return header_info
121+
122+
def gen_file_binding(self, file_to_process):
123+
"""Produce the blockname_python.cc python bindings"""
124+
output_dir = self.get_output_dir(file_to_process)
125+
binding_pathname = None
126+
base_name = os.path.splitext(os.path.basename(file_to_process))[0]
127+
module_include_path = os.path.abspath(os.path.dirname(file_to_process))
128+
top_include_path = os.path.join(
129+
module_include_path.split('include'+os.path.sep)[0], 'include')
130+
131+
include_paths = ','.join(
132+
(module_include_path, top_include_path))
133+
if self.prefix:
134+
prefix_include_path = os.path.abspath(
135+
os.path.join(self.prefix, 'include'))
136+
include_paths = ','.join(
137+
(include_paths, prefix_include_path)
138+
)
139+
if self.addl_include:
140+
include_paths = ','.join((include_paths, self.addl_include))
141+
142+
parser = GenericHeaderParser(
143+
include_paths=include_paths, file_path=file_to_process)
144+
try:
145+
header_info = parser.get_header_info(self.namespace)
146+
# TODO: Scrape the docstrings
147+
self.write_json(header_info, base_name, output_dir)
148+
self.write_pybind_cc(header_info, base_name, output_dir)
149+
self.write_pydoc_h(header_info, base_name, output_dir)
150+
151+
except Exception as e:
152+
print(e)
153+
failure_pathname = os.path.join(
154+
output_dir, 'failed_conversions.txt')
155+
with open(failure_pathname, 'a+') as outfile:
156+
outfile.write(file_to_process)
157+
outfile.write(str(e))
158+
outfile.write('\n')
159+
160+
return binding_pathname
161+
162+
def get_output_dir(self, filename):
163+
"""Get the output directory for a given file"""
164+
output_dir = self.output_dir
165+
rel_path_after_include = ""
166+
if self.match_include_structure:
167+
if 'include'+os.path.sep in filename:
168+
rel_path_after_include = os.path.split(
169+
filename.split('include'+os.path.sep)[-1])[0]
170+
171+
output_dir = os.path.join(
172+
self.output_dir, rel_path_after_include, 'bindings')
173+
doc_dir = os.path.join(output_dir,'docstrings')
174+
if output_dir and not os.path.exists(output_dir) and not os.path.exists(doc_dir):
175+
output_dir = os.path.abspath(output_dir)
176+
print('creating output directory {}'.format(output_dir))
177+
os.makedirs(output_dir)
178+
179+
if doc_dir and not os.path.exists(doc_dir):
180+
doc_dir = os.path.abspath(doc_dir)
181+
print('creating docstrings directory {}'.format(doc_dir))
182+
os.makedirs(doc_dir)
183+
184+
return output_dir
185+
186+
def gen_top_level_cpp(self, file_list):
187+
"""Produce the python_bindings.cc for the bindings"""
188+
current_path = os.path.dirname(pathlib.Path(__file__).absolute())
189+
file = file_list[0]
190+
output_dir = self.get_output_dir(file)
191+
192+
tpl = Template(filename=os.path.join(
193+
current_path, '..', 'templates', 'license.mako'))
194+
license = tpl.render(year=datetime.now().year)
195+
196+
binding_pathname = os.path.join(output_dir, 'python_bindings.cc')
197+
file_list = [os.path.split(f)[-1] for f in file_list]
198+
tpl = Template(filename=os.path.join(current_path, '..',
199+
'templates', 'python_bindings_cc.mako'))
200+
pybind_code = tpl.render(
201+
license=license,
202+
files=file_list,
203+
module_name=self.module_name
204+
)
205+
206+
# print(pybind_code)
207+
try:
208+
with open(binding_pathname, 'w+') as outfile:
209+
outfile.write(pybind_code)
210+
return binding_pathname
211+
except:
212+
return None
213+
214+
def gen_cmake_lists(self, file_list):
215+
"""Produce the CMakeLists.txt for the bindings"""
216+
current_path = os.path.dirname(pathlib.Path(__file__).absolute())
217+
file = file_list[0]
218+
output_dir = self.get_output_dir(file)
219+
220+
tpl = Template(filename=os.path.join(
221+
current_path, '..', 'templates', 'license.mako'))
222+
license = tpl.render(year=datetime.now().year)
223+
224+
binding_pathname = os.path.join(output_dir, 'CMakeLists.txt')
225+
file_list = [os.path.split(f)[-1] for f in file_list]
226+
tpl = Template(filename=os.path.join(current_path, '..',
227+
'templates', 'CMakeLists.txt.mako'))
228+
pybind_code = tpl.render(
229+
license=license,
230+
files=file_list,
231+
module_name=self.module_name
232+
)
233+
234+
# print(pybind_code)
235+
try:
236+
with open(binding_pathname, 'w+') as outfile:
237+
outfile.write(pybind_code)
238+
return binding_pathname
239+
except:
240+
return None
241+
242+
def get_file_list(self, include_path):
243+
"""Recursively get sorted list of files in path"""
244+
file_list = []
245+
for root, _, files in os.walk(include_path):
246+
for file in files:
247+
_, file_extension = os.path.splitext(file)
248+
if (file_extension in self.header_extensions):
249+
pathname = os.path.abspath(os.path.join(root, file))
250+
file_list.append(pathname)
251+
return sorted(file_list)
252+
253+
def gen_bindings(self, module_dir):
254+
"""Generate bindings for an entire GR module
255+
256+
Produces CMakeLists.txt, python_bindings.cc, and blockname_python.cc
257+
for each block in the module
258+
259+
module_dir -- path to the include directory where the public headers live
260+
"""
261+
file_list = self.get_file_list(module_dir)
262+
api_pathnames = [s for s in file_list if 'api.h' in s]
263+
for f in api_pathnames:
264+
file_list.remove(f)
265+
self.gen_top_level_cpp(file_list)
266+
self.gen_cmake_lists(file_list)
267+
for fn in file_list:
268+
self.gen_file_binding(fn)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from gnuradio.bindtool import BindingGenerator
2+
3+
prefix = '/share/gnuradio/grnext'
4+
output_dir = '/share/tmp/test_pybind'
5+
namespace = ['gr']
6+
module_dir = '/share/gnuradio/grnext/src/gnuradio/gnuradio-runtime/include/gnuradio'
7+
prefix_include_root = 'gnuradio' #pmt, gnuradio/digital, etc.
8+
9+
prefix = '/share/gnuradio/grnext'
10+
output_dir = '/share/tmp/test_pybind'
11+
namespace = ['gr','digital']
12+
module_dir = '/share/gnuradio/grnext/src/gnuradio/gr-digital/include'
13+
prefix_include_root = 'gnuradio/digital' #pmt, gnuradio/digital, etc.
14+
15+
prefix = '/share/gnuradio/grnext'
16+
output_dir = '/share/tmp/test_pybind'
17+
namespace = ['gr','fft']
18+
module_dir = '/share/gnuradio/grnext/src/gnuradio/gr-fft/include'
19+
prefix_include_root = 'gnuradio/fft' #pmt, gnuradio/digital, etc.
20+
21+
import warnings
22+
with warnings.catch_warnings():
23+
warnings.filterwarnings("ignore", category=DeprecationWarning)
24+
bg = BindingGenerator(prefix, namespace, prefix_include_root, output_dir)
25+
bg.gen_bindings(module_dir)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import argparse
2+
import os
3+
from gnuradio.bindtool import BindingGenerator
4+
import pathlib
5+
6+
parser = argparse.ArgumentParser(description='Bind a GR header file from the generated json')
7+
parser.add_argument('pathnames', type=str, nargs='+',
8+
help='Json files to bind')
9+
10+
args = parser.parse_args()
11+
12+
bg = BindingGenerator()
13+
14+
for p in args.pathnames:
15+
bg.bind_from_json(p)

0 commit comments

Comments
 (0)