@@ -429,7 +429,10 @@ macro_rules! or {
429429/// The following options are available for the [`extension`] macro:
430430///
431431/// * deps: a comma-separated list of module dependencies, eg: `deps = [ my_other_extension ]`
432- /// * parameters: a comma-separated list of parameters and base traits, eg: `parameters = [ P: MyTrait ]`
432+ /// * parameters: a comma-separated list of parameters and base traits, eg: `parameters = [ P: MyTrait ]`.
433+ /// A generic extension's op table is a `const` in static memory just like a non-generic one's, but
434+ /// there is one such table per monomorphization, so instantiating the same extension with many
435+ /// different parameters multiplies the table in the binary.
433436/// * bounds: a comma-separated list of additional type bounds, eg: `bounds = [ P::MyAssociatedType: MyTrait ]`
434437/// * ops: a comma-separated list of [`OpDecl`]s to provide, eg: `ops = [ op_foo, op_bar ]`
435438/// * esm: a comma-separated list of ESM module filenames (see [`include_js_files`]), eg: `esm = [ dir "dir", "my_file.js" ]`
@@ -530,10 +533,19 @@ macro_rules! extension {
530533 const V : :: std:: option:: Option <& ' static :: std:: primitive:: str > = $crate:: or!( $( :: std:: option:: Option :: Some ( $esm_entry_point) ) ?, :: std:: option:: Option :: None ) ;
531534 V
532535 } ,
533- ops: :: std:: borrow:: Cow :: Owned ( vec![ $( $( {
534- $( #[ $m ] ) *
535- $( $op ) ::+ $( :: < $( $op_param) ,* > ) ? ( )
536- } ) ,+) ?] ) ,
536+ // The op declaration constructors are `const fn`, so the whole table
537+ // is a compile-time constant living in static memory (see the
538+ // `! __ops_table__` arms below). A generic extension gets one such
539+ // table per monomorphization, via an associated const on a generic
540+ // helper type.
541+ ops: $crate:: extension!(
542+ ! __ops_table__
543+ [ $( $( $param : $type ) + ) ? ]
544+ [ $( $( {
545+ $( #[ $m ] ) *
546+ $( $op ) ::+ $( :: < $( $op_param) ,* > ) ? ( )
547+ } ) ,+) ? ]
548+ ) ,
537549 objects: :: std:: borrow:: Cow :: Borrowed ( & [ $( $( {
538550 $( $object ) ::+:: DECL
539551 } ) ,+) ?] ) ,
@@ -661,6 +673,40 @@ macro_rules! extension {
661673 $( $args. op_state_fn = :: std:: option:: Option :: Some ( :: std:: boxed:: Box :: new( $state_fn) ) ; ) ?
662674 } ;
663675
676+ // No type parameters: the op declarations are constant expressions, so the
677+ // table is a `const` item in static memory and `Extension::ops` borrows it.
678+ // `init_ops` then hands it to the realm as an `OpDeclStorage::Static` run,
679+ // i.e. the realm keeps no copy of the decls at all.
680+ ( ! __ops_table__ [ ] [ $( $decl: expr ) ,* $( , ) ? ] ) => {
681+ {
682+ const OPS : & ' static [ $crate:: OpDecl ] = & [ $( $decl ) ,* ] ;
683+ :: std:: borrow:: Cow :: Borrowed ( OPS )
684+ }
685+ } ;
686+
687+ // With type parameters: a plain `const` item inside `fn ext<P: ..>()` cannot
688+ // name `P`, but an *associated* const on a generic type can name that type's
689+ // own parameters. So the table becomes `__OpsTable::<P>::OPS`, which the
690+ // compiler const-evaluates once per monomorphization and places in static
691+ // memory — one static table per `P`, with `Extension::ops` borrowing the one
692+ // that belongs to this instantiation. `init_ops` then hands it to the realm
693+ // as an `OpDeclStorage::Static` run, exactly like the non-generic case.
694+ ( ! __ops_table__ [ $( $param: ident : $type: ident ) + ] [ $( $decl: expr ) ,* $( , ) ? ] ) => {
695+ {
696+ #[ doc( hidden) ]
697+ #[ allow( non_camel_case_types, dead_code, reason = "generated by macro" ) ]
698+ struct __OpsTable< $( $param : $type + ' static ) ,+ >(
699+ :: std:: marker:: PhantomData <( $( $param, ) + ) >,
700+ ) ;
701+
702+ impl < $( $param : $type + ' static ) ,+ > __OpsTable< $( $param ) ,+ > {
703+ const OPS : & ' static [ $crate:: OpDecl ] = & [ $( $decl ) ,* ] ;
704+ }
705+
706+ :: std:: borrow:: Cow :: Borrowed ( <__OpsTable< $( $param ) ,+ >>:: OPS )
707+ }
708+ } ;
709+
664710 ( ! __ops__ $ext: ident __eot__) => {
665711 } ;
666712
@@ -1082,4 +1128,135 @@ mod tests {
10821128 ) ;
10831129 assert_eq ! ( "a" , files[ 0 ] . specifier) ;
10841130 }
1131+
1132+ mod op_tables {
1133+ use std:: borrow:: Cow ;
1134+
1135+ use crate :: extension_set:: init_ops;
1136+ use crate :: op2;
1137+ use crate :: ops:: OpDeclStorage ;
1138+ use crate :: ops_builtin:: op_void_async;
1139+ use crate :: ops_builtin:: op_void_sync;
1140+
1141+ pub trait TestParam {
1142+ const ID : u32 ;
1143+ }
1144+
1145+ /// Resembles the generic ops in `deno_node`/`deno_http`: the parameter is
1146+ /// named in the op's turbofish and reached through the op body.
1147+ #[ op2( fast) ]
1148+ fn op_generic_probe < P : TestParam + ' static > ( ) -> u32 {
1149+ P :: ID
1150+ }
1151+
1152+ crate :: extension!( const_table_ext, ops = [ op_void_sync, op_void_async] , ) ;
1153+
1154+ crate :: extension!(
1155+ generic_table_ext,
1156+ parameters = [ P : TestParam ] ,
1157+ ops = [ op_generic_probe<P >, op_void_sync] ,
1158+ ) ;
1159+
1160+ /// Mirrors `deno_node`'s shape: several parameters, only some of which any
1161+ /// given op names, plus a parameter used by the options/state function.
1162+ #[ op2( fast) ]
1163+ fn op_generic_multi < A : TestParam + ' static , B : TestParam + ' static > ( ) -> u32
1164+ {
1165+ A :: ID + B :: ID
1166+ }
1167+
1168+ crate :: extension!(
1169+ generic_multi_ext,
1170+ parameters = [ A : TestParam , B : TestParam ] ,
1171+ ops = [
1172+ op_generic_multi<A , B >,
1173+ op_generic_probe<B >,
1174+ op_void_async,
1175+ ] ,
1176+ options = { marker: u32 } ,
1177+ state = |state, options| {
1178+ state. put( options. marker + A :: ID + B :: ID ) ;
1179+ } ,
1180+ ) ;
1181+
1182+ struct P1 ;
1183+ impl TestParam for P1 {
1184+ const ID : u32 = 1 ;
1185+ }
1186+
1187+ struct P2 ;
1188+ impl TestParam for P2 {
1189+ const ID : u32 = 2 ;
1190+ }
1191+
1192+ /// A non-generic extension's op table is a `const` in static memory, so
1193+ /// `Extension::ops` borrows it and the realm never copies the decls.
1194+ #[ test]
1195+ fn non_generic_extension_table_is_static ( ) {
1196+ let ext = const_table_ext:: init ( ) ;
1197+ assert ! ( matches!( ext. ops, Cow :: Borrowed ( _) ) ) ;
1198+ assert_eq ! ( ext. ops. len( ) , 2 ) ;
1199+
1200+ // Two calls hand out the very same static allocation.
1201+ let a = const_table_ext:: init ( ) ;
1202+ let b = const_table_ext:: init ( ) ;
1203+ assert_eq ! ( a. ops. as_ptr( ) , b. ops. as_ptr( ) ) ;
1204+ }
1205+
1206+ /// ... and it reaches the realm as an `OpDeclStorage::Static` run.
1207+ #[ test]
1208+ fn non_generic_extension_reaches_realm_as_static ( ) {
1209+ let mut extensions = vec ! [ const_table_ext:: init( ) ] ;
1210+ let ( decls, _methods) = init_ops ( & [ ] , & mut extensions) ;
1211+ let ext_decls = decls
1212+ . iter ( )
1213+ . find ( |d| d. len ( ) == 2 )
1214+ . expect ( "extension ops missing" ) ;
1215+ assert ! ( matches!( ext_decls, OpDeclStorage :: Static ( _) ) ) ;
1216+ }
1217+
1218+ /// A generic extension's table is an associated const on a generic helper
1219+ /// type, so it is also a `const` in static memory — one per
1220+ /// monomorphization.
1221+ #[ test]
1222+ fn generic_extension_table_is_static ( ) {
1223+ let ext = generic_table_ext:: init :: < P1 > ( ) ;
1224+ assert ! ( matches!( ext. ops, Cow :: Borrowed ( _) ) ) ;
1225+ assert_eq ! ( ext. ops. len( ) , 2 ) ;
1226+
1227+ // Two inits of the *same* monomorphization share one static allocation.
1228+ let a = generic_table_ext:: init :: < P1 > ( ) ;
1229+ let b = generic_table_ext:: init :: < P1 > ( ) ;
1230+ assert_eq ! ( a. ops. as_ptr( ) , b. ops. as_ptr( ) ) ;
1231+ }
1232+
1233+ /// Each monomorphization must get its own table: if they collapsed into
1234+ /// one, `generic_table_ext::init::<P2>()` would dispatch to `P1`'s ops.
1235+ #[ test]
1236+ fn generic_extension_table_is_per_monomorphization ( ) {
1237+ let p1 = generic_table_ext:: init :: < P1 > ( ) ;
1238+ let p2 = generic_table_ext:: init :: < P2 > ( ) ;
1239+ assert_ne ! ( p1. ops. as_ptr( ) , p2. ops. as_ptr( ) ) ;
1240+
1241+ let m11 = generic_multi_ext:: init :: < P1 , P1 > ( 0 ) ;
1242+ let m12 = generic_multi_ext:: init :: < P1 , P2 > ( 0 ) ;
1243+ let m12b = generic_multi_ext:: init :: < P1 , P2 > ( 0 ) ;
1244+ assert ! ( matches!( m11. ops, Cow :: Borrowed ( _) ) ) ;
1245+ assert_ne ! ( m11. ops. as_ptr( ) , m12. ops. as_ptr( ) ) ;
1246+ assert_eq ! ( m12. ops. as_ptr( ) , m12b. ops. as_ptr( ) ) ;
1247+ }
1248+
1249+ /// ... and it reaches the realm as an `OpDeclStorage::Static` run, just
1250+ /// like a non-generic extension's table.
1251+ #[ test]
1252+ fn generic_extension_reaches_realm_as_static ( ) {
1253+ let mut extensions = vec ! [ generic_table_ext:: init:: <P1 >( ) ] ;
1254+ let ( decls, _methods) = init_ops ( & [ ] , & mut extensions) ;
1255+ let ext_decls = decls
1256+ . iter ( )
1257+ . find ( |d| d. len ( ) == 2 )
1258+ . expect ( "extension ops missing" ) ;
1259+ assert ! ( matches!( ext_decls, OpDeclStorage :: Static ( _) ) ) ;
1260+ }
1261+ }
10851262}
0 commit comments