blob: 418f0f284d1e51bb106e12ff67b35014dd060d1f [file] [log] [blame]
%%% 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}}).