[Spice-devel] [PATCH spice-streaming-agent 1/2] Implement version checking for plugins without violating ODR
Frediano Ziglio
fziglio at redhat.com
Wed Nov 22 16:11:08 UTC 2017
>
> From: Christophe de Dinechin <dinechin at redhat.com>
>
> The C++ One Definition Rule (ODR) states that all translation units
> must see the same definitions. In the current code, when we call
> Agent::PluginVersionIsCompatible from the plugin, it is an ODR
> violation as soon as we have made any change in the Agent class
> compared to what the plugin was compiled against.
>
This currently is not true in many ABI unless you add a vfunc before
Agent::PluginVersionIsCompatible which you are not supposed to do.
> GCC respects the Standard C++ ABI on most platforms, so I believe this
> could be made to work despite the violation as long as we never
> changed the order of declarations, etc. But the point of the ABI check
> is that we should be able to change the agent interface in any way we
> want and be safe. And technically, the safe cases would still be an
> ODR violation that relies on knowledge of implementation details.
>
Any ABI came with implementation details, if the compiler decide
to change the registers used there's no way to maintain an ABI.
> Another issue is that we don't want to have to rely on the plugins to
> do the version checks. It is more robust to do it in the agent, where
> it ensures that it is done in a consistent way and with consistent
> error messages. As a matter of fact, the new check is robust against
> older plugins and will not load them.
>
No, to read that variable you need to load the plugin, what is changing
is that you don't need to call a function. Personally having an ABI
relying in exporting variables is not that safe and portable.
> The mechanism implemented here is similar to what libtool uses.
> It lets any interface update be marked as either compatible with
> earlier plugins or incompatible.
>
How are you going to implement that a plugin support from version X
to version Y?
> Signed-off-by: Christophe de Dinechin <dinechin at redhat.com>
> ---
> include/spice-streaming-agent/plugin.hpp | 48
> +++++++++++++++++---------------
> src/concrete-agent.cpp | 35 ++++++++++++-----------
> src/concrete-agent.hpp | 4 ---
> src/mjpeg-fallback.cpp | 3 --
> 4 files changed, 43 insertions(+), 47 deletions(-)
>
> diff --git a/include/spice-streaming-agent/plugin.hpp
> b/include/spice-streaming-agent/plugin.hpp
> index 727cb3b..1a58856 100644
> --- a/include/spice-streaming-agent/plugin.hpp
> +++ b/include/spice-streaming-agent/plugin.hpp
> @@ -22,11 +22,20 @@ namespace SpiceStreamingAgent {
> class FrameCapture;
>
> /*!
> - * Plugin version, only using few bits, schema is 0xMMmm
> - * where MM is major and mm is the minor, can be easily expanded
> - * using more bits in the future
> + * Plugins use a versioning system similar to that implemented by libtool
> + *
> http://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html
> + * Update the version information as follows:
> + * [ANY CHANGE] If any interfaces have been added, removed, or changed
> + * since the last update, increment PluginInterfaceVersion.
> + * [COMPATIBLE CHANGE] If interfaces have only been added since the last
> + * public release, then increment PluginInterfaceAge.
> + * [INCOMPATIBLE CHANGE] If any interfaces have been removed or changed
> + * since the last public release, then set PluginInterfaceAge to 0.
> */
> -enum Constants : unsigned { PluginVersion = 0x100u };
> +enum Constants : unsigned {
> + PluginInterfaceVersion = 0,
> + PluginInterfaceAge = 0
> +};
>
> enum Ranks : unsigned {
> /// this plugin should not be used
> @@ -102,20 +111,6 @@ class Agent
> {
> public:
> /*!
> - * Get agent version.
> - * Plugin should check the version for compatibility before doing
> - * everything.
> - * \return version specified like PluginVersion
> - */
> - virtual unsigned Version() const=0;
> -
> - /*!
> - * Check if a given plugin version is compatible with this agent
> - * \return true is compatible
> - */
> - virtual bool PluginVersionIsCompatible(unsigned pluginVersion) const=0;
> -
> - /*!
> * Register a plugin in the system.
> */
> virtual void Register(Plugin& plugin)=0;
> @@ -135,18 +130,25 @@ typedef bool PluginInitFunc(SpiceStreamingAgent::Agent*
> agent);
>
> #ifndef SPICE_STREAMING_AGENT_PROGRAM
> /*!
> + * Plugin interface version
> + * Each plugin should define this variable and set it to
> PluginInterfaceVersion
> + * That version will be checked by the agent before executing any plugin
> code
> + */
> +extern "C" unsigned spice_streaming_agent_plugin_interface_version;
> +
Is not a good idea to export variables. In some environment variable
are copied into the executable. But I can do some test about it, I
think using dynamic linking can be safe.
Also you point to libtool versioning but how to get the age with
this interface?
Or the API version itself define the range?
> +/*!
> * Plugin main entry point.
> - * Plugins should check if the version of the agent is compatible.
> - * If is compatible should register itself to the agent and return
> - * true.
> - * If is not compatible can decide to stay in memory or not returning
> - * true (do not unload) or false (safe to unload). This is necessary
> + * This entry point is only called if the version check passed.
> + * It should return true if it loaded and initialized successfully.
> + * If the plugin does not initialize and does not want to be unloaded,
> + * it may still return true on failure. This is necessary
> * if the plugin uses some library which are not safe to be unloaded.
> * This public interface is also designed to avoid exporting data from
> * the plugin which could be a problem in some systems.
> * \return true if plugin should stay loaded, false otherwise
> */
> extern "C" SpiceStreamingAgent::PluginInitFunc
> spice_streaming_agent_plugin_init;
> +
> #endif
>
> #endif // SPICE_STREAMING_AGENT_PLUGIN_HPP
> diff --git a/src/concrete-agent.cpp b/src/concrete-agent.cpp
> index 192054a..be0ec66 100644
> --- a/src/concrete-agent.cpp
> +++ b/src/concrete-agent.cpp
> @@ -16,28 +16,11 @@
> using namespace std;
> using namespace SpiceStreamingAgent;
>
> -static inline unsigned MajorVersion(unsigned version)
> -{
> - return version >> 8;
> -}
> -
> -static inline unsigned MinorVersion(unsigned version)
> -{
> - return version & 0xffu;
> -}
> -
> ConcreteAgent::ConcreteAgent()
> {
> options.push_back(ConcreteConfigureOption(nullptr, nullptr));
> }
>
> -bool ConcreteAgent::PluginVersionIsCompatible(unsigned pluginVersion) const
> -{
> - unsigned version = Version();
> - return MajorVersion(version) == MajorVersion(pluginVersion) &&
> - MinorVersion(version) >= MinorVersion(pluginVersion);
> -}
> -
> void ConcreteAgent::Register(Plugin& plugin)
> {
> plugins.push_back(shared_ptr<Plugin>(&plugin));
> @@ -85,6 +68,24 @@ void ConcreteAgent::LoadPlugin(const char
> *plugin_filename)
> return;
> }
>
> + unsigned *version =
> + (unsigned *) dlsym(dl,
> "spice_streaming_agent_plugin_interface_version");
> + if (!version) {
> + syslog(LOG_ERR, "error loading plugin %s: no version information",
> + plugin_filename);
> + return;
> + }
> + if (*version < PluginInterfaceVersion - PluginInterfaceAge ||
> + *version > PluginInterfaceVersion) {
> + syslog(LOG_ERR,
> + "error loading plugin %s: plugin interface version %u, "
> + "agent accepts version %u...%u",
> + plugin_filename, *version,
> + PluginInterfaceVersion - PluginInterfaceAge,
> + PluginInterfaceVersion);
> + return;
> + }
> +
> try {
> PluginInitFunc* init_func =
> (PluginInitFunc *) dlsym(dl,
> "spice_streaming_agent_plugin_init");
> diff --git a/src/concrete-agent.hpp b/src/concrete-agent.hpp
> index 828368b..bcf0a76 100644
> --- a/src/concrete-agent.hpp
> +++ b/src/concrete-agent.hpp
> @@ -25,16 +25,12 @@ class ConcreteAgent final : public Agent
> {
> public:
> ConcreteAgent();
> - unsigned Version() const override {
> - return PluginVersion;
> - }
> void Register(Plugin& plugin) override;
> const ConfigureOption* Options() const override;
> void LoadPlugins(const char *directory);
> // pointer must remain valid
> void AddOption(const char *name, const char *value);
> FrameCapture *GetBestFrameCapture();
> - bool PluginVersionIsCompatible(unsigned pluginVersion) const override;
> private:
> void LoadPlugin(const char *plugin_filename);
> std::vector<std::shared_ptr<Plugin>> plugins;
> diff --git a/src/mjpeg-fallback.cpp b/src/mjpeg-fallback.cpp
> index f41e68a..27505e6 100644
> --- a/src/mjpeg-fallback.cpp
> +++ b/src/mjpeg-fallback.cpp
> @@ -208,9 +208,6 @@ void MjpegPlugin::ParseOptions(const ConfigureOption
> *options)
> static bool
> mjpeg_plugin_init(Agent* agent)
> {
> - if (agent->Version() != PluginVersion)
> - return false;
> -
> std::unique_ptr<MjpegPlugin> plugin(new MjpegPlugin());
>
> plugin->ParseOptions(agent->Options());
Was wondering about compatible updates.
Supposing we start at version 1.
1) we add API to Plugin. Version will be 2, agent knows the version
of plugin is 2 and that can safely call the new API. If we load
an old plugin this has version 1 and we don't call the new API.
This works;
2) we add API to Agent. Version will be 2. Plugin versions 2 knows
this new API and will use. Plugin versions 1 don't know and
won't use. Works too.
Internally we discussed and we agreed that current version is
still development, not having a public release.
So no objections about this patch, beside exporting a variable.
I think is safe at the end, I'll have a quick check just to make sure.
Frediano
More information about the Spice-devel
mailing list