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)