[{"id":800,"web_url":"https://patchwork.libcamera.org/comment/800/","msgid":"<20190213105730.GP31044@bigcity.dyn.berto.se>","date":"2019-02-13T10:57:30","subject":"Re: [libcamera-devel] [PATCH 3/3] libcamera: signal: Disconnect\n\tsignal automatically on slot deletion","submitter":{"id":5,"url":"https://patchwork.libcamera.org/api/people/5/","name":"Niklas Söderlund","email":"niklas.soderlund@ragnatech.se"},"content":"Hi Laurent,\n\nThanks for your patch^H^H^H^H^H magic,\n\nOn 2019-02-13 00:37:02 +0200, Laurent Pinchart wrote:\n> When a signal is connected to a member function slot, the slot is not\n> disconnected when the slot object is deleted. This can lead to calling a\n> member function of a deleted object if the signal isn't disconnected\n> manually by the slot object's destructor.\n> \n> Make signal handling easier by implementing a base Object class that\n> tracks all connected signals and disconnects from them automatically\n> when the object is deleted, using template specialization resolution in\n> the Signal class.\n> \n> As inheriting from the Object class may to a too harsh requirement for\n> Signal usage in applications, keep the existing behaviour working if the\n> slot doesn't inherit from the Object class. We may reconsider this later\n> and require all slot objects to inherit from the Object class.\n> \n> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n\nThis was quiet fun to review and I still remembers how all \nstd::enable_if<std::is_base_of<...>> works, lets see for how long that \nsticks in my head...\n\nThere are some small style fixups (see bellow) which could be moved to \n2/3 but nothing to get your knickers in a twist over,\n\nReviewed-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>\n\n> ---\n>  Documentation/Doxyfile.in     |   4 +-\n>  include/libcamera/meson.build |   1 +\n>  include/libcamera/object.h    |  35 +++++++++++\n>  include/libcamera/signal.h    | 115 +++++++++++++++++++++++-----------\n>  src/libcamera/meson.build     |   1 +\n>  src/libcamera/object.cpp      |  50 +++++++++++++++\n>  src/libcamera/signal.cpp      |   5 ++\n>  test/signal.cpp               |  37 +++++++++++\n>  8 files changed, 211 insertions(+), 37 deletions(-)\n>  create mode 100644 include/libcamera/object.h\n>  create mode 100644 src/libcamera/object.cpp\n> \n> diff --git a/Documentation/Doxyfile.in b/Documentation/Doxyfile.in\n> index b78fb3a0b0b6..3e2b7fd9da0e 100644\n> --- a/Documentation/Doxyfile.in\n> +++ b/Documentation/Doxyfile.in\n> @@ -860,7 +860,9 @@ EXCLUDE_PATTERNS       =\n>  # Note that the wildcards are matched against the file with absolute path, so to\n>  # exclude all test directories use the pattern */test/*\n>  \n> -EXCLUDE_SYMBOLS        = libcamera::SlotBase \\\n> +EXCLUDE_SYMBOLS        = libcamera::SignalBase \\\n> +                         libcamera::SlotArgs \\\n> +                         libcamera::SlotBase \\\n>                           libcamera::SlotMember \\\n>                           libcamera::SlotStatic\n>  \n> diff --git a/include/libcamera/meson.build b/include/libcamera/meson.build\n> index 5788e9bbdf3e..3f4d1e28208b 100644\n> --- a/include/libcamera/meson.build\n> +++ b/include/libcamera/meson.build\n> @@ -5,6 +5,7 @@ libcamera_api = files([\n>      'event_dispatcher.h',\n>      'event_notifier.h',\n>      'libcamera.h',\n> +    'object.h',\n>      'request.h',\n>      'signal.h',\n>      'stream.h',\n> diff --git a/include/libcamera/object.h b/include/libcamera/object.h\n> new file mode 100644\n> index 000000000000..eadd41f9a41f\n> --- /dev/null\n> +++ b/include/libcamera/object.h\n> @@ -0,0 +1,35 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2019, Google Inc.\n> + *\n> + * object.h - Base object\n> + */\n> +#ifndef __LIBCAMERA_OBJECT_H__\n> +#define __LIBCAMERA_OBJECT_H__\n> +\n> +#include <list>\n> +\n> +namespace libcamera {\n> +\n> +class SignalBase;\n> +template<typename... Args>\n> +class Signal;\n> +\n> +class Object\n> +{\n> +public:\n> +\tvirtual ~Object();\n> +\n> +private:\n> +\ttemplate<typename... Args>\n> +\tfriend class Signal;\n> +\n> +\tvoid connect(SignalBase *signal);\n> +\tvoid disconnect(SignalBase *signal);\n> +\n> +\tstd::list<SignalBase *> signals_;\n> +};\n> +\n> +}; /* namespace libcamera */\n> +\n> +#endif /* __LIBCAMERA_OBJECT_H__ */\n> diff --git a/include/libcamera/signal.h b/include/libcamera/signal.h\n> index 85dc481d6957..434fc0109f42 100644\n> --- a/include/libcamera/signal.h\n> +++ b/include/libcamera/signal.h\n> @@ -10,32 +10,47 @@\n>  #include <list>\n>  #include <vector>\n>  \n> +#include <libcamera/object.h>\n> +\n>  namespace libcamera {\n>  \n>  template<typename... Args>\n>  class Signal;\n>  \n> -template<typename... Args>\n>  class SlotBase\n>  {\n>  public:\n> -\tSlotBase(void *obj)\n> -\t\t: obj_(obj) { }\n> -\tvirtual ~SlotBase() { }\n> +\tSlotBase(void *obj, bool isObject)\n> +\t\t: obj_(obj), isObject_(isObject) {}\n> +\tvirtual ~SlotBase() {}\n\nThe s/{ }/{}/ could be moved to 2/3.\n\n>  \n> -\tvirtual void invoke(Args... args) = 0;\n> +\tvoid *obj() { return obj_; }\n> +\tbool isObject() const { return isObject_; }\n>  \n>  protected:\n> -\tfriend class Signal<Args...>;\n>  \tvoid *obj_;\n> +\tbool isObject_;\n> +};\n> +\n> +template<typename... Args>\n> +class SlotArgs : public SlotBase\n> +{\n> +public:\n> +\tSlotArgs(void *obj, bool isObject)\n> +\t\t: SlotBase(obj, isObject) {}\n> +\n> +\tvirtual void invoke(Args... args) = 0;\n> +\n> +protected:\n> +\tfriend class Signal<Args...>;\n>  };\n>  \n>  template<typename T, typename... Args>\n> -class SlotMember : public SlotBase<Args...>\n> +class SlotMember : public SlotArgs<Args...>\n>  {\n>  public:\n> -\tSlotMember(T *obj, void (T::*func)(Args...))\n> -\t\t: SlotBase<Args...>(obj), func_(func) { }\n> +\tSlotMember(T *obj, bool isObject, void (T::*func)(Args...))\n> +\t\t: SlotArgs<Args...>(obj, isObject), func_(func) {}\n\nThe s/{ }/{}/ could be moved to 2/3.\n\n>  \n>  \tvoid invoke(Args... args) { (static_cast<T *>(this->obj_)->*func_)(args...); }\n>  \n> @@ -45,11 +60,11 @@ private:\n>  };\n>  \n>  template<typename... Args>\n> -class SlotStatic : public SlotBase<Args...>\n> +class SlotStatic : public SlotArgs<Args...>\n>  {\n>  public:\n>  \tSlotStatic(void (*func)(Args...))\n> -\t\t: SlotBase<Args...>(nullptr), func_(func) { }\n> +\t\t: SlotArgs<Args...>(nullptr, false), func_(func) {}\n\nThe s/{ }/{}/ could be moved to 2/3.\n\n>  \n>  \tvoid invoke(Args... args) { (*func_)(args...); }\n>  \n> @@ -58,21 +73,57 @@ private:\n>  \tvoid (*func_)(Args...);\n>  };\n>  \n> +class SignalBase\n> +{\n> +public:\n> +\ttemplate<typename T>\n> +\tvoid disconnect(T *object)\n> +\t{\n> +\t\tfor (auto iter = slots_.begin(); iter != slots_.end(); ) {\n> +\t\t\tSlotBase *slot = *iter;\n> +\t\t\tif (slot->obj() == object) {\n> +\t\t\t\titer = slots_.erase(iter);\n> +\t\t\t\tdelete slot;\n> +\t\t\t} else {\n> +\t\t\t\t++iter;\n> +\t\t\t}\n> +\t\t}\n> +\t}\n> +\n> +protected:\n> +\tfriend class Object;\n> +\tstd::list<SlotBase *> slots_;\n> +};\n> +\n>  template<typename... Args>\n> -class Signal\n> +class Signal : public SignalBase\n>  {\n>  public:\n>  \tSignal() { }\n>  \t~Signal()\n>  \t{\n> -\t\tfor (SlotBase<Args...> *slot : slots_)\n> +\t\tfor (SlotBase *slot : slots_) {\n> +\t\t\tif (slot->isObject())\n> +\t\t\t\tstatic_cast<Object *>(slot->obj())->disconnect(this);\n>  \t\t\tdelete slot;\n> +\t\t}\n>  \t}\n>  \n> +#ifndef __DOXYGEN__\n> +\ttemplate<typename T, typename std::enable_if<std::is_base_of<Object, T>::value>::type * = nullptr>\n> +\tvoid connect(T *object, void (T::*func)(Args...))\n> +\t{\n> +\t\tobject->connect(this);\n> +\t\tslots_.push_back(new SlotMember<T, Args...>(object, true, func));\n> +\t}\n> +\n> +\ttemplate<typename T, typename std::enable_if<!std::is_base_of<Object, T>::value>::type * = nullptr>\n> +#else\n>  \ttemplate<typename T>\n> +#endif\n\nIfdefs, templates and C++ magic what's not to love! I really like what \nthis enables us to do in the library and applications using it.\n\n>  \tvoid connect(T *object, void (T::*func)(Args...))\n>  \t{\n> -\t\tslots_.push_back(new SlotMember<T, Args...>(object, func));\n> +\t\tslots_.push_back(new SlotMember<T, Args...>(object, false, func));\n>  \t}\n>  \n>  \tvoid connect(void (*func)(Args...))\n> @@ -82,7 +133,7 @@ public:\n>  \n>  \tvoid disconnect()\n>  \t{\n> -\t\tfor (SlotBase<Args...> *slot : slots_)\n> +\t\tfor (SlotBase *slot : slots_)\n>  \t\t\tdelete slot;\n>  \t\tslots_.clear();\n>  \t}\n> @@ -90,27 +141,21 @@ public:\n>  \ttemplate<typename T>\n>  \tvoid disconnect(T *object)\n>  \t{\n> -\t\tfor (auto iter = slots_.begin(); iter != slots_.end(); ) {\n> -\t\t\tSlotBase<Args...> *slot = *iter;\n> -\t\t\tif (slot->obj_ == object) {\n> -\t\t\t\titer = slots_.erase(iter);\n> -\t\t\t\tdelete slot;\n> -\t\t\t} else {\n> -\t\t\t\t++iter;\n> -\t\t\t}\n> -\t\t}\n> +\t\tSignalBase::disconnect(object);\n>  \t}\n>  \n>  \ttemplate<typename T>\n>  \tvoid disconnect(T *object, void (T::*func)(Args...))\n>  \t{\n>  \t\tfor (auto iter = slots_.begin(); iter != slots_.end(); ) {\n> -\t\t\tSlotBase<Args...> *slot = *iter;\n> +\t\t\tSlotArgs<Args...> *slot = static_cast<SlotArgs<Args...> *>(*iter);\n>  \t\t\t/*\n> -\t\t\t * If the obj_ pointer matches the object types must\n> -\t\t\t * match, so we can safely cast to SlotMember<T, Args>.\n> +\t\t\t * If the obj() pointer matches the object, the slot is\n> +\t\t\t * guaranteed to be a member slot, so we can safely\n> +\t\t\t * cast it to SlotMember<T, Args...> and access its\n> +\t\t\t * func_ member.\n>  \t\t\t */\n> -\t\t\tif (slot->obj_ == object &&\n> +\t\t\tif (slot->obj() == object &&\n>  \t\t\t    static_cast<SlotMember<T, Args...> *>(slot)->func_ == func) {\n>  \t\t\t\titer = slots_.erase(iter);\n>  \t\t\t\tdelete slot;\n> @@ -123,8 +168,8 @@ public:\n>  \tvoid disconnect(void (*func)(Args...))\n>  \t{\n>  \t\tfor (auto iter = slots_.begin(); iter != slots_.end(); ) {\n> -\t\t\tSlotBase<Args...> *slot = *iter;\n> -\t\t\tif (slot->obj_ == nullptr &&\n> +\t\t\tSlotArgs<Args...> *slot = *iter;\n> +\t\t\tif (slot->obj() == nullptr &&\n>  \t\t\t    static_cast<SlotStatic<Args...> *>(slot)->func_ == func) {\n>  \t\t\t\titer = slots_.erase(iter);\n>  \t\t\t\tdelete slot;\n> @@ -140,13 +185,11 @@ public:\n>  \t\t * Make a copy of the slots list as the slot could call the\n>  \t\t * disconnect operation, invalidating the iterator.\n>  \t\t */\n> -\t\tstd::vector<SlotBase<Args...> *> slots{ slots_.begin(), slots_.end() };\n> -\t\tfor (SlotBase<Args...> *slot : slots)\n> -\t\t\tslot->invoke(args...);\n> +\t\tstd::vector<SlotBase *> slots{ slots_.begin(), slots_.end() };\n> +\t\tfor (SlotBase *slot : slots) {\n> +\t\t\tstatic_cast<SlotArgs<Args...> *>(slot)->invoke(args...);\n> +\t\t}\n>  \t}\n> -\n> -private:\n> -\tstd::list<SlotBase<Args...> *> slots_;\n>  };\n>  \n>  } /* namespace libcamera */\n> diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build\n> index c5354c136563..8384cd0af451 100644\n> --- a/src/libcamera/meson.build\n> +++ b/src/libcamera/meson.build\n> @@ -10,6 +10,7 @@ libcamera_sources = files([\n>      'log.cpp',\n>      'media_device.cpp',\n>      'media_object.cpp',\n> +    'object.cpp',\n>      'pipeline_handler.cpp',\n>      'request.cpp',\n>      'signal.cpp',\n> diff --git a/src/libcamera/object.cpp b/src/libcamera/object.cpp\n> new file mode 100644\n> index 000000000000..826eed6f9b3a\n> --- /dev/null\n> +++ b/src/libcamera/object.cpp\n> @@ -0,0 +1,50 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2019, Google Inc.\n> + *\n> + * object.cpp - Base object\n> + */\n> +\n> +#include <libcamera/object.h>\n> +#include <libcamera/signal.h>\n> +\n> +/**\n> + * \\file object.h\n> + * \\brief Base object to support automatic signal disconnection\n> + */\n> +\n> +namespace libcamera {\n> +\n> +/**\n> + * \\class Object\n> + * \\brief Base object to support automatic signal disconnection\n> + *\n> + * The Object class simplifies signal/slot handling for classes implementing\n> + * slots. By inheriting from Object, an object is automatically disconnected\n> + * from all connected signals when it gets destroyed.\n> + *\n> + * \\sa Signal\n> + */\n> +\n> +Object::~Object()\n> +{\n> +\tfor (SignalBase *signal : signals_)\n> +\t\tsignal->disconnect(this);\n> +}\n> +\n> +void Object::connect(SignalBase *signal)\n> +{\n> +\tsignals_.push_back(signal);\n> +}\n> +\n> +void Object::disconnect(SignalBase *signal)\n> +{\n> +\tfor (auto iter = signals_.begin(); iter != signals_.end(); ) {\n> +\t\tif (*iter == signal)\n> +\t\t\titer = signals_.erase(iter);\n> +\t\telse\n> +\t\t\titer++;\n> +\t}\n> +}\n> +\n> +}; /* namespace libcamera */\n> diff --git a/src/libcamera/signal.cpp b/src/libcamera/signal.cpp\n> index 8d62b5beeeac..cb7daa11dab1 100644\n> --- a/src/libcamera/signal.cpp\n> +++ b/src/libcamera/signal.cpp\n> @@ -47,6 +47,11 @@ namespace libcamera {\n>   * \\brief Connect the signal to a member function slot\n>   * \\param object The slot object pointer\n>   * \\param func The slot member function\n> + *\n> + * If the typename T inherits from Object, the signal will be automatically\n> + * disconnected from the \\a func slot of \\a object when \\a object is destroyed.\n> + * Otherwise the caller shall disconnect signals manually before destroying \\a\n> + * object.\n>   */\n>  \n>  /**\n> diff --git a/test/signal.cpp b/test/signal.cpp\n> index ab69ca1b663d..19a52c603a4a 100644\n> --- a/test/signal.cpp\n> +++ b/test/signal.cpp\n> @@ -8,6 +8,7 @@\n>  #include <iostream>\n>  #include <string.h>\n>  \n> +#include <libcamera/object.h>\n>  #include <libcamera/signal.h>\n>  \n>  #include \"test.h\"\n> @@ -22,6 +23,15 @@ static void slotStatic(int value)\n>  \tvalueStatic_ = value;\n>  }\n>  \n> +class SlotObject : public Object\n> +{\n> +public:\n> +\tvoid slot()\n> +\t{\n> +\t\tvalueStatic_ = 1;\n> +\t}\n> +};\n> +\n>  class SignalTest : public Test\n>  {\n>  protected:\n> @@ -139,6 +149,33 @@ protected:\n>  \t\t\treturn TestFail;\n>  \t\t}\n>  \n> +\t\t/*\n> +\t\t * Test automatic disconnection on object deletion. Connect the\n> +\t\t * slot twice to ensure all instances are disconnected.\n> +\t\t */\n> +\t\tsignalVoid_.disconnect();\n> +\n> +\t\tSlotObject *slotObject = new SlotObject();\n> +\t\tsignalVoid_.connect(slotObject, &SlotObject::slot);\n> +\t\tsignalVoid_.connect(slotObject, &SlotObject::slot);\n> +\t\tdelete slotObject;\n> +\t\tvalueStatic_ = 0;\n> +\t\tsignalVoid_.emit();\n> +\t\tif (valueStatic_ != 0) {\n> +\t\t\tcout << \"Signal disconnection on object deletion test failed\" << endl;\n> +\t\t\treturn TestFail;\n> +\t\t}\n> +\n> +\t\t/*\n> +\t\t * Test that signal deletion disconnects objects. This shall\n> +\t\t * not generate any valgrind warning.\n> +\t\t */\n> +\t\tSignal<> *signal = new Signal<>();\n> +\t\tslotObject = new SlotObject();\n> +\t\tsignal->connect(slotObject, &SlotObject::slot);\n> +\t\tdelete signal;\n> +\t\tdelete slotObject;\n> +\n>  \t\treturn TestPass;\n>  \t}\n>  \n> -- \n> Regards,\n> \n> Laurent Pinchart\n> \n> _______________________________________________\n> libcamera-devel mailing list\n> libcamera-devel@lists.libcamera.org\n> https://lists.libcamera.org/listinfo/libcamera-devel","headers":{"Return-Path":"<niklas.soderlund@ragnatech.se>","Received":["from mail-lj1-x241.google.com (mail-lj1-x241.google.com\n\t[IPv6:2a00:1450:4864:20::241])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 1E7B3610B1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 13 Feb 2019 11:57:32 +0100 (CET)","by mail-lj1-x241.google.com with SMTP id g80so1605560ljg.6\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 13 Feb 2019 02:57:32 -0800 (PST)","from localhost (89-233-230-99.cust.bredband2.com. [89.233.230.99])\n\tby smtp.gmail.com with ESMTPSA id\n\tc30sm2634787lfj.0.2019.02.13.02.57.30\n\t(version=TLS1_2 cipher=ECDHE-RSA-CHACHA20-POLY1305 bits=256/256);\n\tWed, 13 Feb 2019 02:57:30 -0800 (PST)"],"DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=ragnatech-se.20150623.gappssmtp.com; s=20150623;\n\th=date:from:to:cc:subject:message-id:references:mime-version\n\t:content-disposition:content-transfer-encoding:in-reply-to\n\t:user-agent; bh=3EOdhzW4puG3csd0mclyxpdoqujNe2MrQcy3fxChZ1g=;\n\tb=U5Ppyxbo4VL6LCy4U+eY3xpu5gnzbs3gtatqNksO/NcfVAH1vwQ/eJcNn/9TIIB1qV\n\t69VAblpLDjPjUCHK3M1kcLQsyaaS97vvyTOmqzd/2Bg4wrRj81tPxehz0tU5MMJy1ZsY\n\tmzxxJySZrCGKfJ0yQMZhjR/0/ShLouZ8/o1rPgk67BwuM+6j9Y23kCixj0WYtj1QOswV\n\tR1DNTVmnHVGvMl8gY+/X2yYpDBEiI+vMPqghu6uIKm5hFmHMYmcrSfxm0RZSwGVsaTAx\n\tt07ZM9A70Y90f8Rfr4t3TeIjAby+QznI1Un4+O69hV3ITFixONUT9B9EWKPDP9rOaMLP\n\tdZ4Q==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20161025;\n\th=x-gm-message-state:date:from:to:cc:subject:message-id:references\n\t:mime-version:content-disposition:content-transfer-encoding\n\t:in-reply-to:user-agent;\n\tbh=3EOdhzW4puG3csd0mclyxpdoqujNe2MrQcy3fxChZ1g=;\n\tb=VDXF4vRnw+Dq8EXvHih+lUhFh26PoA6r/qwAQyTLBQ6LyhBvXCMoGwoFOhWGNPKTq+\n\t9KqpR93ynlXaN30pxDEh4ItTAiNZUSPbw/JSPJqeWwsjiovV70Xiar9kHw99tPwaM948\n\tTlVJ6o9ctLtqvtIAPKNaYPPhXi42l1SWIoHdseBHoDES+Prp8rDKEuVhP1eEbmsHygoD\n\tPFQ8eUtzthHLMcBXP2HJU22Jj26okLd0MWoj7Yk2BdNYJA1wiDgCofwbLNAMpPd2o7Ec\n\tY/cASlDPKtpa9rmNVVJd0UIUzq+EnMkXSPQ82H2w51HDjVrrE3tRk5Yff66zM3m/5JyO\n\tjFaA==","X-Gm-Message-State":"AHQUAubs746XcV98LFK/nXUVX8+Cektxh1a/KImay5Islz3LH959HJAF\n\tDaR8leLKZy0azm0y9j9ssu07IN9zYMo=","X-Google-Smtp-Source":"AHgI3Ia2do7GWP+bbJk4nl/bbjnviHBc9RI9FeMNtRDmg5a2ls+/qdeWHvM5TNaXobXE37lZ6OcP+A==","X-Received":"by 2002:a2e:7a03:: with SMTP id v3mr910385ljc.22.1550055451249; \n\tWed, 13 Feb 2019 02:57:31 -0800 (PST)","Date":"Wed, 13 Feb 2019 11:57:30 +0100","From":"Niklas =?iso-8859-1?q?S=F6derlund?= <niklas.soderlund@ragnatech.se>","To":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Message-ID":"<20190213105730.GP31044@bigcity.dyn.berto.se>","References":"<20190212223702.9582-1-laurent.pinchart@ideasonboard.com>\n\t<20190212223702.9582-3-laurent.pinchart@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=iso-8859-1","Content-Disposition":"inline","Content-Transfer-Encoding":"8bit","In-Reply-To":"<20190212223702.9582-3-laurent.pinchart@ideasonboard.com>","User-Agent":"Mutt/1.10.1 (2018-07-13)","Subject":"Re: [libcamera-devel] [PATCH 3/3] libcamera: signal: Disconnect\n\tsignal automatically on slot deletion","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.23","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","X-List-Received-Date":"Wed, 13 Feb 2019 10:57:32 -0000"}}]