|  | %%% Copyright (c) 2007- Facebook | 
|  | %%% Distributed under the Thrift Software License | 
|  | %%% | 
|  | %%% See accompanying file LICENSE or visit the Thrift site at: | 
|  | %%% http://developers.facebook.com/thrift/ | 
|  |  | 
|  | %%% | 
|  | %%% dox: | 
|  | %%% | 
|  | %%% C++                   <-> Erlang | 
|  | %%% classes                   modules | 
|  | %%%   class b : public a        a:super() -> b. | 
|  | %%% | 
|  |  | 
|  | -module(oop). | 
|  |  | 
|  | -export([start_new/2, get/2, set/3, call/2, call/3, inspect/1, class/1, is_object/1, is_a/2]). | 
|  | -export([call1/3]). %% only for thrift_oop_server ... don't use it | 
|  | -export([behaviour_info/1]). | 
|  |  | 
|  | -include("thrift.hrl"). | 
|  | -include("oop.hrl"). | 
|  |  | 
|  | %% state for the call loop | 
|  | -record(cstate, { | 
|  | obj,      %% the current object (on which we want to invoke MFA) | 
|  | module,   %% the current module we're considering | 
|  | func,     %% the method name (i.e. the function we're trying to invoke in Module) | 
|  | args,     %% the arguments, the first of which is Obj | 
|  | tried,    %% a (backwards) list of modules we've tried | 
|  | first_obj %% the original object | 
|  | }). | 
|  |  | 
|  | %%% | 
|  | %%% behavior definition | 
|  | %%% | 
|  |  | 
|  | behaviour_info(callbacks) -> | 
|  | [ {attr, 4}, | 
|  | {super, 0} | 
|  | ]; | 
|  | behaviour_info(_) -> | 
|  | undefined. | 
|  |  | 
|  | %%% | 
|  | %%% public interface | 
|  | %%% | 
|  |  | 
|  | %% TODO: voids take only ok as return? | 
|  | start_new(none=Resv, _) -> | 
|  | ?ERROR("can't instantiate ~p: class name is a reserved word", [Resv]), | 
|  | error; | 
|  | start_new(Class, Args) -> | 
|  | {ok, Pid} = gen_server:start_link(thrift_oop_server, {Class, Args}, []), | 
|  | Pid. | 
|  |  | 
|  | %% get(Obj, Field) -> term() | 
|  | %% looks up Field in Obj or its ancestor objects | 
|  | get(Obj, Field) -> | 
|  | call(Obj, attr, [get, Field, get]). | 
|  |  | 
|  | set(Obj, Field, Value) -> %% TODO: could be tail-recursive | 
|  | Module = ?CLASS(Obj), | 
|  | case apply_if_defined(Module, attr, [Obj, set, Field, Value]) of | 
|  | {ok, V} -> V; | 
|  | undef   -> | 
|  | case get_superobject(Obj) of | 
|  | {ok, Superobj} -> | 
|  | Superobj1 = set(Superobj, Field, Value), | 
|  | Module:attr(Obj, set, super, Superobj1); | 
|  | undef -> | 
|  | error(missing_attr_set, Field, Obj) | 
|  | end | 
|  | end. | 
|  |  | 
|  | %% | 
|  | %% ** dynamic method dispatch ** | 
|  | %% | 
|  | %% calls Module:Func(*Args) if it exists | 
|  | %% if not, Module <- Module:super() and try again recursively | 
|  | %% | 
|  | %% Module:attr(*Args) is handled specially: | 
|  | %% Obj needs to be replaced with Obj's "superobject" | 
|  | %% | 
|  | call(Obj, Func) -> | 
|  | call(Obj, Func, []). | 
|  |  | 
|  | call(Obj, Func, ArgsProper) -> | 
|  | %% this is WAY too expensive | 
|  | %% ?INFO("oop:call called: Obj=~p Func=~p ArgsProper=~p", [inspect(Obj), Func, ArgsProper]), | 
|  | case call1(Obj, Func, ArgsProper) of | 
|  | {ok, Value}       -> Value; | 
|  | {error, Kind, S1} -> error(Kind, S1) | 
|  | end. | 
|  |  | 
|  | call1(Obj, Func, ArgsProper) -> | 
|  | S = #cstate{ | 
|  | obj       = Obj, | 
|  | module    = ?CLASS(Obj), | 
|  | func      = Func, | 
|  | args      = [Obj|ArgsProper], %% prepend This to args | 
|  | tried     = [], | 
|  | first_obj = Obj | 
|  | }, | 
|  | call1(S). | 
|  |  | 
|  | call1(S = #cstate{obj=Obj, module=Module, func=Func, args=Args}) -> | 
|  | %% ?INFO("call1~n obj=~p~n MFA=~p, ~p, ~p", [inspect(Obj), Module, Func, Args]), | 
|  | %% io:format("call ~p~n", [Module]), | 
|  | case apply_if_defined(Module, Func, Args) of | 
|  | {ok, Value} -> {ok, Value}; | 
|  | undef       -> call1_try_super(S) | 
|  | end. | 
|  |  | 
|  | call1_try_super(S = #cstate{func=attr, module=Module, tried=Tried}) -> | 
|  | case Module:super() of | 
|  | none       -> {error, missing_attr, S}; | 
|  | Superclass -> call1_try_super_attr(Superclass, S) | 
|  | end; | 
|  | call1_try_super(S = #cstate{func=Func, module=Module, tried=Tried}) -> | 
|  | case Module:super() of | 
|  | none       -> {error, missing_method, S}; | 
|  | Superclass -> | 
|  | S1 = S#cstate{ | 
|  | module = Superclass, | 
|  | tried  = [Module|Tried] | 
|  | }, | 
|  | call1(S1) | 
|  | end. | 
|  |  | 
|  | call1_try_super_attr(Superclass, S = #cstate{obj=Obj, module=Module, args=Args, tried=Tried}) -> | 
|  | %% look for attrs in the "super object" | 
|  | case get_superobject(Obj) of | 
|  | undef -> {error, missing_superobj, S}; | 
|  | {ok, Superobj} when Module == ?CLASS(Obj) -> | 
|  | %% replace This with Superobj | 
|  | S1 = S#cstate{ | 
|  | obj    = Superobj, | 
|  | args   = [Superobj|tl(Args)], | 
|  | module = Superclass, | 
|  | tried  = [Module|Tried] | 
|  | }, | 
|  | call1(S1) | 
|  | end. | 
|  |  | 
|  | %% careful: not robust against records beginning with a class name | 
|  | %% (note: we can't just guard with is_record(?CLASS(Obj), Obj) since we | 
|  | %% can't/really really shouldn't require all record definitions in this file | 
|  | inspect(Obj) -> | 
|  | try | 
|  | case is_object(Obj) of | 
|  | true -> | 
|  | DeepList = inspect1(Obj, "#<"), | 
|  | lists:flatten(DeepList); | 
|  | false -> | 
|  | thrift_utils:sformat("~p", [Obj]) | 
|  | end | 
|  | catch | 
|  | _:E -> | 
|  | thrift_utils:sformat("INSPECT_ERROR(~p) ~p", [E, Obj]) | 
|  |  | 
|  | %% TODO(cpiro): bring this back once we're done testing: | 
|  | %% _:E -> thrift_utils:sformat("~p", [Obj]) | 
|  | end. | 
|  |  | 
|  | inspect1(Obj, Str) -> | 
|  | Class   = ?CLASS(Obj), | 
|  | Inspect = Class:inspect(Obj), | 
|  | Current = atom_to_list(Class) ++ ": " ++ Inspect, | 
|  |  | 
|  | case get_superobject(Obj) of | 
|  | {ok, Superobj} -> | 
|  | inspect1(Superobj, Str ++ Current ++ " | "); | 
|  | undef -> | 
|  | Str ++ Current ++ ">" | 
|  | end. | 
|  |  | 
|  | %% class(Obj) -> atom() = Class | 
|  | %%             | none | 
|  | class(Obj) when is_tuple(Obj) -> | 
|  | %% if it's an object its first element will be a class name, and it'll have super/0 | 
|  | case apply_if_defined(?CLASS(Obj), super, []) of | 
|  | {ok, _} -> ?CLASS(Obj); | 
|  | undef   -> none | 
|  | end; | 
|  | class(_)    -> none. | 
|  |  | 
|  | %% is_a relationship | 
|  | is_a(Obj, Class) -> | 
|  | %% ?INFO("is_a ~p ~p", [Obj, Class]), | 
|  | case is_object(Obj) of | 
|  | true -> | 
|  | is_a1(Obj, Class); | 
|  | false -> | 
|  | false | 
|  | end. | 
|  | is_a1(Obj, Class) when Class == ?CLASS(Obj) -> | 
|  | true; | 
|  | is_a1(Obj, Class) -> | 
|  | case get_superobject(Obj) of | 
|  | undef -> | 
|  | false; | 
|  | {ok, SuperObj} -> | 
|  | is_a1(SuperObj, Class) | 
|  | end. | 
|  |  | 
|  | %% is the tuple/record an object? | 
|  | %% is_object(Obj) = bool() | 
|  | is_object(Obj) when is_tuple(Obj) -> | 
|  | case class(Obj) of | 
|  | none -> false; | 
|  | _    -> true | 
|  | end; | 
|  | is_object(_) -> false. | 
|  |  | 
|  | %%% | 
|  | %%% private helpers | 
|  | %%% | 
|  |  | 
|  | %% apply_if_defined(MFA) -> {ok, apply(MFA)} | 
|  | %%                        | undef | 
|  | %% this could be worth some money | 
|  | apply_if_defined(M, F, A) -> | 
|  | apply_if_defined({M,F,A}). | 
|  |  | 
|  | apply_if_defined({M,F,A} = MFA) -> | 
|  | try | 
|  | %% io:format("apply ~p ~p ~p~n", [M,F,A]), | 
|  | {ok, apply(M, F, A)} | 
|  | catch | 
|  | _:Kind when Kind == undef; Kind == function_clause -> | 
|  | case erlang:get_stacktrace() of | 
|  | %% the first stack call should match MFA when `apply' fails because the function is undefined | 
|  | %% they won't match if the function is currently running and an error happens in the middle | 
|  | [MFA|_] -> undef;           % trapped successfully | 
|  | ST -> | 
|  | io:format("DONIT THE EXIT THING ~p~n", [Kind]), | 
|  | exit({Kind, ST}) % some unrelated error, re-exit | 
|  | end | 
|  | end. | 
|  |  | 
|  | get_superobject(Obj) -> | 
|  | apply_if_defined(?CLASS(Obj), attr, [Obj, get, super, get]). | 
|  |  | 
|  | %%% | 
|  | %%% errors | 
|  | %%% | 
|  |  | 
|  | tried(S = #cstate{module=Module, tried=Tried}) -> | 
|  | lists:reverse([Module|Tried]). | 
|  |  | 
|  | error(missing_superobj, S = #cstate{obj=Obj}) -> | 
|  | exit({missing_superobj, {inspect(Obj), tried(S)}}); | 
|  | error(missing_method, S = #cstate{obj=Obj, func=Func, args=Args}) -> | 
|  | exit({missing_method, {Func, inspect(Obj), tl(Args), tried(S)}}); | 
|  | error(missing_attr, S = #cstate{args=Args, first_obj=FirstObj}) -> | 
|  | exit({missing_attr, {hd(tl(Args)), inspect(FirstObj), tried(S)}}). | 
|  |  | 
|  | error(missing_attr_set, Field, Obj) -> | 
|  | BT = "..", %% TODO: give a backtrace | 
|  | exit({missing_attr, {Field, inspect(Obj), BT}}). |