DCCL v5
Loading...
Searching...
No Matches
pb_plugin.cpp
1// Copyright 2015-2023:
2// GobySoft, LLC (2013-)
3// Community contributors (see AUTHORS file)
4// File authors:
5// Toby Schneider <toby@gobysoft.org>
6// Stephanie Petillo <stephanie@gobysoft.org>
7// Nathan Knotts <nknotts@gmail.com>
8//
9//
10// This file is part of the Dynamic Compact Control Language Library
11// ("DCCL").
12//
13// DCCL is free software: you can redistribute it and/or modify
14// it under the terms of the GNU Lesser General Public License as published by
15// the Free Software Foundation, either version 2.1 of the License, or
16// (at your option) any later version.
17//
18// DCCL is distributed in the hope that it will be useful,
19// but WITHOUT ANY WARRANTY; without even the implied warranty of
20// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21// GNU Lesser General Public License for more details.
22//
23// You should have received a copy of the GNU Lesser General Public License
24// along with DCCL. If not, see <http://www.gnu.org/licenses/>.
25#include "gen_units_class_plugin.h"
26#include "option_extensions.pb.h"
27#include <algorithm>
28#include <cctype>
29#include <fstream>
30#include <google/protobuf/compiler/code_generator.h>
31#include <google/protobuf/compiler/plugin.h>
32#include <google/protobuf/descriptor.h>
33#include <google/protobuf/io/printer.h>
34#include <google/protobuf/io/zero_copy_stream.h>
35#include <iostream>
36#include <memory>
37#include <set>
38#include <sstream>
39#include <filesystem>
40
41std::set<std::string> systems_to_include_;
42std::set<std::string> base_units_to_include_;
43std::set<std::string> custom_unit_headers_to_include_;
44std::string filename_h_;
45std::string load_file_cpp_;
46std::shared_ptr<std::fstream> load_file_output_;
47
48std::string load_file_base_{
49 R"DEL(#include <dccl/codec.h>
50
51// DO NOT REMOVE: required by loader code
52std::vector<const google::protobuf::Descriptor*> descriptors;
53template<typename PB> struct DCCLLoader { DCCLLoader() { descriptors.push_back(PB::descriptor()); }};
54// END: required by loader code
55
56
57extern "C"
58{
59 void dccl3_load(dccl::Codec* dccl)
60 {
61 // DO NOT REMOVE: required by loader code
62 for(auto d : descriptors)
63 dccl->load(d);
64 // END: required by loader code
65 }
66
67 void dccl3_unload(dccl::Codec* dccl)
68 {
69 // DO NOT REMOVE: required by loader code
70 for(auto d : descriptors)
71 dccl->unload(d);
72 // END: required by loader code
73 }
74}
75
76// BEGIN (TO END OF FILE): AUTOGENERATED LOADERS
77)DEL"};
78
80{
81 public:
82 DCCLGenerator() = default;
83 ~DCCLGenerator() override = default;
84
85 // implements CodeGenerator ----------------------------------------
86 bool Generate(const google::protobuf::FileDescriptor* file, const std::string& parameter,
87 google::protobuf::compiler::GeneratorContext* generator_context,
88 std::string* error) const override;
89
90 uint64_t GetSupportedFeatures() const override
91 {
92 return FEATURE_PROTO3_OPTIONAL;
93 }
94
95 private:
96 void generate_message(
97 const google::protobuf::Descriptor* desc,
98 google::protobuf::compiler::GeneratorContext* generator_context,
99 std::shared_ptr<std::string> message_unit_system = std::shared_ptr<std::string>()) const;
100 void generate_field(const google::protobuf::FieldDescriptor* field,
101 google::protobuf::io::Printer* printer,
102 std::shared_ptr<std::string> message_unit_system) const;
103 bool check_field_type(const google::protobuf::FieldDescriptor* field) const;
104
105 void generate_load_file_headers() const;
106 void generate_load_file_message_loader(const google::protobuf::Descriptor* desc) const;
107};
108
109bool DCCLGenerator::check_field_type(const google::protobuf::FieldDescriptor* field) const
110{
111 bool is_integer = field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_INT32 ||
112 field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_INT64 ||
113 field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_UINT32 ||
114 field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_UINT64;
115
116 bool is_float = field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE ||
117 field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_FLOAT;
118
119 if (!is_float && !is_integer)
120 {
121 throw std::runtime_error("Can only use (dccl.field).units on numeric fields");
122 }
123 return is_integer;
124}
125
126bool DCCLGenerator::Generate(const google::protobuf::FileDescriptor* file,
127 const std::string& parameter,
128 google::protobuf::compiler::GeneratorContext* generator_context,
129 std::string* error) const
130{
131 std::vector<std::pair<std::string, std::string>> options;
132 google::protobuf::compiler::ParseGeneratorParameter(parameter, &options);
133
134 for (const auto& [key, value] : options)
135 {
136 if (key == "dccl3_load_file")
137 {
138 load_file_cpp_ = value;
139 load_file_output_ = std::make_shared<std::fstream>(
140 load_file_cpp_, std::ios::in | std::ios::out | std::ios::app);
141
142 if (!load_file_output_->is_open())
143 {
144 *error = "Failed to open dccl3_load_file: " + load_file_cpp_;
145 return false;
146 }
147 }
148 else
149 {
150 *error = "Unknown parameter: " + key;
151 return false;
152 }
153 }
154
155 try
156 {
157 const std::string filename(file->name());
158 filename_h_ = filename.substr(0, filename.find(".proto")) + ".pb.h";
159 // std::string filename_cc = filename.substr(0, filename.find(".proto")) + ".pb.cc";
160
161 if (load_file_output_)
162 generate_load_file_headers();
163
164 for (int message_i = 0, message_n = file->message_type_count(); message_i < message_n;
165 ++message_i)
166 {
167 try
168 {
169 generate_message(file->message_type(message_i), generator_context);
170 }
171 catch (std::exception& e)
172 {
173 throw(
174 std::runtime_error(std::string("Failed to generate DCCL code: \n") + e.what()));
175 }
176 }
177
178 std::shared_ptr<google::protobuf::io::ZeroCopyOutputStream> include_output(
179 generator_context->OpenForInsert(filename_h_, "includes"));
180 google::protobuf::io::Printer include_printer(include_output.get(), '$');
181 std::stringstream includes_ss;
182
183 includes_ss << "#include <boost/units/quantity.hpp>" << std::endl;
184 includes_ss << "#include <boost/units/absolute.hpp>" << std::endl;
185 includes_ss << "#include <boost/units/dimensionless_type.hpp>" << std::endl;
186 includes_ss << "#include <boost/units/make_scaled_unit.hpp>" << std::endl;
187
188 for (const auto& it : systems_to_include_) include_units_headers(it, includes_ss);
189 for (const auto& it : base_units_to_include_) include_base_unit_headers(it, includes_ss);
190 for (const auto& it : custom_unit_headers_to_include_)
191 include_custom_unit_headers(it, includes_ss);
192
193 include_printer.Print(includes_ss.str().c_str());
194
195 return true;
196 }
197 catch (std::exception& e)
198 {
199 *error = e.what();
200 return false;
201 }
202}
203
204void DCCLGenerator::generate_message(
205 const google::protobuf::Descriptor* desc,
206 google::protobuf::compiler::GeneratorContext* generator_context,
207 std::shared_ptr<std::string> message_unit_system) const
208{
209 try
210 {
211 {
212 std::shared_ptr<google::protobuf::io::ZeroCopyOutputStream> output(
213 generator_context->OpenForInsert(filename_h_, "class_scope:" + std::string(desc->full_name())));
214 google::protobuf::io::Printer printer(output.get(), '$');
215
216 if (desc->options().HasExtension(dccl::msg))
217 {
218 if (desc->options().GetExtension(dccl::msg).id() != 0)
219 {
220 std::stringstream id_enum;
221 id_enum << "enum DCCLParameters { DCCL_ID = "
222 << desc->options().GetExtension(dccl::msg).id() << ", "
223 << " DCCL_MAX_BYTES = "
224 << desc->options().GetExtension(dccl::msg).max_bytes() << " };\n";
225 printer.Print(id_enum.str().c_str());
226
227 if (load_file_output_)
228 generate_load_file_message_loader(desc);
229 }
230
231 // set message level unit system - used if fields do not specify
232 const dccl::DCCLMessageOptions& dccl_msg_options =
233 desc->options().GetExtension(dccl::msg);
234 if (dccl_msg_options.has_unit_system())
235 {
236 message_unit_system.reset(new std::string(dccl_msg_options.unit_system()));
237 systems_to_include_.insert(dccl_msg_options.unit_system());
238 }
239 }
240
241 for (int field_i = 0, field_n = desc->field_count(); field_i < field_n; ++field_i)
242 {
243 generate_field(desc->field(field_i), &printer, message_unit_system);
244 }
245
246 for (int nested_type_i = 0, nested_type_n = desc->nested_type_count();
247 nested_type_i < nested_type_n; ++nested_type_i)
248 generate_message(desc->nested_type(nested_type_i), generator_context,
249 message_unit_system);
250 }
251 }
252 catch (std::exception& e)
253 {
254 throw(std::runtime_error(std::string("Message: \n") + std::string(desc->full_name()) + "\n" + e.what()));
255 }
256}
257
258void DCCLGenerator::generate_field(const google::protobuf::FieldDescriptor* field,
259 google::protobuf::io::Printer* printer,
260 std::shared_ptr<std::string> message_unit_system) const
261{
262 try
263 {
264 const dccl::DCCLFieldOptions& dccl_field_options =
265 field->options().GetExtension(dccl::field);
266
267 if (!dccl_field_options.has_units())
268 {
269 return;
270 }
271
272 // units are generated as boost::units quantities over the field's own
273 // type, so the field has to be numeric
274 check_field_type(field);
275
276 if ((dccl_field_options.units().has_base_dimensions() &&
277 dccl_field_options.units().has_derived_dimensions()) ||
278 (dccl_field_options.units().has_base_dimensions() &&
279 dccl_field_options.units().has_unit()) ||
280 (dccl_field_options.units().has_unit() &&
281 dccl_field_options.units().has_derived_dimensions()))
282 {
283 throw(std::runtime_error("May define either (dccl.field).units.base_dimensions or "
284 "(dccl.field).units.derived_dimensions or "
285 "(dccl.field).units.unit, but not more than one."));
286 }
287 else if (dccl_field_options.units().has_unit())
288 {
289 std::stringstream new_methods;
290
291 construct_units_typedef_from_base_unit(
292 std::string(field->name()), dccl_field_options.units().unit(),
293 dccl_field_options.units().relative_temperature(),
294 dccl_field_options.units().prefix(), new_methods);
295 construct_field_class_plugin(std::string(field->name()), new_methods,
296 dccl::units::get_field_type_name(field->cpp_type()),
297 field->is_repeated());
298 printer->Print(new_methods.str().c_str());
299 base_units_to_include_.insert(dccl_field_options.units().unit());
300 }
301 else if (dccl_field_options.units().has_custom())
302 {
303 std::stringstream new_methods;
304
305 construct_units_typedef_from_custom_unit(
306 std::string(field->name()), dccl_field_options.units().custom().unit(),
307 dccl_field_options.units().relative_temperature(),
308 dccl_field_options.units().prefix(), new_methods);
309 construct_field_class_plugin(std::string(field->name()), new_methods,
310 dccl::units::get_field_type_name(field->cpp_type()),
311 field->is_repeated());
312 printer->Print(new_methods.str().c_str());
313
314 if (dccl_field_options.units().custom().has_header())
315 custom_unit_headers_to_include_.insert(
316 dccl_field_options.units().custom().header());
317 }
318 else if (dccl_field_options.units().has_base_dimensions())
319 {
320 std::stringstream new_methods;
321
322 std::vector<double> powers;
323 std::vector<std::string> short_dimensions;
324 std::vector<std::string> dimensions;
325 if (dccl::units::parse_base_dimensions(
326 dccl_field_options.units().base_dimensions().begin(),
327 dccl_field_options.units().base_dimensions().end(), powers, short_dimensions,
328 dimensions))
329 {
330 if (!dccl_field_options.units().has_system() && !message_unit_system)
331 throw(std::runtime_error(
332 std::string("Field must have 'system' defined or message must have "
333 "'unit_system' defined when using 'base_dimensions'.")));
334
335 // default to system set in the field, otherwise use the system set at the message level
336 const std::string unit_system =
337 (!dccl_field_options.units().has_system() && message_unit_system)
338 ? *message_unit_system
339 : std::string(dccl_field_options.units().system());
340
341 dccl::units::validate_dimensions_for_system(dimensions, unit_system);
342
343 construct_base_dims_typedef(dimensions, powers, std::string(field->name()), unit_system,
344 dccl_field_options.units().relative_temperature(),
345 dccl_field_options.units().prefix(), new_methods);
346
347 construct_field_class_plugin(std::string(field->name()), new_methods,
348 dccl::units::get_field_type_name(field->cpp_type()),
349 field->is_repeated());
350 printer->Print(new_methods.str().c_str());
351 systems_to_include_.insert(unit_system);
352 }
353 else
354 {
355 throw(std::runtime_error(std::string("Failed to parse base_dimensions string: \"") +
356 std::string(dccl_field_options.units().base_dimensions()) +
357 "\""));
358 }
359 }
360 else if (dccl_field_options.units().has_derived_dimensions())
361 {
362 std::stringstream new_methods;
363
364 std::vector<std::string> operators;
365 std::vector<std::string> dimensions;
366 if (dccl::units::parse_derived_dimensions(
367 dccl_field_options.units().derived_dimensions().begin(),
368 dccl_field_options.units().derived_dimensions().end(), operators, dimensions))
369 {
370 if (!dccl_field_options.units().has_system() && !message_unit_system)
371 throw(std::runtime_error(
372 std::string("Field must have 'system' defined or message must have "
373 "'unit_system' defined when using 'derived_dimensions'.")));
374 const std::string unit_system =
375 (!dccl_field_options.units().has_system() && message_unit_system)
376 ? *message_unit_system
377 : std::string(dccl_field_options.units().system());
378
379 dccl::units::validate_dimensions_for_system(dimensions, unit_system);
380
381 construct_derived_dims_typedef(dimensions, operators, std::string(field->name()), unit_system,
382 dccl_field_options.units().relative_temperature(),
383 dccl_field_options.units().prefix(), new_methods);
384
385 construct_field_class_plugin(std::string(field->name()), new_methods,
386 dccl::units::get_field_type_name(field->cpp_type()),
387 field->is_repeated());
388 printer->Print(new_methods.str().c_str());
389 systems_to_include_.insert(unit_system);
390 }
391 else
392 {
393 throw(std::runtime_error(
394 std::string("Failed to parse derived_dimensions string: \"") +
395 std::string(dccl_field_options.units().derived_dimensions()) + "\""));
396 }
397 }
398 }
399 catch (std::exception& e)
400 {
401 throw(
402 std::runtime_error(std::string("Field: \n" + field->DebugString() + "\n" + e.what())));
403 }
404}
405
406void DCCLGenerator::generate_load_file_headers() const
407{
408 bool file_is_empty = std::filesystem::is_empty(load_file_cpp_);
409
410 bool header_already_written = false;
411 if (file_is_empty)
412 {
413 *load_file_output_ << load_file_base_ << std::flush;
414 }
415 else
416 {
417 for (std::string line; getline(*load_file_output_, line);)
418 {
419 if (line.find("\"" + filename_h_ + "\"") != std::string::npos)
420 {
421 header_already_written = true;
422 break;
423 }
424 }
425 }
426 // clear EOF
427 load_file_output_->clear();
428 // seek to the end
429 load_file_output_->seekp(0, std::ios_base::end);
430
431 if (!header_already_written)
432 *load_file_output_ << "#include \"" << filename_h_ << "\"" << std::endl;
433}
434
435void DCCLGenerator::generate_load_file_message_loader(
436 const google::protobuf::Descriptor* desc) const
437{
438 // seek to the beginning
439 load_file_output_->seekp(0, std::ios_base::beg);
440
441 // cpp class name
442 std::string cpp_name(desc->full_name());
443 {
444 std::string::size_type pos = 0;
445 while ((pos = cpp_name.find('.', pos)) != std::string::npos)
446 {
447 cpp_name.replace(pos, 1, "::");
448 pos += 2;
449 }
450 }
451
452 // lower case class name with underscores
453 std::string loader_name = std::string(desc->full_name()) + "_loader";
454 {
455 std::string::size_type pos = 0;
456 while ((pos = loader_name.find('.', pos)) != std::string::npos)
457 {
458 loader_name.replace(pos, 1, "_");
459 pos += 1;
460 }
461 }
462 std::transform(loader_name.begin(), loader_name.end(), loader_name.begin(),
463 [](unsigned char c) { return std::tolower(c); });
464
465 bool loader_already_written = false;
466 for (std::string line; getline(*load_file_output_, line);)
467 {
468 if (line.find("<" + cpp_name + ">") != std::string::npos)
469 {
470 loader_already_written = true;
471 break;
472 }
473 }
474 load_file_output_->clear();
475 load_file_output_->seekp(0, std::ios_base::end);
476
477 if (!loader_already_written)
478 *load_file_output_ << "DCCLLoader<" << cpp_name << "> " << loader_name << ";" << std::endl;
479}
480
481int main(int argc, char* argv[])
482{
483 DCCLGenerator generator;
484 return google::protobuf::compiler::PluginMain(argc, argv, &generator);
485}