% Copyright (C) 2003-2019 Olivier Boudeville % % This file is part of the Ceylan-WOOPER library. % % This library is free software: you can redistribute it and/or modify % it under the terms of the GNU Lesser General Public License or % the GNU General Public License, as they are published by the Free Software % Foundation, either version 3 of these Licenses, or (at your option) % any later version. % You can also redistribute it and/or modify it under the terms of the % Mozilla Public License, version 1.1 or later. % % This library is distributed in the hope that it will be useful, % but WITHOUT ANY WARRANTY; without even the implied warranty of % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the % GNU Lesser General Public License and the GNU General Public License % for more details. % % You should have received a copy of the GNU Lesser General Public % License, of the GNU General Public License and of the Mozilla Public License % along with this library. % If not, see and % . % % Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com] % Modular WOOPER header gathering all serialisation-related functions. % Note: because of the hooks (which must be class-specific) and of the lack of a % common ancestor to all WOOPER classes (we could have defined methods like % serialise/3 and all in this module), some serialisation services are provided % through an header file (this one), short of being able to be defined in % wooper_serialisation.erl (which would be preferable). % Serialises the specified instance (i.e. the state thereof), using specified % entry transformer and user data. % % Returns a binary corresponding to a { InnerPair, UpdatedUserData } pair made % of: % % 1. an inner pair { Classname, Entries }, converted into a binary and % containing the corresponding class name (as an atom) and the associated % serialisation (as a list of transformed terms) % % 2. the resulting user-data (possibly modified by the successive operations % done by the entry transformer) % % This is a method, but as it is defined unconditionally in the WOOPER header % (in the future, a WOOPER object superclass will be available, defining a % default, overridable serialise/3 method), it cannot be overridden on a % per-class basis. Hence the needs for serialisation hooks. We still need to % specify the classname however, as at deserialisation time this information % will be necessary. % % Notes: % % - we do not take the 'request_sender' field into account for serialisation, % as, by design, there is indeed such a caller (since serialise/3 is a request), % but it is of no interest here % % - as we do not have a typed (class-specific) state (record-like) % data-structures, we have to store not only the values, but also the keys; a % major progress in terms of compactness still lies there % % (const request) % -spec serialise( wooper:state(), wooper_serialisation:entry_transformer(), basic_utils:user_data() ) -> const_request_return( { wooper_serialisation:bin_serialisation(), basic_utils:user_data() } ). serialise( State, _EntryTransformer=undefined, UserData ) -> % Here no entry transformer is to be used, raw serialisation. % Hooks may be defined on a per-class basis: PreState = #state_holder{ attribute_table=AttributeTable, actual_class=Classname } = pre_serialise_hook( State ), io:format( " - serialising, with no transformer, instance ~p of class ~s~n", [ self(), Classname ] ), % There are, for all Erlang processes, some extra information that are % contextual, implicit, like: whether they are linked (and with whom), their % process dictionary, whether they trap exits, etc. % % The WOOPER serialisation mechanisms do not account for them currently (ex: % links may be dictated by the application logic and thus may not have to be % stored), except one: the current random state of the serialised process, % which is transparently managed by WOOPER so that the deserialisation will % lead to restoring the right random state. % So, let's add the WOOPER extra information: CurrentRandomState = random_utils:get_random_state(), RandomAttribute = { wooper_random_state, CurrentRandomState }, % Retrieving all attribute key/value pairs (sorting is probably a bit % cleaner): % Entries = lists:sort( [ RandomAttribute | ?wooper_table_type:enumerate( AttributeTable ) ] ), % By default returns { Classname, Entries }: FullContent = post_serialise_hook( Classname, Entries, PreState ), SerialisedContent = term_to_binary( FullContent, _Opts=[ { compressed, 9 } ] ), Res = { SerialisedContent, UserData }, % Yes, the returned state is 'State', as we do not want to continue with any % state forged for the serialisation (ex: with transformed local processes), % we want to continue as we were! % wooper:const_return_result( Res ); serialise( State, EntryTransformer, UserData ) -> % Here an entry transformer is to be used, for a smarter serialisation (ex: % PID-aware). % Hooks may be defined on a per-class basis: PreState = #state_holder{ attribute_table=AttributeTable, actual_class=Classname } = pre_serialise_hook( State ), io:format( " - serialising, with transformer, instance ~p of class ~s~n", [ self(), Classname ] ), % There are, for all Erlang processes, some extra information that are % contextual, implicit, like: whether they are linked (and with whom), their % process dictionary, whether they trap exits, etc. % % The WOOPER serialisation mechanisms do not account for them currently (ex: % links may be dictated by the application logic and thus may not have to be % stored), except one: the current random state of the serialised process, % which is transparently managed by WOOPER so that the deserialisation will % lead to restoring the right random state. % So, let's add the WOOPER extra information: CurrentRandomState = random_utils:get_random_state(), RandomAttribute = { wooper_random_state, CurrentRandomState }, % Retrieving all attribute key/value pairs (sorting is probably a bit % cleaner): % Entries = lists:sort( [ RandomAttribute | ?wooper_table_type:enumerate( AttributeTable ) ] ), %io:format( "Original entries:~n~p~n", [ Entries ] ), % Applying the entry transformer on each of them: { TransformedEntries, FinalUserData } = lists:foldl( EntryTransformer, _Acc0={ _ResultingEntries=[], UserData }, _List=Entries ), %io:format( "Transformed entries:~n~p~n", [ TransformedEntries ] ), % No need to reverse the transformed list. % By default returns { Classname, TransformedEntries }: FullContent = post_serialise_hook( Classname, TransformedEntries, PreState ), SerialisedContent = term_to_binary( FullContent, _Opts=[ { compressed, 9 } ] ), Res = { SerialisedContent, FinalUserData }, % Yes, returning the initial 'State', as we do not want to continue with any % state forged for the serialisation (ex: with transformed local processes), % we want to continue as we were before the serialisat % wooper:const_return_result( Res ). % Hooks section. % For some classes, the implementor may want to define pre/post hooks for % serialisation/deserialisation. % % In this case he should define 'wooper_serialisation_hooks' and specify the % four corresponding hooks. % -ifndef(wooper_serialisation_hooks). % Default do-nothing hooks: % Triggered just before serialisation. % % We are here to return a state directly suitable for serialisation, for example % with no transient technical identifiers (like PID, open files, etc.) - unless % a later entry transformer is able to manage them. % -spec pre_serialise_hook( wooper:state() ) -> wooper:state(). pre_serialise_hook( State ) -> State. % Triggered just after serialisation, based on the selected entries. % % The value returned by this hook will be converted "as is" into a binary, that % will be written. % % (we do not want to return a state, as we do not want that a state modified by % the serialisation be mistakenly used afterwards) % -spec post_serialise_hook( classname(), wooper_serialisation:term_serialisation(), wooper:state() ) -> term(). post_serialise_hook( Classname, Entries, _State ) -> { Classname, Entries }. % Triggered just before deserialisation. % % Default version corresponding to post_serialise_hook/3. % -spec pre_deserialise_hook( term(), basic_utils:user_data() ) -> wooper_serialisation:term_serialisation(). pre_deserialise_hook( _SerialisationTerm={ _Classname, Entries }, _UserData ) -> Entries. % Triggered just after deserialisation. % -spec post_deserialise_hook( wooper:state() ) -> wooper:state(). post_deserialise_hook( State ) -> State. -endif. % wooper_serialisation_hooks