Skip to content

Instantly share code, notes, and snippets.

@sagudev
Last active August 26, 2024 16:04
Show Gist options
  • Save sagudev/c4a029cfccda9553adac32749a9ba583 to your computer and use it in GitHub Desktop.
Save sagudev/c4a029cfccda9553adac32749a9ba583 to your computer and use it in GitHub Desktop.

Modular Script

Stage 1: by seperating generated bindings/implementations using associated types in traits:

Existing work

issue: servo/servo#1799, https://github.com/servo/servo/wiki/Prototype-ways-of-splitting-the-script-crate

blog: https://servo.org/blog/2018/08/09/gsoc-generic-servo/

PR: servo/servo#21371

jdm's minimal example (already contains codegen changes): servo/servo#1799 (comment)

Main idea

jdm's idea with asajeffrey's idea:

mod script_bindings {
    // this module can be considered as "forward declaratins"
    // but in servo it should also contain all generated stuff from codegen (uses)
    // while actual defieinitions/implementations is outside

    pub trait TypeHolderTrait: Sized {
        type Dom: DomTrait<Self>;
        type WGPU: WgpuTrait<Self>;
    }

    // all exposed functions need to be behind trait
    // this is not a problem for webidl exposed methods
    // as the trait for them is generated by codegen
    pub trait DomTrait<TH: TypeHolderTrait> {
        fn method_on_dom(&self) -> &TH::WGPU;
    }

    pub trait WgpuTrait<TH: TypeHolderTrait> {
        fn method_on_wgpu(&self) -> &TH::Dom;
    }
}

mod script {
    use crate::script_bindings::TypeHolderTrait;
    use crate::script_bindings::DomTrait;

    pub struct ActualDom<TH: TypeHolderTrait> {
        wgpu: TH::WGPU,
    }

    /// This method is not exposed via traits,
    /// so users needs to depend on script directly
    ///
    /// See `uses_no_trait` for usage
    impl<TH: TypeHolderTrait> ActualDom<TH> {
        pub fn no_trait(&self) {

        }
    }

    impl<TH: TypeHolderTrait> DomTrait<TH> for ActualDom<TH> {
        fn method_on_dom(&self) -> &TH::WGPU {
            &self.wgpu
        }
    }

    pub fn start<TH: TypeHolderTrait>() {
        // ...
    }
}

// This should be done in stage 2 after bindings/impl separations is done
mod script_webgpu {
    use crate::script_bindings::TypeHolderTrait;
    use crate::script_bindings::WgpuTrait;

    pub struct ActualWGPU<TH: TypeHolderTrait> {
        dom: TH::Dom,
    }

    /// Here we use associated type binding syntax to tell compiler
    /// to use script::ActualDom for Dom type
    /// this allows us to call methods on ActualDom that were not "forward declered" via traits
    impl<TH: TypeHolderTrait<Dom = crate::script::ActualDom<TH>>> ActualWGPU<TH> {
        pub fn uses_no_trait(&self) {
            self.dom.no_trait()
        }
    }

    impl<TH: TypeHolderTrait> WgpuTrait<TH> for ActualWGPU<TH> {
        fn method_on_wgpu(&self) -> &TH::Dom {
            &self.dom
        }
    }
}

/// This links implementations from script-* with "forward declarations" from script_bindings
pub struct TypeHolder;
impl script_bindings::TypeHolderTrait for TypeHolder {
    type Dom = script::ActualDom<Self>;
    type WGPU = script_webgpu::ActualWGPU<Self>;
}

fn main() {
    // when starting script we just need to put puzzle together
    script::start::<TypeHolder>();
}

More details

servo/servo#1799 (comment): For non-IDL methods that are impl on concrete DOM types we can use specific traits for for concrete DOM types (ex: GlobalScopeHelpers that would be implemented on GlobalScope), to add this to traits that are impl on DomTypes::GlobalScope, we can use new Bindings.conf field:

'GlobalScope': {
    'traits': ['GlobalScopeHelpers'],
},

and user will have to manually define it in core script crate:

pub trait GlobalScopeHelpers<D: crate::codegen::DomTypes::DomTypes<GlobalScope = Self>> {
    fn global_scope_from_object(obj: *mut JSObject) -> DomRoot<Self>;

    fn global_scope_origin(global: &Self) -> &servo_url::MutableOrigin;
}

but this is only worth doing on very commonly used methods, else we should just use associated type binding syntax (as showcased by uses_no_trait).

functions that are not methods should simply be defined in core script with generic arg throw_dom_exception<D: DomTypes>(...).

Proposed directory structure

components/
            script/
                lib.rs -- where we will link impl with declerations, home of TypeHolder struct
                Cargo.toml
                core/ -- core script stuff & bindings
                    lib.rs
                    Cargo.toml
                    ...
                main/ -- actual implementation for DOM
                webgpu/ -- actual implementation for WebGPU DOM objects (stage 2)
                ... (place for future modules from stage 2)

(Potential) Problems

Evaluation

To properly evaluate result we need to check 3 things:

  • build times (clean, incremental)
  • binary size
  • performance (specifically bindings performance, but general purpose benches should also do)

Stage 2:

stage 1 only enabled seperation between codegen/impl stuff but we can make it more modular (see script_webgpu example), but all generated stuff will still live in single crate. This will make it easier to disable some script stuff via features.

The most interesting candidate for tryout is WebGPU, because is mostly standalone, only few points where it uses extrenal stuff:

  • AsyncWGPUListener
  • Navigator includes NavigatorGPU
  • gpucanvascontext
  • some core stuff like promises and JS internals (external array), but those will be simply included from script-core

we need to reevaluate if this still brings us benefit! (although it might still be worth doing).

Parsers are also demonstrated to be standalone enough see servo/servo#21371 and by that point communty should be able to start helping us (peace by peace).

possible seperations:

  • WebGL
  • WebXR
  • HTML
  • ...

Future work: Linkme

servo/servo#32883 (reply in thread)

will enable bigger isolation between modules (still needs to depend on depending modules)

Problems

codegen needs all webidl files

all undefined are external where? type aliases for providers as we need to depend on them anyway

two way depending

EX: canvas has multiple types of context

https://github.com/servo/servo/blob/e956b53827e6c4ad6d3c2b0d9281dfc0e37b89d9/components/script/dom/htmlcanvaselement.rs#L61

we would still want to use associated types in traits to inject types back to main (so this path is more like next step in even more modularization on top of previous work)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment