On Mon, May 4, 2026 at 1:32 PM Josef Melcr <[email protected]> wrote: > > Hi, > this is a v3 of this patch (v2: > https://gcc.gnu.org/pipermail/gcc-patches/2026-May/715612.html) > > Sorry about the formatting issues, formatting is the bane of my > existence for some reason. > > I did not retest the patch, as I only made formatting changes. > > Changes in v3: > - Fixed formatting > > Changes in v2: > - Documentation tweaks suggested by Andrew > > This patch allows the callback attribute to be used outside of GCC under > the name 'gnu::callback_only'. It also fixes some missing bounds checks > in the attribute handler. The attribute is not recognized outside of > the gnu namespace, I did this so that there is no accidental mixup with > the LLVM callback attribute, which has different syntax. I am not great > at user-facing documentation, I hope the docs are clear about the > attribute's usage. Suggestions are of course welcome. > > OK for master?
This is OK for master, but please make sure to re-test. Also please give frontend people the chance to chime in to nack the exposal of this new attribute to users. Thanks, Richard. > Best regards, > Josef > > gcc/ChangeLog: > > * attr-callback.cc (callback_edge_callee_has_attr): Specify gnu > namespace in attribute lookup. > (callback_fetch_attr_by_edge): Likewise. > (handle_callback_attribute): Fix bugs in bounds checks, add gnu > namespace to attribute lookup. > * attr-callback.h (CALLBACK_ATTR_IDENT): Change identifier to > 'callback_only'. > * builtin-attrs.def (ATTR_CALLBACK): Likewise. > * cgraph.cc (cgraph_edge::redirect_call_stmt_to_callee): Specify > gnu namespace in attribute lookup. > (cgraph_node::verify_node): Likewise. > * doc/extend.texi: Add gnu::callback_only attribute > documentation. > * ipa-cp.cc (purge_useless_callback_edges): Specify gnu > namespace in attribute lookup. > * ipa-prop.cc (ipa_compute_jump_functions_for_edge): Likewise. > * tree-core.h: Change ECF_CB_* comment from 'callback' to > 'callback_only'. > > gcc/c-family/ChangeLog: > > * c-attribs.cc: Define the 'callback_only' attribute in gnu > namespace only. > * c-common.h: Add extern decl for the callback attribute table. > > gcc/c/ChangeLog: > > * c-objc-common.h: Add the callback attribute table to allow it > to be registered. > > gcc/cp/ChangeLog: > > * cp-objcp-common.h: Likewise. > > gcc/testsuite/ChangeLog: > > * gcc.dg/attr-callback.c: New test. > * gcc.dg/ipa/ipcp-cb2.c: New test. > > Signed-off-by: Josef Melcr <[email protected]> > --- > gcc/attr-callback.cc | 20 ++--- > gcc/attr-callback.h | 2 +- > gcc/builtin-attrs.def | 2 +- > gcc/c-family/c-attribs.cc | 13 +++- > gcc/c-family/c-common.h | 1 + > gcc/c/c-objc-common.h | 3 +- > gcc/cgraph.cc | 12 +-- > gcc/cp/cp-objcp-common.h | 1 + > gcc/doc/extend.texi | 39 ++++++++++ > gcc/ipa-cp.cc | 2 +- > gcc/ipa-prop.cc | 4 +- > gcc/testsuite/gcc.dg/attr-callback.c | 107 +++++++++++++++++++++++++++ > gcc/testsuite/gcc.dg/ipa/ipcp-cb2.c | 60 +++++++++++++++ > gcc/tree-core.h | 2 +- > 14 files changed, 245 insertions(+), 23 deletions(-) > create mode 100755 gcc/testsuite/gcc.dg/attr-callback.c > create mode 100644 gcc/testsuite/gcc.dg/ipa/ipcp-cb2.c > > diff --git a/gcc/attr-callback.cc b/gcc/attr-callback.cc > index c654d74164d..041e2463592 100644 > --- a/gcc/attr-callback.cc > +++ b/gcc/attr-callback.cc > @@ -87,7 +87,7 @@ callback_special_case_attr (tree decl) > bool > callback_edge_callee_has_attr (cgraph_edge *e) > { > - return lookup_attribute (CALLBACK_ATTR_IDENT, > + return lookup_attribute ("gnu", CALLBACK_ATTR_IDENT, > DECL_ATTRIBUTES (e->callee->decl)) > || callback_is_special_cased (e->callee->decl, e->call_stmt); > } > @@ -113,12 +113,12 @@ callback_fetch_attr_by_edge (cgraph_edge *e, > cgraph_edge *carrying) > if (callback_is_special_cased (carrying->callee->decl, e->call_stmt)) > return callback_special_case_attr (carrying->callee->decl); > > - tree cb_attr = lookup_attribute (CALLBACK_ATTR_IDENT, > + tree cb_attr = lookup_attribute ("gnu", CALLBACK_ATTR_IDENT, > DECL_ATTRIBUTES (carrying->callee->decl)); > gcc_checking_assert (cb_attr); > tree res = NULL_TREE; > - for (; cb_attr; > - cb_attr = lookup_attribute (CALLBACK_ATTR_IDENT, TREE_CHAIN > (cb_attr))) > + for (; cb_attr; cb_attr = lookup_attribute ("gnu", CALLBACK_ATTR_IDENT, > + TREE_CHAIN (cb_attr))) > { > unsigned id = callback_get_fn_index (cb_attr); > if (id == e->callback_id) > @@ -229,10 +229,11 @@ handle_callback_attribute (tree *node, tree name, tree > args, > return NULL_TREE; > } > --callback_fn_idx; > - if (callback_fn_idx >= decl_nargs) > + if (callback_fn_idx < 0 || callback_fn_idx >= decl_nargs) > { > error_at (DECL_SOURCE_LOCATION (decl), > - "callback function position out of range"); > + "callback function index %d is out of range", > + callback_fn_idx + 1); > *no_add_attrs = true; > return NULL_TREE; > } > @@ -305,7 +306,7 @@ handle_callback_attribute (tree *node, tree name, tree > args, > arg_idx -= 1; > /* Report an error if the position is out of bounds, > but we can still check the rest of the arguments. */ > - if (arg_idx >= decl_nargs) > + if (arg_idx < 0 || arg_idx >= decl_nargs) > { > error_at (DECL_SOURCE_LOCATION (decl), > "callback argument index %d is out of range", arg_idx + > 1); > @@ -331,8 +332,9 @@ handle_callback_attribute (tree *node, tree name, tree > args, > > /* Check that the decl does not already have a callback attribute > describing > the same argument. */ > - it = lookup_attribute (CALLBACK_ATTR_IDENT, DECL_ATTRIBUTES (decl)); > - for (; it; it = lookup_attribute (CALLBACK_ATTR_IDENT, TREE_CHAIN (it))) > + it = lookup_attribute ("gnu", CALLBACK_ATTR_IDENT, DECL_ATTRIBUTES (decl)); > + for (; it; > + it = lookup_attribute ("gnu", CALLBACK_ATTR_IDENT, TREE_CHAIN (it))) > if (callback_get_fn_index (it) == callback_fn_idx) > { > error_at (DECL_SOURCE_LOCATION (decl), > diff --git a/gcc/attr-callback.h b/gcc/attr-callback.h > index b2c1c3c09c5..e51c4c6c571 100644 > --- a/gcc/attr-callback.h > +++ b/gcc/attr-callback.h > @@ -28,7 +28,7 @@ enum callback_position > CB_UNKNOWN_POS = 0 > }; > > -#define CALLBACK_ATTR_IDENT " callback" > +#define CALLBACK_ATTR_IDENT "callback_only" > > /* Returns a callback attribute with callback index FN_IDX, and ARG_COUNT > arguments specified by VA_ARGS. */ > diff --git a/gcc/builtin-attrs.def b/gcc/builtin-attrs.def > index be80184b1a1..a395f454be1 100644 > --- a/gcc/builtin-attrs.def > +++ b/gcc/builtin-attrs.def > @@ -130,7 +130,7 @@ DEF_ATTR_IDENT (ATTR_TM_TMPURE, "transaction_pure") > DEF_ATTR_IDENT (ATTR_RETURNS_TWICE, "returns_twice") > DEF_ATTR_IDENT (ATTR_RETURNS_NONNULL, "returns_nonnull") > DEF_ATTR_IDENT (ATTR_WARN_UNUSED_RESULT, "warn_unused_result") > -DEF_ATTR_IDENT (ATTR_CALLBACK, " callback") > +DEF_ATTR_IDENT (ATTR_CALLBACK, "callback_only") > > DEF_ATTR_TREE_LIST (ATTR_NOVOPS_LIST, ATTR_NOVOPS, ATTR_NULL, ATTR_NULL) > > diff --git a/gcc/c-family/c-attribs.cc b/gcc/c-family/c-attribs.cc > index d437c55285e..598acc70d9f 100644 > --- a/gcc/c-family/c-attribs.cc > +++ b/gcc/c-family/c-attribs.cc > @@ -485,8 +485,6 @@ const struct attribute_spec c_common_gnu_attributes[] = > handle_tm_attribute, NULL }, > { "transaction_may_cancel_outer", 0, 0, false, true, false, false, > handle_tm_attribute, NULL }, > - { CALLBACK_ATTR_IDENT, 1, -1, true, false, false, false, > - handle_callback_attribute, NULL }, > /* ??? These two attributes didn't make the transition from the > Intel language document to the multi-vendor language document. */ > { "transaction_pure", 0, 0, false, true, false, false, > @@ -707,6 +705,17 @@ const struct scoped_attribute_specs > c_common_format_attribute_table = > "gnu", { c_common_format_attributes } > }; > > +/* Attribute table for the callback attribute to be used by the C frontends. > + We don't want to expose the attribute outside of the GNU namespace, so it > + has to be separated out. */ > +const struct attribute_spec c_common_callback_attribute[] = { > + { CALLBACK_ATTR_IDENT, 1, -1, true, false, false, false, > + handle_callback_attribute, NULL }, > +}; > + > +const struct scoped_attribute_specs c_common_callback_attribute_table > + = { "gnu", { c_common_callback_attribute } }; > + > /* Returns TRUE iff the attribute indicated by ATTR_ID takes a plain > identifier as an argument, so the front end shouldn't look it up. */ > > diff --git a/gcc/c-family/c-common.h b/gcc/c-family/c-common.h > index a4cd6786109..bcd23f098d0 100644 > --- a/gcc/c-family/c-common.h > +++ b/gcc/c-family/c-common.h > @@ -832,6 +832,7 @@ extern struct visibility_flags visibility_options; > extern const struct scoped_attribute_specs c_common_gnu_attribute_table; > extern const struct scoped_attribute_specs c_common_clang_attribute_table; > extern const struct scoped_attribute_specs c_common_format_attribute_table; > +extern const struct scoped_attribute_specs c_common_callback_attribute_table; > > /* Pointer to function to lazily generate the VAR_DECL for __FUNCTION__ etc. > ID is the identifier to use, NAME is the string. > diff --git a/gcc/c/c-objc-common.h b/gcc/c/c-objc-common.h > index e103646b67e..11f08a2f0ca 100644 > --- a/gcc/c/c-objc-common.h > +++ b/gcc/c/c-objc-common.h > @@ -82,7 +82,8 @@ static const scoped_attribute_specs *const > c_objc_attribute_table[] = > &std_attribute_table, > &c_common_gnu_attribute_table, > &c_common_clang_attribute_table, > - &c_common_format_attribute_table > + &c_common_format_attribute_table, > + &c_common_callback_attribute_table > }; > > #undef LANG_HOOKS_ATTRIBUTE_TABLE > diff --git a/gcc/cgraph.cc b/gcc/cgraph.cc > index 353e8b498aa..85a717bf865 100644 > --- a/gcc/cgraph.cc > +++ b/gcc/cgraph.cc > @@ -1843,7 +1843,7 @@ cgraph_edge::redirect_call_stmt_to_callee (cgraph_edge > *e, > { > cgraph_edge *carrying = e->get_callback_carrying_edge (); > if (!callback_is_special_cased (carrying->callee->decl, e->call_stmt) > - && !lookup_attribute (CALLBACK_ATTR_IDENT, > + && !lookup_attribute ("gnu", CALLBACK_ATTR_IDENT, > DECL_ATTRIBUTES (carrying->callee->decl))) > /* Callback attribute is removed if the dispatching function changes > signature, as the indices wouldn't be correct anymore. These edges > @@ -4419,11 +4419,13 @@ cgraph_node::verify_node (void) > { > int ncallbacks = 0; > int nfound_edges = 0; > - for (tree cb = lookup_attribute (CALLBACK_ATTR_IDENT, > DECL_ATTRIBUTES ( > - > e->callee->decl)); > - cb; cb = lookup_attribute (CALLBACK_ATTR_IDENT, TREE_CHAIN > (cb)), > - ncallbacks++) > + for (tree cb > + = lookup_attribute ("gnu", CALLBACK_ATTR_IDENT, > + DECL_ATTRIBUTES (e->callee->decl)); > + cb; cb = lookup_attribute ("gnu", CALLBACK_ATTR_IDENT, > + TREE_CHAIN (cb)), ncallbacks++) > ; > + > for (cgraph_edge *cbe = callees; cbe; cbe = cbe->next_callee) > { > if (cbe->callback && cbe->call_stmt == e->call_stmt > diff --git a/gcc/cp/cp-objcp-common.h b/gcc/cp/cp-objcp-common.h > index ed29e65e4f3..4a4c6de1aea 100644 > --- a/gcc/cp/cp-objcp-common.h > +++ b/gcc/cp/cp-objcp-common.h > @@ -131,6 +131,7 @@ static const scoped_attribute_specs *const > cp_objcp_attribute_table[] = > &c_common_gnu_attribute_table, > &c_common_clang_attribute_table, > &c_common_format_attribute_table, > + &c_common_callback_attribute_table, > &internal_attribute_table > }; > > diff --git a/gcc/doc/extend.texi b/gcc/doc/extend.texi > index 8dd0b3137bc..b6505e3914a 100644 > --- a/gcc/doc/extend.texi > +++ b/gcc/doc/extend.texi > @@ -2357,6 +2357,45 @@ Note that the BTF format currently only has a > representation for type > tags associated with pointer types. Type tags on non-pointer types > may be silently skipped when generating BTF. > > +@atindex @code{callback_only} > +@cindex functions with callbacks > +@item callback_only > +The @code{callback_only} attribute specifies that the annotated function may > +call the specified callback function. The first parameter identifies the > index > +of the callback function, the rest of the arguments specify the indices of > the > +arguments of the indirect call. All indices start from 1. If the function > takes > +the implicit @code{this} pointer, it is referred to by the index 1, with the > +rest of the arguments starting at index 2. The index 0 marks an argument not > +present in the arguments of the annotated function, an argument which is > +modified before calling the callback function. The annotated function must > pass > +the specified arguments in the specified order to the callback function, > which > +must be callable with the number, order and type of the arguments. The > +specified pointer to the callback may not escape the translation unit of the > +annotated function and it may not be captured. The annotated function is > +required to pass the arguments through, it may not change or dereference > them. > +The arguments also may not escape. The attribute may be used multiple times > per > +function, though only one @code{callback_only} attribute may be used per > +function parameter. > + > +The attribute exposes the potentially hidden callsite in the annotated > +function, enabling interprocedural optimizations which may not be possible > +without the attribute. It is most useful for annotating functions from > +dynamically linked libraries, as their bodies are not available during > +compilation. > + > +This attribute is similar to the clang @code{callback} attribute but it is > not > +compatible with it. The clang implementation allows identifiers as arguments, > +marks an unknown argument with -1 and the @code{this} pointer with the index > 0. > + > +An example usage: > + > +@smallexample > +[[gnu::callback_only(1, 3, 2)]] > +void foo(void (*bar)(int*, double*), double* y, int* x); > +@end smallexample > + > +means that there is a call @code{bar (x, y)} inside @code{foo}. > + > @atindex @code{cleanup} > @cindex cleanup functions > @item cleanup (@var{cleanup_function}) > diff --git a/gcc/ipa-cp.cc b/gcc/ipa-cp.cc > index 337fca71013..763ec9db8ae 100644 > --- a/gcc/ipa-cp.cc > +++ b/gcc/ipa-cp.cc > @@ -6505,7 +6505,7 @@ purge_useless_callback_edges () > if (dump_file) > fprintf (dump_file, "\tExamining callbacks of edge %s -> > %s:\n", > e->caller->dump_name (), e->callee->dump_name ()); > - if (!lookup_attribute (CALLBACK_ATTR_IDENT, > + if (!lookup_attribute ("gnu", CALLBACK_ATTR_IDENT, > DECL_ATTRIBUTES (e->callee->decl)) > && !callback_is_special_cased (e->callee->decl, > e->call_stmt)) > { > diff --git a/gcc/ipa-prop.cc b/gcc/ipa-prop.cc > index cb89934ee38..f34ca56dd4f 100644 > --- a/gcc/ipa-prop.cc > +++ b/gcc/ipa-prop.cc > @@ -2608,11 +2608,11 @@ ipa_compute_jump_functions_for_edge (struct > ipa_func_body_info *fbi, > /* Argument is a pointer to a function. Look for a callback > attribute describing this argument. */ > tree callback_attr > - = lookup_attribute (CALLBACK_ATTR_IDENT, > + = lookup_attribute ("gnu", CALLBACK_ATTR_IDENT, > DECL_ATTRIBUTES (cs->callee->decl)); > for (; callback_attr; > callback_attr > - = lookup_attribute (CALLBACK_ATTR_IDENT, > + = lookup_attribute ("gnu", CALLBACK_ATTR_IDENT, > TREE_CHAIN (callback_attr))) > if (callback_get_fn_index (callback_attr) == n) > break; > diff --git a/gcc/testsuite/gcc.dg/attr-callback.c > b/gcc/testsuite/gcc.dg/attr-callback.c > new file mode 100755 > index 00000000000..a45ad8464cd > --- /dev/null > +++ b/gcc/testsuite/gcc.dg/attr-callback.c > @@ -0,0 +1,107 @@ > +/* Test callback attribute error checking. */ > + > +/* { dg-do compile } */ > +/* { dg-options "-std=gnu17 -Wattributes" } */ > + > +[[gnu::callback_only(1, 2)]] > +void > +correct_1(void (*)(int*), int*); > + > +[[gnu::callback_only(1, 2, 3)]] > +void > +correct_2(void (*)(int*, double*), int*, double*); > + > +[[gnu::callback_only(1, 2, 3), gnu::callback_only(4, 5)]] > +void > +correct_3(void (*)(int*, double*), int*, double*, int (*)(void*), void*); > + > +[[gnu::callback_only(1, 0)]] > +void > +unknown_1(void (*)(int*)); > + > +[[gnu::callback_only(1, 2, 0)]] > +void > +unknown_2(void (*)(int*, double*), int*, double*, char*); > + > +[[gnu::callback_only(1, 0, 3, 3)]] > +void > +too_many(void (*)(int*, double*), int*, double*); /* { dg-error "argument > number mismatch, 2 expected, got 3" }*/ > + > +[[gnu::callback_only(1, 2)]] > +void > +too_few_1(void (*)(int*, double*), int*, double*); /* { dg-error "argument > number mismatch, 2 expected, got 1" }*/ > + > +[[gnu::callback_only(1)]] > +void > +too_few_2(void (*)(int*, double*), int*, double*); /* { dg-error "argument > number mismatch, 2 expected, got 0" }*/ > + > +[[gnu::callback_only(3, 1)]] > +void > +promotion(char*, float, int (*)(int*)); > + > +[[gnu::callback_only(2, 3)]] > +void > +downcast(char*, void* (*)(float*), double*); > + > +[[gnu::callback_only(1, 2, 5)]] > +void > +out_of_range_1(char (*)(float*, double*), float*, double*, int*); /* { > dg-error "callback argument index 5 is out of range" } */ > + > +[[gnu::callback_only(1, -2, 3)]] > +void > +out_of_range_2(char (*)(float*, double*), float*, double*, int*); /* { > dg-error "callback argument index -2 is out of range" } */ > + > +[[gnu::callback_only(-1, 2, 3)]] > +void > +out_of_range_3(char (*)(float*, double*), float*, double*, int*); /* { > dg-error "callback function index -1 is out of range" } */ > + > +[[gnu::callback_only(67, 2, 3)]] > +void > +out_of_range_4(char (*)(float*, double*), float*, double*, int*); /* { > dg-error "callback function index 67 is out of range" } */ > + > +[[gnu::callback_only(0, 2, 3)]] > +void > +unknown_fn(char (*)(float*, double*), float*, double*, int*); /* { dg-error > "callback function position cannot be marked as unknown" } */ > + > +[[gnu::callback_only(1, 2)]] > +void > +not_a_fn(int, int); /* { dg-error "argument no. 1 is not an address of a > function" } */ > + > +struct S > +{ > + int x; > +}; > + > +[[gnu::callback_only(1, 2)]] > +void > +incompatible_types_1(void (*)(struct S*), struct S); /* { dg-error "argument > type at index 2 is not compatible with callback argument type at index 1" } */ > + > +[[gnu::callback_only(1, 3, 2)]] > +void > +incompatible_types_2(void (*)(struct S*, int*), int*, double); /* { dg-error > "argument type at index 3 is not compatible with callback argument type at > index 1" } */ > + > +[[gnu::callback_only(1, "2")]] > +void > +wrong_arg_type_1(void (*)(void*), void*); /* { dg-error "argument no. 1 is > not an integer constant" } */ > + > +[[gnu::callback_only("not a number", 2, 2)]] > +void > +wrong_arg_type_2(void (*)(void*, void*), void*); /* { dg-error "argument > specifying callback function position is not an integer constant" } */ > + > +[[gnu::callback_only(1, 2), gnu::callback_only(1, 3)]] > +void > +multiple_single_fn(void (*)(int*), int*, int*); /* { dg-error "function > declaration has multiple callback attributes describing argument no. 1" } */ > + > +/* Check that the attribute won't resolve outside of our namespace. */ > + > +[[callback_only(1, 2)]] /* { dg-warning "ignored" } */ > +void > +ignore_1(void (*)(int*), int*); > + > +[[callback(1, 2)]] /* { dg-warning "ignored" } */ > +void > +ignore_2(void (*)(int*), int*); > + > +[[gnu::callback(1, 2)]] > +void > +ignore_3(void (*)(int*), int*); /* { dg-warning "ignored" } */ > diff --git a/gcc/testsuite/gcc.dg/ipa/ipcp-cb2.c > b/gcc/testsuite/gcc.dg/ipa/ipcp-cb2.c > new file mode 100644 > index 00000000000..201d41d48f3 > --- /dev/null > +++ b/gcc/testsuite/gcc.dg/ipa/ipcp-cb2.c > @@ -0,0 +1,60 @@ > +/* Test that we can handle multiple callback attributes and use them to > + propagate into callbacks. 'cb1' body borrowed from a ipa-cp test to get > the > + pass to work. */ > + > +/* { dg-do compile } */ > +/* { dg-options "-O3 -fdump-ipa-cp" } */ > + > +struct S > +{ > + int a, b, c; > +}; > + > +extern void *blah (int, void *); > + > +[[gnu::callback_only (1, 2), gnu::callback_only (3, 4, 5)]] extern void > +call (void (*fn1) (struct S *), struct S *a, > + void (*fn2) (struct S *, struct S *), struct S *b, struct S *c); > + > +void > +cb1 (struct S *p) > +{ > + int i, c = p->c; > + int b = p->b; > + void *v = (void *) p; > + > + for (i = 0; i < c; i++) > + v = blah (b + i, v); > +} > + > +void > +cb2 (struct S *a, struct S *b) > +{ > + cb1 (a); > + cb1 (b); > +} > + > +void > +test (int a, int b, int c) > +{ > + struct S s; > + s.a = a; > + s.b = b; > + s.c = c; > + struct S ss; > + ss.a = s.c; > + ss.b = s.b; > + ss.c = s.a; > + call (cb1, &s, cb2, &s, &ss); > +} > + > +int > +main () > +{ > + test (1, 64, 32); > + return 0; > +} > + > +/* { dg-final { scan-ipa-dump "Creating a specialized node of cb1" "cp" } } > */ > +/* { dg-final { scan-ipa-dump "Creating a specialized node of cb2" "cp" } } > */ > +/* { dg-final { scan-ipa-dump-times "Aggregate replacements: " 2 "cp" } } */ > diff --git a/gcc/tree-core.h b/gcc/tree-core.h > index 07e9318f5e8..0169c0d4c67 100644 > --- a/gcc/tree-core.h > +++ b/gcc/tree-core.h > @@ -102,7 +102,7 @@ struct die_struct; > meant to be used for the construction of builtin functions. They were > only > added because Fortran uses them for attributes of builtins. */ > > -/* callback(1, 2) */ > +/* callback_only(1, 2) */ > #define ECF_CB_1_2 (1 << 17) > > /* Call argument flags. */ > -- > 2.54.0 >
