emqx_bpapi_trans.erl 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2022-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
  3. %%
  4. %% Licensed under the Apache License, Version 2.0 (the "License");
  5. %% you may not use this file except in compliance with the License.
  6. %% You may obtain a copy of the License at
  7. %%
  8. %% http://www.apache.org/licenses/LICENSE-2.0
  9. %%
  10. %% Unless required by applicable law or agreed to in writing, software
  11. %% distributed under the License is distributed on an "AS IS" BASIS,
  12. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. %% See the License for the specific language governing permissions and
  14. %% limitations under the License.
  15. %%--------------------------------------------------------------------
  16. %% @hidden This parse transform generates BPAPI metadata function for
  17. %% a module, and helps dialyzer typechecking RPC calls
  18. -module(emqx_bpapi_trans).
  19. -export([parse_transform/2, format_error/1]).
  20. %%-define(debug, true).
  21. -define(META_FUN, bpapi_meta).
  22. -type semantics() :: call | cast.
  23. -record(s, {
  24. api :: emqx_bpapi:api(),
  25. module :: module(),
  26. version :: emqx_bpapi:api_version() | undefined,
  27. targets = [] :: [{semantics(), emqx_bpapi:call(), emqx_bpapi:call()}],
  28. errors = [] :: list(),
  29. file
  30. }).
  31. format_error(invalid_name) ->
  32. "BPAPI module name should follow <API>_proto_v<number> pattern";
  33. format_error({invalid_fun, Name, Arity}) ->
  34. io_lib:format(
  35. "malformed function ~p/~p. "
  36. "BPAPI functions should have exactly one clause "
  37. "and call (emqx_|e)rpc at the top level",
  38. [Name, Arity]
  39. ).
  40. parse_transform(Forms, _Options) ->
  41. log("Original:~n~p", [Forms]),
  42. State = #s{file = File} = lists:foldl(fun go/2, #s{}, Forms),
  43. log("parse_trans state: ~p", [State]),
  44. case check(State) of
  45. [] ->
  46. finalize(Forms, State);
  47. Errors ->
  48. {error, [{File, [{Line, ?MODULE, Msg} || {Line, Msg} <- Errors]}], []}
  49. end.
  50. %% Scan erl_forms:
  51. go({attribute, _, file, {File, _}}, S) ->
  52. S#s{file = File};
  53. go({attribute, Line, module, Mod}, S) ->
  54. case api_and_version(Mod) of
  55. {ok, API, Vsn} -> S#s{api = API, version = Vsn, module = Mod};
  56. error -> push_err(Line, invalid_name, S)
  57. end;
  58. go({function, _Line, introduced_in, 0, _}, S) ->
  59. S;
  60. go({function, _Line, deprecated_since, 0, _}, S) ->
  61. S;
  62. go({function, Line, Name, Arity, Clauses}, S) ->
  63. analyze_fun(Line, Name, Arity, Clauses, S);
  64. go(_, S) ->
  65. S.
  66. check(#s{errors = Err}) ->
  67. %% Post-processing checks can be placed here
  68. Err.
  69. finalize(Forms, S) ->
  70. {Attrs, Funcs} = lists:splitwith(fun is_attribute/1, Forms),
  71. AST = mk_meta_fun(S),
  72. log("Meta fun:~n~p", [AST]),
  73. Attrs ++ [mk_export()] ++ [AST | Funcs].
  74. mk_meta_fun(#s{api = API, version = Vsn, targets = Targets}) ->
  75. Line = 0,
  76. Calls = [{From, To} || {call, From, To} <- Targets],
  77. Casts = [{From, To} || {cast, From, To} <- Targets],
  78. Ret = typerefl_quote:const(Line, #{
  79. api => API,
  80. version => Vsn,
  81. calls => Calls,
  82. casts => Casts
  83. }),
  84. {function, Line, ?META_FUN, _Arity = 0, [{clause, Line, _Args = [], _Guards = [], [Ret]}]}.
  85. mk_export() ->
  86. {attribute, 0, export, [{?META_FUN, 0}]}.
  87. is_attribute({attribute, _Line, _Attr, _Val}) -> true;
  88. is_attribute(_) -> false.
  89. %% Extract the target function of the RPC call
  90. analyze_fun(Line, Name, Arity, [{clause, Line, Head, _Guards, Exprs}], S) ->
  91. analyze_exprs(Line, Name, Arity, Head, Exprs, S);
  92. analyze_fun(Line, Name, Arity, _Clauses, S) ->
  93. invalid_fun(Line, Name, Arity, S).
  94. analyze_exprs(Line, Name, Arity, Head, Exprs, S) ->
  95. log("~p/~p (~p):~n~p", [Name, Arity, Head, Exprs]),
  96. try
  97. [{call, _, CallToBackend, CallArgs}] = Exprs,
  98. OuterArgs = extract_outer_args(Head),
  99. Key = {S#s.module, Name, OuterArgs},
  100. {Semantics, Target} = extract_target_call(CallToBackend, CallArgs),
  101. push_target({Semantics, Key, Target}, S)
  102. catch
  103. _:Err:Stack ->
  104. log("Failed to process function call:~n~s~nStack: ~p", [Err, Stack]),
  105. invalid_fun(Line, Name, Arity, S)
  106. end.
  107. -spec extract_outer_args([erl_parse:abstract_form()]) -> [atom()].
  108. extract_outer_args(Abs) ->
  109. lists:map(
  110. fun
  111. ({var, _, Var}) ->
  112. Var;
  113. ({match, _, {var, _, Var}, _}) ->
  114. Var;
  115. ({match, _, _, {var, _, Var}}) ->
  116. Var
  117. end,
  118. Abs
  119. ).
  120. -spec extract_target_call(_AST, [_AST]) -> {semantics(), emqx_bpapi:call()}.
  121. extract_target_call(RPCBackend, OuterArgs) ->
  122. {Semantics, {atom, _, M}, {atom, _, F}, A} = extract_mfa(RPCBackend, OuterArgs),
  123. {Semantics, {M, F, list_to_args(A)}}.
  124. -define(BACKEND(MOD, FUN), {remote, _, {atom, _, MOD}, {atom, _, FUN}}).
  125. -define(IS_RPC(MOD), (MOD =:= erpc orelse MOD =:= rpc)).
  126. %% gen_rpc:
  127. extract_mfa(?BACKEND(gen_rpc, _), _) ->
  128. %% gen_rpc has an extremely messy API, thankfully it's fully wrapped
  129. %% by emqx_rpc, so we simply forbid direct calls to it:
  130. error("direct call to gen_rpc");
  131. %% emqx_rpc:
  132. extract_mfa(?BACKEND(emqx_rpc, CallOrCast), [_Node, M, F, A]) ->
  133. {call_or_cast(CallOrCast), M, F, A};
  134. extract_mfa(?BACKEND(emqx_rpc, CallOrCast), [_Tag, _Node, M, F, A]) ->
  135. {call_or_cast(CallOrCast), M, F, A};
  136. extract_mfa(?BACKEND(emqx_rpc, call), [_Tag, _Node, M, F, A, _Timeout]) ->
  137. {call_or_cast(call), M, F, A};
  138. %% (e)rpc:
  139. extract_mfa(?BACKEND(rpc, multicall), [M, F, A]) ->
  140. {call_or_cast(multicall), M, F, A};
  141. extract_mfa(?BACKEND(rpc, multicall), [M, F, A, {integer, _, _Timeout}]) ->
  142. {call_or_cast(multicall), M, F, A};
  143. extract_mfa(?BACKEND(RPC, CallOrCast), [_Node, M, F, A]) when ?IS_RPC(RPC) ->
  144. {call_or_cast(CallOrCast), M, F, A};
  145. extract_mfa(?BACKEND(RPC, CallOrCast), [_Node, M, F, A, _Timeout]) when ?IS_RPC(RPC) ->
  146. {call_or_cast(CallOrCast), M, F, A};
  147. %% emqx_cluster_rpc:
  148. extract_mfa(?BACKEND(emqx_cluster_rpc, multicall), [M, F, A]) ->
  149. {call, M, F, A};
  150. extract_mfa(?BACKEND(emqx_cluster_rpc, multicall), [M, F, A, _RequiredNum, _Timeout]) ->
  151. {call, M, F, A};
  152. extract_mfa(_, _) ->
  153. error("unrecognized RPC call").
  154. call_or_cast(cast) -> cast;
  155. call_or_cast(multicast) -> cast;
  156. call_or_cast(multicall) -> call;
  157. call_or_cast(call) -> call.
  158. list_to_args({cons, _, {var, _, A}, T}) ->
  159. [A | list_to_args(T)];
  160. list_to_args({nil, _}) ->
  161. [].
  162. invalid_fun(Line, Name, Arity, S) ->
  163. push_err(Line, {invalid_fun, Name, Arity}, S).
  164. push_err(Line, Err, S = #s{errors = Errs}) ->
  165. S#s{errors = [{Line, Err} | Errs]}.
  166. push_target(Target, S = #s{targets = Targets}) ->
  167. S#s{targets = [Target | Targets]}.
  168. -spec api_and_version(module()) -> {ok, emqx_bpapi:api(), emqx_bpapi:api_version()} | error.
  169. api_and_version(Module) ->
  170. Opts = [{capture, all_but_first, list}],
  171. case re:run(atom_to_list(Module), "(.*)_proto_v([0-9]+)$", Opts) of
  172. {match, [API, VsnStr]} ->
  173. {ok, list_to_atom(API), list_to_integer(VsnStr)};
  174. nomatch ->
  175. error
  176. end.
  177. -ifdef(debug).
  178. log(Fmt, Args) ->
  179. io:format(user, "!! " ++ Fmt ++ "~n", Args).
  180. -else.
  181. log(_, _) ->
  182. ok.
  183. -endif.