Add flatbuffer to json using reflection schema

Change-Id: I6f32c0403a74ec951e6c730c4006ede3bc91099c
diff --git a/aos/BUILD b/aos/BUILD
index 8683d18..fca684b 100644
--- a/aos/BUILD
+++ b/aos/BUILD
@@ -336,6 +336,7 @@
 flatbuffer_cc_library(
     name = "json_to_flatbuffer_flatbuffer",
     srcs = ["json_to_flatbuffer.fbs"],
+    gen_reflections = 1,
 )
 
 cc_library(
@@ -359,7 +360,10 @@
 
 cc_library(
     name = "json_to_flatbuffer",
-    srcs = ["json_to_flatbuffer.cc"],
+    srcs = [
+        "flatbuffer_introspection.cc",
+        "json_to_flatbuffer.cc",
+    ],
     hdrs = ["json_to_flatbuffer.h"],
     visibility = ["//visibility:public"],
     deps = [
@@ -384,6 +388,23 @@
     ],
 )
 
+cc_test(
+    name = "flatbuffer_introspection_test",
+    srcs = [
+        "flatbuffer_introspection_test.cc",
+    ],
+    data = [
+        ":json_to_flatbuffer_flatbuffer_reflection_out",
+    ],
+    deps = [
+        ":json_to_flatbuffer",
+        ":json_to_flatbuffer_flatbuffer",
+        "//aos/testing:googletest",
+        "//aos/util:file",
+        "@com_github_google_flatbuffers//:flatbuffers",
+    ],
+)
+
 cc_library(
     name = "flatbuffer_merge",
     srcs = ["flatbuffer_merge.cc"],
@@ -440,6 +461,7 @@
         "testdata/expected.json",
         "//aos/events:config.fb.json",
         "//aos/events:ping.bfbs",
+        "//aos/events:pingpong_config.json",
         "//aos/events:pong.bfbs",
     ],
     deps = [
@@ -462,3 +484,18 @@
         "@com_github_google_glog//:glog",
     ],
 )
+
+cc_binary(
+    name = "log_fbs_shmem",
+    srcs = [
+        "log_fbs_shmem.cc",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":configuration",
+        ":json_to_flatbuffer",
+        "//aos:init",
+        "//aos/events:shm_event_loop",
+        "@com_github_google_glog//:glog",
+    ],
+)
diff --git a/aos/flatbuffer_introspection.cc b/aos/flatbuffer_introspection.cc
new file mode 100644
index 0000000..751666d
--- /dev/null
+++ b/aos/flatbuffer_introspection.cc
@@ -0,0 +1,279 @@
+#include <iostream>
+#include <sstream>
+
+#include "aos/json_to_flatbuffer.h"
+
+namespace aos {
+
+namespace {
+
+using reflection::BaseType;
+
+void IntToString(int64_t val, reflection::BaseType type,
+                 std::stringstream *out) {
+  switch (type) {
+    case BaseType::Bool:
+      *out << (val ? "true" : "false");
+      break;
+    case BaseType::UByte:
+      *out << std::to_string(static_cast<uint8_t>(val));
+      break;
+    case BaseType::Byte:
+      *out << std::to_string(static_cast<int8_t>(val));
+      break;
+    case BaseType::Short:
+      *out << static_cast<int16_t>(val);
+      break;
+    case BaseType::UShort:
+      *out << static_cast<uint16_t>(val);
+      break;
+    case BaseType::Int:
+      *out << static_cast<int32_t>(val);
+      break;
+    case BaseType::UInt:
+      *out << static_cast<uint32_t>(val);
+      break;
+    case BaseType::Long:
+      *out << static_cast<int64_t>(val);
+      break;
+    case BaseType::ULong:
+      *out << static_cast<uint64_t>(val);
+      break;
+    default:
+      *out << "null";
+  }
+}
+
+void FloatToString(double val, reflection::BaseType type,
+                   std::stringstream *out) {
+  switch (type) {
+    case BaseType::Float:
+      out->precision(std::numeric_limits<float>::digits10);
+      *out << static_cast<float>(val);
+      break;
+    case BaseType::Double:
+      out->precision(std::numeric_limits<double>::digits10);
+      *out << val;
+      break;
+    default:
+      *out << "null";
+  }
+}
+
+template <typename ObjT>
+void ObjectToString(
+    const reflection::Object *obj,
+    const flatbuffers::Vector<flatbuffers::Offset<reflection::Object>> *objects,
+    const flatbuffers::Vector<flatbuffers::Offset<reflection::Enum>> *enums,
+    const ObjT *object, std::stringstream *out);
+
+// Get enum value name
+const char *EnumToString(
+    int64_t enum_value,
+    const flatbuffers::Vector<flatbuffers::Offset<reflection::EnumVal>>
+        *values) {
+  // Replace with binary search? Enum values are pre-sorted.
+  for (auto iter = values->begin(); iter != values->end(); iter++) {
+    if (enum_value == iter->value()) {
+      return iter->name()->c_str();
+    }
+  }
+  return nullptr;
+}
+
+// Convert integer to string, checking if it is an enum.
+void IntOrEnumToString(
+    int64_t val, const reflection::Type *type,
+    const flatbuffers::Vector<flatbuffers::Offset<reflection::Enum>> *enums,
+    std::stringstream *out) {
+  // Check if integer is an enum and print string, otherwise fallback to
+  // printing as int.
+  if (type->index() > -1 && type->index() < (int32_t)enums->size()) {
+    const reflection::Enum *enum_props = enums->Get(type->index());
+    if (!enum_props->is_union()) {
+      const char *value_string = EnumToString(val, enum_props->values());
+
+      if (value_string != nullptr) {
+        *out << '"' << value_string << '"';
+      }
+    }
+  } else {
+    if (type->base_type() == BaseType::Vector ||
+        type->base_type() == BaseType::Array) {
+      IntToString(val, type->element(), out);
+    } else {
+      IntToString(val, type->base_type(), out);
+    }
+  }
+}
+
+// Print field in flatbuffer table. Field must be populated.
+template <typename ObjT>
+void FieldToString(
+    const ObjT *table, const reflection::Field *field,
+    const flatbuffers::Vector<flatbuffers::Offset<reflection::Object>> *objects,
+    const flatbuffers::Vector<flatbuffers::Offset<reflection::Enum>> *enums,
+    std::stringstream *out) {
+  const reflection::Type *type = field->type();
+
+  switch (type->base_type()) {
+    case BaseType::Bool:
+    case BaseType::UByte:
+    case BaseType::Byte:
+    case BaseType::Short:
+    case BaseType::UShort:
+    case BaseType::Int:
+    case BaseType::UInt:
+    case BaseType::Long:
+    case BaseType::ULong:
+      IntOrEnumToString(GetAnyFieldI(*table, *field), type, enums, out);
+      break;
+    case BaseType::Float:
+    case BaseType::Double:
+      FloatToString(GetAnyFieldF(*table, *field), type->base_type(), out);
+      break;
+    case BaseType::String:
+      if constexpr (std::is_same<flatbuffers::Table, ObjT>()) {
+        std::string str = flatbuffers::GetFieldS(*table, *field)->str();
+        std::string out_str;
+        out_str.reserve(str.size());
+        for (char c : str) {
+          // out_str += c;
+          switch (c) {
+            case '"':
+              out_str += "\\\"";
+              break;
+            case '\\':
+              out_str += "\\\\";
+              break;
+            case '\b':
+              out_str += "\\b";
+              break;
+            case '\f':
+              out_str += "\\f";
+              break;
+            case '\n':
+              out_str += "\\n";
+              break;
+            case '\r':
+              out_str += "\\r";
+              break;
+            case '\t':
+              out_str += "\\t";
+              break;
+            default:
+              out_str += c;
+          }
+        }
+        *out << '"' << out_str << '"';
+      } else {
+        *out << "null";
+      }
+      break;
+    case BaseType::Vector: {
+      if constexpr (std::is_same<flatbuffers::Table, ObjT>()) {
+        const flatbuffers::VectorOfAny *vector =
+            flatbuffers::GetFieldAnyV(*table, *field);
+        reflection::BaseType elem_type = type->element();
+
+        *out << '[';
+        for (flatbuffers::uoffset_t i = 0; i < vector->size(); ++i) {
+          if (i != 0) {
+            *out << ", ";
+          }
+          if (flatbuffers::IsInteger(elem_type)) {
+            IntOrEnumToString(
+                flatbuffers::GetAnyVectorElemI(vector, elem_type, i), type,
+                enums, out);
+          } else if (flatbuffers::IsFloat(elem_type)) {
+            FloatToString(flatbuffers::GetAnyVectorElemF(vector, elem_type, i),
+                          elem_type, out);
+          } else if (elem_type == BaseType::String) {
+            *out << '"' << flatbuffers::GetAnyVectorElemS(vector, elem_type, i)
+                 << '"';
+          } else if (elem_type == BaseType::Obj) {
+            if (type->index() > -1 &&
+                type->index() < (int32_t)objects->size()) {
+              if (objects->Get(type->index())->is_struct()) {
+                ObjectToString(
+                    objects->Get(type->index()), objects, enums,
+                    flatbuffers::GetAnyVectorElemAddressOf<
+                        const flatbuffers::Struct>(
+                        vector, i, objects->Get(type->index())->bytesize()),
+                    out);
+              } else {
+                ObjectToString(objects->Get(type->index()), objects, enums,
+                               flatbuffers::GetAnyVectorElemPointer<
+                                   const flatbuffers::Table>(vector, i),
+                               out);
+              }
+            }
+          }
+        }
+        *out << ']';
+      } else {
+        *out << "null";
+      }
+    } break;
+    case BaseType::Obj: {
+      if (type->index() > -1 && type->index() < (int32_t)objects->size()) {
+        if (objects->Get(type->index())->is_struct()) {
+          ObjectToString(objects->Get(type->index()), objects, enums,
+                         flatbuffers::GetFieldStruct(*table, *field), out);
+        } else if constexpr (std::is_same<flatbuffers::Table, ObjT>()) {
+          ObjectToString(objects->Get(type->index()), objects, enums,
+                         flatbuffers::GetFieldT(*table, *field), out);
+        }
+      } else {
+        *out << "null";
+      }
+    } break;
+    default:
+      *out << "null";
+  }
+}
+
+// Prints flatbuffer table or struct given list of possible child objects and
+// enums. Prints "null" if the child object type is not found.
+template <typename ObjT>
+void ObjectToString(
+    const reflection::Object *obj,
+    const flatbuffers::Vector<flatbuffers::Offset<reflection::Object>> *objects,
+    const flatbuffers::Vector<flatbuffers::Offset<reflection::Enum>> *enums,
+    const ObjT *object, std::stringstream *out) {
+  static_assert(std::is_same<flatbuffers::Table, ObjT>() ||
+                    std::is_same<flatbuffers::Struct, ObjT>(),
+                "Type must be either flatbuffer table or struct");
+  bool print_sep = false;
+  *out << '{';
+  for (const reflection::Field *field : *obj->fields()) {
+    // Check whether this object has the field populated (even for structs,
+    // which should have all fields populated)
+    if (object->GetAddressOf(field->offset())) {
+      if (print_sep) {
+        *out << ", ";
+      } else {
+        print_sep = true;
+      }
+      *out << '"' << field->name()->c_str() << "\": ";
+      FieldToString(object, field, objects, enums, out);
+    }
+  }
+  *out << '}';
+}
+
+}  // namespace
+
+std::string FlatbufferToJson(const reflection::Schema *schema,
+                             const uint8_t *data) {
+  const flatbuffers::Table *table = flatbuffers::GetAnyRoot(data);
+
+  const reflection::Object *obj = schema->root_table();
+
+  std::stringstream out;
+
+  ObjectToString(obj, schema->objects(), schema->enums(), table, &out);
+
+  return out.str();
+}
+}  // namespace aos
diff --git a/aos/flatbuffer_introspection_test.cc b/aos/flatbuffer_introspection_test.cc
new file mode 100644
index 0000000..54a2ab2
--- /dev/null
+++ b/aos/flatbuffer_introspection_test.cc
@@ -0,0 +1,317 @@
+#include "aos/json_to_flatbuffer.h"
+#include "aos/json_to_flatbuffer_generated.h"
+#include "aos/util/file.h"
+#include "flatbuffers/reflection.h"
+#include "gtest/gtest.h"
+
+namespace aos {
+namespace testing {
+
+class FlatbufferIntrospectionTest : public ::testing::Test {
+ public:
+  FlatbufferIntrospectionTest()
+      : schema_data_(
+            util::ReadFileToStringOrDie("aos/json_to_flatbuffer.bfbs")) {
+    schema_ = reflection::GetSchema(schema_data_.data());
+  }
+
+ protected:
+  FlatbufferString<reflection::Schema> schema_data_;
+  const reflection::Schema *schema_;
+};
+
+TEST_F(FlatbufferIntrospectionTest, IntegerTest) {
+  flatbuffers::FlatBufferBuilder builder;
+  ConfigurationBuilder config_builder(builder);
+
+  config_builder.add_foo_byte(-5);
+  config_builder.add_foo_ubyte(5);
+  config_builder.add_foo_bool(true);
+
+  config_builder.add_foo_short(-10);
+  config_builder.add_foo_ushort(10);
+
+  config_builder.add_foo_int(-20);
+  config_builder.add_foo_uint(20);
+
+  config_builder.add_foo_long(-100);
+  config_builder.add_foo_ulong(100);
+
+  builder.Finish(config_builder.Finish());
+
+  std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
+
+  EXPECT_EQ(out,
+            "{\"foo_bool\": true, \"foo_byte\": -5, \"foo_int\": -20, "
+            "\"foo_long\": -100, \"foo_short\": -10, \"foo_ubyte\": 5, "
+            "\"foo_uint\": 20, \"foo_ulong\": 100, \"foo_ushort\": 10}");
+}
+
+TEST_F(FlatbufferIntrospectionTest, FloatTest) {
+  flatbuffers::FlatBufferBuilder builder;
+  ConfigurationBuilder config_builder(builder);
+
+  config_builder.add_foo_float(1.0 / 3.0);
+  config_builder.add_foo_double(5.0 / 9.0);
+
+  builder.Finish(config_builder.Finish());
+
+  std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
+
+  EXPECT_EQ(out,
+            "{\"foo_double\": 0.555555555555556, \"foo_float\": 0.333333}");
+}
+
+TEST_F(FlatbufferIntrospectionTest, VectorScalarTest) {
+  flatbuffers::FlatBufferBuilder builder;
+
+  // Flatbuffers don't like creating vectors simultaneously with table, so do
+  // first.
+  auto foo_bytes = builder.CreateVector<int8_t>({-3, -2, -1, 0, 1, 2, 3});
+  auto foo_ubytes = builder.CreateVector<uint8_t>({0, 1, 2, 3, 4, 5, 6});
+  auto foo_bools = builder.CreateVector<uint8_t>({true, false, true, false});
+
+  auto foo_shorts =
+      builder.CreateVector<int16_t>({-30, -20, -10, 0, 10, 20, 30});
+  auto foo_ushorts =
+      builder.CreateVector<uint16_t>({0, 10, 20, 30, 40, 50, 60});
+
+  auto foo_ints =
+      builder.CreateVector<int32_t>({-300, -200, -100, 0, 100, 200, 300});
+  auto foo_uints =
+      builder.CreateVector<uint32_t>({0, 100, 200, 300, 400, 500, 600});
+
+  auto foo_longs =
+      builder.CreateVector<int64_t>({-3000, -2000, -1000, 0, 1000, 2000, 3000});
+  auto foo_ulongs =
+      builder.CreateVector<uint64_t>({0, 1000, 2000, 3000, 4000, 5000, 6000});
+
+  auto foo_floats =
+      builder.CreateVector<float>({0.0, 1.0 / 9.0, 2.0 / 9.0, 3.0 / 9.0});
+  auto foo_doubles =
+      builder.CreateVector<double>({0, 1.0 / 9.0, 2.0 / 9.0, 3.0 / 9.0});
+
+  ConfigurationBuilder config_builder(builder);
+  config_builder.add_vector_foo_byte(foo_bytes);
+  config_builder.add_vector_foo_ubyte(foo_ubytes);
+  config_builder.add_vector_foo_bool(foo_bools);
+
+  config_builder.add_vector_foo_short(foo_shorts);
+  config_builder.add_vector_foo_ushort(foo_ushorts);
+
+  config_builder.add_vector_foo_int(foo_ints);
+  config_builder.add_vector_foo_uint(foo_uints);
+
+  config_builder.add_vector_foo_long(foo_longs);
+  config_builder.add_vector_foo_ulong(foo_ulongs);
+
+  config_builder.add_vector_foo_float(foo_floats);
+  config_builder.add_vector_foo_double(foo_doubles);
+
+  builder.Finish(config_builder.Finish());
+
+  std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
+
+  EXPECT_EQ(
+      out,
+      "{\"vector_foo_bool\": [true, false, true, false], \"vector_foo_byte\": "
+      "[-3, -2, -1, 0, 1, 2, 3], \"vector_foo_double\": [0, 0.111111111111111, "
+      "0.222222222222222, 0.333333333333333], \"vector_foo_float\": [0, "
+      "0.111111, 0.222222, 0.333333], \"vector_foo_int\": [-300, -200, -100, "
+      "0, 100, 200, 300], \"vector_foo_long\": [-3000, -2000, -1000, 0, 1000, "
+      "2000, 3000], \"vector_foo_short\": [-30, -20, -10, 0, 10, 20, 30], "
+      "\"vector_foo_ubyte\": [0, 1, 2, 3, 4, 5, 6], \"vector_foo_uint\": [0, "
+      "100, 200, 300, 400, 500, 600], \"vector_foo_ulong\": [0, 1000, 2000, "
+      "3000, 4000, 5000, 6000], \"vector_foo_ushort\": [0, 10, 20, 30, 40, 50, "
+      "60]}");
+}
+
+TEST_F(FlatbufferIntrospectionTest, StringTest) {
+  flatbuffers::FlatBufferBuilder builder;
+
+  auto foo_string = builder.CreateString("I <3 FlatBuffers!");
+
+  ConfigurationBuilder config_builder(builder);
+  config_builder.add_foo_string(foo_string);
+
+  builder.Finish(config_builder.Finish());
+
+  std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
+
+  EXPECT_EQ(out, "{\"foo_string\": \"I <3 FlatBuffers!\"}");
+}
+
+TEST_F(FlatbufferIntrospectionTest, EnumTest) {
+  flatbuffers::FlatBufferBuilder builder;
+
+  ConfigurationBuilder config_builder(builder);
+  config_builder.add_foo_enum(BaseType_UShort);
+
+  builder.Finish(config_builder.Finish());
+
+  std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
+
+  EXPECT_EQ(out, "{\"foo_enum\": \"UShort\"}");
+}
+
+TEST_F(FlatbufferIntrospectionTest, VectorStringTest) {
+  flatbuffers::FlatBufferBuilder builder;
+
+  std::vector<std::vector<std::string>> words{
+      {"abc", "acb"}, {"bac", "bca"}, {"cab", "cba"}};
+  std::vector<flatbuffers::Offset<
+      flatbuffers::Vector<flatbuffers::Offset<flatbuffers::String>>>>
+      strings;
+
+  for (const auto &v : words) {
+    strings.push_back(builder.CreateVectorOfStrings(v));
+  }
+
+  std::vector<flatbuffers::Offset<VectorOfStrings>> sub_vectors;
+
+  for (const auto &v : strings) {
+    VectorOfStringsBuilder v_builder(builder);
+    v_builder.add_str(v);
+    sub_vectors.push_back(v_builder.Finish());
+  }
+
+  auto foo_vov = builder.CreateVector(sub_vectors);
+
+  VectorOfVectorOfStringBuilder vov_builder(builder);
+  vov_builder.add_v(foo_vov);
+  auto vov = vov_builder.Finish();
+
+  ConfigurationBuilder config_builder(builder);
+  config_builder.add_vector_foo_string(strings[0]);
+  config_builder.add_vov(vov);
+
+  builder.Finish(config_builder.Finish());
+
+  std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
+
+  EXPECT_EQ(out,
+            "{\"vector_foo_string\": [\"abc\", \"acb\"], \"vov\": {\"v\": "
+            "[{\"str\": [\"abc\", \"acb\"]}, {\"str\": [\"bac\", \"bca\"]}, "
+            "{\"str\": [\"cab\", \"cba\"]}]}}");
+}
+
+TEST_F(FlatbufferIntrospectionTest, TableTest) {
+  flatbuffers::FlatBufferBuilder builder;
+
+  auto foo_string2 = builder.CreateString("Nested Config String");
+  auto foo_bytes2 = builder.CreateVector<int8_t>({6, 7, 8, 9, 10});
+
+  ConfigurationBuilder config_builder2(builder);
+  config_builder2.add_foo_byte(10);
+  config_builder2.add_foo_string(foo_string2);
+  config_builder2.add_vector_foo_byte(foo_bytes2);
+
+  flatbuffers::Offset<Configuration> config_2 = config_builder2.Finish();
+
+  auto foo_string = builder.CreateString("Root Config String");
+  auto foo_bytes = builder.CreateVector<int8_t>({0, 1, 2, 3, 4, 5});
+
+  ConfigurationBuilder config_builder(builder);
+  config_builder.add_nested_config(config_2);
+  config_builder.add_foo_byte(5);
+  config_builder.add_foo_string(foo_string);
+  config_builder.add_vector_foo_byte(foo_bytes);
+
+  builder.Finish(config_builder.Finish());
+
+  std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
+
+  EXPECT_EQ(out,
+            "{\"foo_byte\": 5, \"foo_string\": \"Root Config String\", "
+            "\"nested_config\": {\"foo_byte\": 10, \"foo_string\": \"Nested "
+            "Config String\", \"vector_foo_byte\": [6, 7, 8, 9, 10]}, "
+            "\"vector_foo_byte\": [0, 1, 2, 3, 4, 5]}");
+}
+
+TEST_F(FlatbufferIntrospectionTest, StructTest) {
+  flatbuffers::FlatBufferBuilder builder;
+
+  FooStructNested foo_struct2(10);
+
+  FooStruct foo_struct(5, foo_struct2);
+
+  ConfigurationBuilder config_builder(builder);
+  config_builder.add_foo_struct(&foo_struct);
+
+  builder.Finish(config_builder.Finish());
+
+  std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
+
+  EXPECT_EQ(out,
+            "{\"foo_struct\": {\"foo_byte\": 5, \"nested_struct\": "
+            "{\"foo_byte\": 10}}}");
+}
+
+TEST_F(FlatbufferIntrospectionTest, VectorStructTest) {
+  flatbuffers::FlatBufferBuilder builder;
+
+  FooStructNested foo_struct2(1);
+
+  auto structs = builder.CreateVectorOfStructs(
+      std::vector<FooStruct>({{5, foo_struct2}, {10, foo_struct2}}));
+
+  ConfigurationBuilder config_builder(builder);
+  config_builder.add_vector_foo_struct(structs);
+
+  builder.Finish(config_builder.Finish());
+
+  std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
+
+  EXPECT_EQ(out,
+            "{\"vector_foo_struct\": [{\"foo_byte\": 5, \"nested_struct\": "
+            "{\"foo_byte\": 1}}, {\"foo_byte\": 10, \"nested_struct\": "
+            "{\"foo_byte\": 1}}]}");
+}
+
+TEST_F(FlatbufferIntrospectionTest, VectorEnumTest) {
+  flatbuffers::FlatBufferBuilder builder;
+
+  auto enums = builder.CreateVector<int8_t>(
+      {BaseType_UShort, BaseType_Obj, BaseType_UInt});
+
+  ConfigurationBuilder config_builder(builder);
+  config_builder.add_vector_foo_enum(enums);
+
+  builder.Finish(config_builder.Finish());
+
+  std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
+
+  EXPECT_EQ(out, "{\"vector_foo_enum\": [\"UShort\", \"Obj\", \"UInt\"]}");
+}
+
+TEST_F(FlatbufferIntrospectionTest, StructEnumTest) {
+  flatbuffers::FlatBufferBuilder builder;
+
+  StructEnum foo_struct(BaseType_UShort);
+
+  ConfigurationBuilder config_builder(builder);
+  config_builder.add_foo_struct_enum(&foo_struct);
+
+  builder.Finish(config_builder.Finish());
+
+  std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
+
+  EXPECT_EQ(out, "{\"foo_struct_enum\": {\"foo_enum\": \"UShort\"}}");
+}
+
+TEST_F(FlatbufferIntrospectionTest, StringEscapeTest) {
+  flatbuffers::FlatBufferBuilder builder;
+
+  auto foo_string = builder.CreateString("\"\\\b\f\n\r\t");
+
+  ConfigurationBuilder config_builder(builder);
+  config_builder.add_foo_string(foo_string);
+
+  builder.Finish(config_builder.Finish());
+
+  std::string out = FlatbufferToJson(schema_, builder.GetBufferPointer());
+  EXPECT_EQ(out, "{\"foo_string\": \"\\\"\\\\\\b\\f\\n\\r\\t\"}");
+}
+
+}  // namespace testing
+}  // namespace aos
diff --git a/aos/json_to_flatbuffer.fbs b/aos/json_to_flatbuffer.fbs
index 8942cc8..8c0402c 100644
--- a/aos/json_to_flatbuffer.fbs
+++ b/aos/json_to_flatbuffer.fbs
@@ -47,6 +47,19 @@
   v:[VectorOfStrings];
 }
 
+struct FooStructNested {
+  foo_byte:byte;
+}
+
+struct FooStruct {
+  foo_byte:byte;
+  nested_struct:FooStructNested;
+}
+
+struct StructEnum {
+  foo_enum:BaseType;
+}
+
 table Configuration {
   locations:[Location] (id: 0);
   maps:[Map] (id: 1);
@@ -108,6 +121,10 @@
   nested_config:Configuration (id: 32);
 
   vov:VectorOfVectorOfString (id: 33);
+
+  foo_struct:FooStruct (id: 34);
+  vector_foo_struct:[FooStruct] (id: 35);
+  foo_struct_enum:StructEnum (id: 36);
 }
 
 root_type Configuration;
diff --git a/aos/json_to_flatbuffer.h b/aos/json_to_flatbuffer.h
index 94b982a..f90a602 100644
--- a/aos/json_to_flatbuffer.h
+++ b/aos/json_to_flatbuffer.h
@@ -7,13 +7,13 @@
 
 #include "aos/flatbuffers.h"
 #include "flatbuffers/flatbuffers.h"
+#include "flatbuffers/reflection.h"
 
 namespace aos {
 
 // Parses the flatbuffer into the vector, or returns an empty vector.
 flatbuffers::DetachedBuffer JsonToFlatbuffer(
-    const std::string_view data,
-    const flatbuffers::TypeTable *typetable);
+    const std::string_view data, const flatbuffers::TypeTable *typetable);
 
 // Converts a flatbuffer into a Json string.
 // multi_line controls if the Json is written out on multiple lines or one.
@@ -53,6 +53,9 @@
       Flatbuffer<T>::MiniReflectTypeTable(), multi_line);
 }
 
+std::string FlatbufferToJson(const reflection::Schema *const schema,
+                             const uint8_t *const data);
+
 }  // namespace aos
 
 #endif  // AOS_JSON_TO_FLATBUFFER_H_
diff --git a/aos/log_fbs_shmem.cc b/aos/log_fbs_shmem.cc
new file mode 100644
index 0000000..c2ed056
--- /dev/null
+++ b/aos/log_fbs_shmem.cc
@@ -0,0 +1,66 @@
+#include <iostream>
+#include <map>
+
+#include "aos/configuration.h"
+#include "aos/events/shm_event_loop.h"
+#include "aos/init.h"
+#include "aos/json_to_flatbuffer.h"
+#include "gflags/gflags.h"
+
+DEFINE_string(config, "./config.json", "File path of aos configuration");
+int main(int argc, char **argv) {
+  aos::InitGoogle(&argc, &argv);
+
+  std::string channel_name;
+  std::string message_type;
+  if (argc > 1) {
+    channel_name = argv[1];
+  }
+  if (argc > 2) {
+    message_type = argv[2];
+  }
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(FLAGS_config);
+
+  const aos::Configuration *config_msg = &config.message();
+  ::aos::ShmEventLoop event_loop(config_msg);
+
+  if (argc == 1) {
+    std::cout << "Channels:\n";
+    for (const aos::Channel *channel : *config_msg->channels()) {
+      std::cout << channel->name()->c_str() << ' ' << channel->type()->c_str()
+                << '\n';
+    }
+    return 0;
+  }
+
+  int found_channels = 0;
+  const flatbuffers::Vector<flatbuffers::Offset<aos::Channel>> *channels =
+      config_msg->channels();
+  for (const aos::Channel *channel : *channels) {
+    if (channel->name()->c_str() == channel_name &&
+        channel->type()->str().find(message_type) != std::string::npos) {
+      event_loop.MakeRawWatcher(
+          channel,
+          [channel](const aos::Context /* &context*/, const void *message) {
+            LOG(INFO) << '(' << channel->type()->c_str() << ") "
+                      << aos::FlatbufferToJson(
+                             channel->schema(),
+                             static_cast<const uint8_t *>(message))
+                      << '\n';
+          });
+      found_channels++;
+    }
+  }
+
+  if (found_channels == 0) {
+    LOG(FATAL) << "Could not find any channels with the given name and type.";
+  } else if (found_channels > 1 && message_type.size() != 0) {
+    LOG(FATAL) << "Multiple channels found with same type";
+  }
+
+  event_loop.Run();
+  ::aos::Cleanup();
+  return 0;
+}