Better support C++ types in generated structs (#577)
Better support C++ types in generated structs
Makes it possible to include C++ system headers like <vector> using generator option (nanopb_fileopt).include = '<vector>'.
diff --git a/generator/nanopb_generator.py b/generator/nanopb_generator.py
index a63703f..e778593 100755
--- a/generator/nanopb_generator.py
+++ b/generator/nanopb_generator.py
@@ -1541,8 +1541,12 @@
yield '\n'
for incfile in self.file_options.include:
- yield options.genformat % incfile
- yield '\n'
+ # allow including system headers
+ if (incfile.startswith('<')):
+ yield '#include %s\n' % incfile
+ else:
+ yield options.genformat % incfile
+ yield '\n'
for incfile in includes:
noext = os.path.splitext(incfile)[0]
@@ -1559,10 +1563,6 @@
yield '#endif\n'
yield '\n'
- yield '#ifdef __cplusplus\n'
- yield 'extern "C" {\n'
- yield '#endif\n\n'
-
if self.enums:
yield '/* Enum definitions */\n'
for enum in self.enums:
@@ -1587,6 +1587,10 @@
yield enum.auxiliary_defines() + '\n'
yield '\n'
+ yield '#ifdef __cplusplus\n'
+ yield 'extern "C" {\n'
+ yield '#endif\n\n'
+
if self.messages:
yield '/* Initializer values for message structs */\n'
for msg in self.messages:
diff --git a/tests/cxx_callback_datatype/SConscript b/tests/cxx_callback_datatype/SConscript
new file mode 100644
index 0000000..bd6fa63
--- /dev/null
+++ b/tests/cxx_callback_datatype/SConscript
@@ -0,0 +1,25 @@
+Import('env')
+
+import os
+
+base_env = env.Clone()
+base_env.Replace(NANOPBFLAGS = '--cpp-descriptor')
+base_env.NanopbProtoCpp('message')
+
+for std in ["c++03", "c++11", "c++14", "c++17", "c++20"]:
+ e = base_env.Clone()
+ e.Append(CXXFLAGS = '-std={}'.format(std))
+
+ # Make sure compiler supports this version of C++ before we actually run the
+ # test.
+ conf = Configure(e)
+ compiler_valid = conf.CheckCXX()
+ e = conf.Finish()
+ if not compiler_valid:
+ print("Skipping {} test - compiler doesn't support it".format(std))
+ continue
+
+ sources = [ 'cxx_callback_datatype.cpp', 'message.pb.cpp', '$NANOPB/pb_decode.c', '$NANOPB/pb_encode.c', '$NANOPB/pb_common.c' ]
+ objects = [ e.Object('{}_{}'.format(os.path.basename(s), std), s) for s in sources ]
+ p = e.Program(target = 'cxx_callback_datatype_{}'.format(std), source = objects)
+ e.RunTest(p)
diff --git a/tests/cxx_callback_datatype/cxx_callback_datatype.cpp b/tests/cxx_callback_datatype/cxx_callback_datatype.cpp
new file mode 100644
index 0000000..da76bc7
--- /dev/null
+++ b/tests/cxx_callback_datatype/cxx_callback_datatype.cpp
@@ -0,0 +1,91 @@
+#include "message.pb.hpp"
+
+#include <pb_encode.h>
+#include <pb_decode.h>
+
+#include <cstdio>
+
+// See tests/alltypes_callback, tests/oneoff_callback and examples/network_server for more...
+bool TestMessage_submessages_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field)
+{
+ if (ostream != NULL) {
+ const std::vector<int> &v = *(const std::vector<int> *)field->pData;
+ for (std::vector<int>::const_iterator i = v.begin(); i != v.end(); ++i) {
+ if (!pb_encode_tag_for_field(ostream, field)) {
+ return false;
+ }
+ SubMessage tmp;
+ tmp.actual_value = *i;
+ if (!pb_encode_submessage(ostream, SubMessage_fields, &tmp)) {
+ return false;
+ }
+ }
+ } else if (istream != NULL) {
+ std::vector<int> &v = *(std::vector<int> *)field->pData;
+ SubMessage tmp;
+ if (!pb_decode(istream, SubMessage_fields, &tmp)) {
+ return false;
+ }
+ v.push_back(tmp.actual_value);
+ }
+ return true;
+}
+
+extern "C"
+bool TestMessage_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field)
+{
+ if (field->tag == TestMessage_submessages_tag) {
+ return TestMessage_submessages_callback(istream, ostream, field);
+ }
+ return true;
+}
+
+extern "C"
+int main() {
+ std::vector<int> source;
+ source.push_back(5);
+ source.push_back(4);
+ source.push_back(3);
+ source.push_back(2);
+ source.push_back(1);
+
+
+ std::vector<uint8_t> serialized;
+ pb_ostream_t sizestream = {0};
+ pb_encode(&sizestream, TestMessage_fields, &source);
+ serialized.resize(sizestream.bytes_written);
+ pb_ostream_t outstream = pb_ostream_from_buffer(&serialized.front(), serialized.size());
+ if (!pb_encode(&outstream, TestMessage_fields, &source)) {
+ fprintf(stderr, "Failed to encode: %s\n", PB_GET_ERROR(&outstream));
+ return 1;
+ }
+
+
+ std::vector<int> destination;
+ pb_istream_t instream = pb_istream_from_buffer(&serialized.front(), outstream.bytes_written);
+ if (!pb_decode(&instream, TestMessage_fields, &destination)) {
+ fprintf(stderr, "Failed to decode: %s\n", PB_GET_ERROR(&instream));
+ return 2;
+ }
+ if (source != destination) {
+ fprintf(stderr, "Result does not match\n");
+ fprintf(stderr, "source(%lu): ", source.size());
+ for (std::vector<int>::iterator i = source.begin(); i != source.end(); ++i)
+ {
+ fprintf(stderr, "%d, ", *i);
+ }
+ fprintf(stderr, "\nencoded(%lu): ", serialized.size());
+ for (unsigned i = 0; i != std::min(serialized.size(), outstream.bytes_written); ++i) {
+ fprintf(stderr, "%#06x ", serialized[i]);
+ }
+ fprintf(stderr, "\ndestination(%lu): ", destination.size());
+ for (std::vector<int>::iterator i = destination.begin(); i != destination.end(); ++i)
+ {
+ fprintf(stderr, "%d, ", *i);
+ }
+ fprintf(stderr, "\n");
+ return 3;
+ }
+
+ return 0;
+}
diff --git a/tests/cxx_callback_datatype/message.proto b/tests/cxx_callback_datatype/message.proto
new file mode 100644
index 0000000..605f289
--- /dev/null
+++ b/tests/cxx_callback_datatype/message.proto
@@ -0,0 +1,14 @@
+syntax = "proto3";
+
+import "nanopb.proto";
+
+option(nanopb_fileopt).include = '<vector>';
+
+message SubMessage {
+ sint32 actual_value = 1;
+}
+
+message TestMessage {
+ // Instead of std::vector<SubMessage> callback handles wrapping/unwrapping of the int.
+ repeated SubMessage submessages = 1 [(nanopb).callback_datatype = "std::vector<int>"];
+}
diff --git a/tests/site_scons/site_tools/nanopb.py b/tests/site_scons/site_tools/nanopb.py
index f0f386c..de30b3e 100644
--- a/tests/site_scons/site_tools/nanopb.py
+++ b/tests/site_scons/site_tools/nanopb.py
@@ -137,18 +137,23 @@
if not os.path.isabs(d): d = os.path.relpath(d, prefix)
include_dirs += ' -I' + esc(d)
+ # when generating .pb.cpp sources, instead of pb.h generate .pb.hpp headers
+ source_extension = os.path.splitext(str(target[0]))[1]
+ header_extension = '.h' + source_extension[2:]
nanopb_flags = env['NANOPBFLAGS']
if nanopb_flags:
- nanopb_flags = '%s:.' % nanopb_flags
+ nanopb_flags = '--source-extension=%s,--header-extension=%s,%s:.' % (source_extension, header_extension, nanopb_flags)
else:
- nanopb_flags = '.'
+ nanopb_flags = '--source-extension=%s,--header-extension=%s:.' % (source_extension, header_extension)
return SCons.Action.CommandAction('$PROTOC $PROTOCFLAGS %s --nanopb_out=%s %s' % (include_dirs, nanopb_flags, srcfile),
chdir = prefix)
def _nanopb_proto_emitter(target, source, env):
basename = os.path.splitext(str(source[0]))[0]
- target.append(basename + '.pb.h')
+ source_extension = os.path.splitext(str(target[0]))[1]
+ header_extension = '.h' + source_extension[2:]
+ target.append(basename + '.pb' + header_extension)
# This is a bit of a hack. protoc include paths work the sanest
# when the working directory is the same as the source root directory.
@@ -167,6 +172,12 @@
src_suffix = '.proto',
emitter = _nanopb_proto_emitter)
+_nanopb_proto_cpp_builder = SCons.Builder.Builder(
+ generator = _nanopb_proto_actions,
+ suffix = '.pb.cpp',
+ src_suffix = '.proto',
+ emitter = _nanopb_proto_emitter)
+
def generate(env):
'''Add Builder for nanopb protos.'''
@@ -181,6 +192,7 @@
env.SetDefault(NANOPB_PROTO_CMD = '$PROTOC $PROTOCFLAGS --nanopb_out=$NANOPBFLAGS:. $SOURCES')
env['BUILDERS']['NanopbProto'] = _nanopb_proto_builder
+ env['BUILDERS']['NanopbProtoCpp'] = _nanopb_proto_cpp_builder
def exists(env):
return _detect_protoc(env) and _detect_protoc_opts(env)