on master help sign in
Dynamically load grammar libraries at runtime
commitdd2903ff10387c04e933aa37846663131297b8b3 authorBlaž Hrastnik <blaz@mxxn.io> dateSun 11 Jul 2021, 19:36 (5 years ago) parentdd5e808
8 files changed+201−166
.gitignore+1 Cargo.lock+12−13 helix-core/src/indent.rs+2−2 helix-core/src/syntax.rs+6−6 helix-syntax/Cargo.toml+4−2 helix-syntax/build.rs+146−58 helix-syntax/src/lib.rs+30−85 runtime/grammars/.gitkeep
.gitignore+1
@@ -3,3 +3,4 @@ target
33helix-term/rustfmt.toml
44helix-syntax/languages/
55result
6+runtime/grammars
Cargo.lock+12−13
@@ -61,9 +61,6 @@ name = "cc"
6161version = "1.0.69"
6262source = "registry+https://github.com/rust-lang/crates.io-index"
6363checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
64−dependencies = [
65− "jobserver",
66−]
6764
6865[[package]]
6966name = "cfg-if"
@@ -354,8 +351,9 @@ dependencies = [
354351name = "helix-syntax"
355352version = "0.3.0"
356353dependencies = [
354+ "anyhow",
357355 "cc",
358− "serde",
356+ "libloading",
359357 "threadpool",
360358 "tree-sitter",
361359]
@@ -475,15 +473,6 @@ version = "0.4.7"
475473source = "registry+https://github.com/rust-lang/crates.io-index"
476474checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
477475
478−[[package]]
479−name = "jobserver"
480−version = "0.1.22"
481−source = "registry+https://github.com/rust-lang/crates.io-index"
482−checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd"
483−dependencies = [
484− "libc",
485−]
486−
487476[[package]]
488477name = "jsonrpc-core"
489478version = "17.1.0"
@@ -509,6 +498,16 @@ version = "0.2.97"
509498source = "registry+https://github.com/rust-lang/crates.io-index"
510499checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
511500
501+[[package]]
502+name = "libloading"
503+version = "0.7.0"
504+source = "registry+https://github.com/rust-lang/crates.io-index"
505+checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a"
506+dependencies = [
507+ "cfg-if 1.0.0",
508+ "winapi",
509+]
510+
512511[[package]]
513512name = "lock_api"
514513version = "0.4.4"
helix-core/src/indent.rs+2−2
@@ -253,14 +253,14 @@ where
253253
254254 let doc = Rope::from(doc);
255255 use crate::syntax::{
256− Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
256+ Configuration, IndentationConfiguration, LanguageConfiguration, Loader,
257257 };
258258 use once_cell::sync::OnceCell;
259259 let loader = Loader::new(Configuration {
260260 language: vec![LanguageConfiguration {
261261 scope: "source.rust".to_string(),
262262 file_types: vec!["rs".to_string()],
263− language_id: Lang::Rust,
263+ language_id: "Rust".to_string(),
264264 highlight_config: OnceCell::new(),
265265 //
266266 roots: vec![],
helix-core/src/syntax.rs+6−6
@@ -5,7 +5,7 @@ use crate::{
55 Rope, RopeSlice, Tendril,
66};
77
8−pub use helix_syntax::{get_language, get_language_name, Lang};
8+pub use helix_syntax::get_language;
99
1010use arc_swap::ArcSwap;
1111
@@ -31,7 +31,7 @@ pub struct Configuration {
3131#[serde(rename_all = "kebab-case")]
3232pub struct LanguageConfiguration {
3333 #[serde(rename = "name")]
34− pub(crate) language_id: Lang,
34+ pub(crate) language_id: String,
3535 pub scope: String, // source.rust
3636 pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
3737 pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
@@ -153,7 +153,7 @@ fn read_query(language: &str, filename: &str) -> String {
153153
154154impl LanguageConfiguration {
155155 fn initialize_highlight(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
156− let language = get_language_name(self.language_id).to_ascii_lowercase();
156+ let language = self.language_id.to_ascii_lowercase();
157157
158158 let highlights_query = read_query(&language, "highlights.scm");
159159 // always highlight syntax errors
@@ -166,7 +166,7 @@ impl LanguageConfiguration {
166166 if highlights_query.is_empty() {
167167 None
168168 } else {
169− let language = get_language(self.language_id);
169+ let language = get_language(&crate::RUNTIME_DIR, &self.language_id).ok()?;
170170 let config = HighlightConfiguration::new(
171171 language,
172172 &highlights_query,
@@ -198,7 +198,7 @@ impl LanguageConfiguration {
198198 pub fn indent_query(&self) -> Option<&IndentQuery> {
199199 self.indent_query
200200 .get_or_init(|| {
201− let language = get_language_name(self.language_id).to_ascii_lowercase();
201+ let language = self.language_id.to_ascii_lowercase();
202202
203203 let toml = load_runtime_file(&language, "indents.toml").ok()?;
204204 toml::from_slice(toml.as_bytes()).ok()
@@ -1802,7 +1802,7 @@ mod test {
18021802 .map(String::from)
18031803 .collect();
18041804
1805− let language = get_language(Lang::Rust);
1805+ let language = get_language(&crate::RUNTIME_DIR, "Rust").unwrap();
18061806 let config = HighlightConfiguration::new(
18071807 language,
18081808 &std::fs::read_to_string(
helix-syntax/Cargo.toml+4−2
@@ -12,8 +12,10 @@ include = ["src/**/*", "languages/**/*", "build.rs", "!**/docs/**/*", "!**/test/
1212
1313[dependencies]
1414tree-sitter = "0.19"
15−serde = { version = "1.0", features = ["derive"] }
15+libloading = "0.7"
16+anyhow = "1"
1617
1718[build-dependencies]
18−cc = { version = "1", features = ["parallel"] }
19+cc = { version = "1" }
1920threadpool = { version = "1.0" }
21+anyhow = "1"
helix-syntax/build.rs+146−58
@@ -1,5 +1,7 @@
1+use anyhow::Result;
12use std::fs;
23use std::path::PathBuf;
4+use std::time::SystemTime;
35
46use std::sync::mpsc::channel;
57
@@ -15,66 +17,156 @@ fn collect_tree_sitter_dirs(ignore: &[String]) -> Vec<String> {
1517 dirs
1618}
1719
18−fn collect_src_files(dir: &str) -> (Vec<String>, Vec<String>) {
19− eprintln!("Collect files for {}", dir);
20+#[cfg(unix)]
21+const DYLIB_EXTENSION: &str = "so";
2022
21− let mut c_files = Vec::new();
22− let mut cpp_files = Vec::new();
23− let path = PathBuf::from("languages").join(&dir).join("src");
24− for entry in fs::read_dir(path).unwrap().flatten() {
25− let path = entry.path();
26− if path
27− .file_stem()
28− .unwrap()
29− .to_str()
30− .unwrap()
31− .starts_with("binding")
32− {
33− continue;
23+#[cfg(windows)]
24+const DYLIB_EXTENSION: &str = "dll";
25+
26+// const BUILD_TARGET: &'static str = env!("BUILD_TARGET");
27+
28+use anyhow::{anyhow, Context};
29+use std::{path::Path, process::Command};
30+
31+fn build_library(src_path: &Path, language: &str) -> Result<()> {
32+ let header_path = src_path;
33+ // let grammar_path = src_path.join("grammar.json");
34+ let parser_path = src_path.join("parser.c");
35+ let mut scanner_path = src_path.join("scanner.c");
36+
37+ let scanner_path = if scanner_path.exists() {
38+ Some(scanner_path)
39+ } else {
40+ scanner_path.set_extension("cc");
41+ if scanner_path.exists() {
42+ Some(scanner_path)
43+ } else {
44+ None
45+ }
46+ };
47+ let parser_lib_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../runtime/grammars");
48+ let mut library_path = parser_lib_path.join(language);
49+ library_path.set_extension(DYLIB_EXTENSION);
50+
51+ let recompile = needs_recompile(&library_path, &parser_path, &scanner_path)
52+ .with_context(|| "Failed to compare source and binary timestamps")?;
53+
54+ if !recompile {
55+ return Ok(());
56+ }
57+
58+ let mut config = cc::Build::new();
59+ config.cpp(true).opt_level(2).cargo_metadata(false);
60+ // .target(BUILD_TARGET)
61+ // .host(BUILD_TARGET);
62+ let compiler = config.get_compiler();
63+ let mut command = Command::new(compiler.path());
64+ for (key, value) in compiler.env() {
65+ command.env(key, value);
66+ }
67+
68+ if cfg!(windows) {
69+ command
70+ .args(&["/nologo", "/LD", "/I"])
71+ .arg(header_path)
72+ .arg("/Od")
73+ .arg(parser_path);
74+ if let Some(scanner_path) = scanner_path.as_ref() {
75+ command.arg(scanner_path);
3476 }
35− if let Some(ext) = path.extension() {
36− if ext == "c" {
37− c_files.push(path.to_str().unwrap().to_string());
38− } else if ext == "cc" || ext == "cpp" || ext == "cxx" {
39− cpp_files.push(path.to_str().unwrap().to_string());
77+ command
78+ .arg("/link")
79+ .arg(format!("/out:{}", library_path.to_str().unwrap()));
80+ } else {
81+ command
82+ .arg("-shared")
83+ .arg("-fPIC")
84+ .arg("-fno-exceptions")
85+ .arg("-g")
86+ .arg("-I")
87+ .arg(header_path)
88+ .arg("-o")
89+ .arg(&library_path)
90+ .arg("-O2");
91+ if let Some(scanner_path) = scanner_path.as_ref() {
92+ if scanner_path.extension() == Some("c".as_ref()) {
93+ command.arg("-xc").arg("-std=c99").arg(scanner_path);
94+ } else {
95+ command.arg(scanner_path);
4096 }
4197 }
98+ command.arg("-xc").arg(parser_path);
4299 }
43− (c_files, cpp_files)
44−}
45100
46−fn build_c(files: Vec<String>, language: &str) {
47− let mut build = cc::Build::new();
48− for file in files {
49− build
50− .file(&file)
51− .include(PathBuf::from(file).parent().unwrap())
52− .pic(true)
53− .warnings(false);
101+ let output = command
102+ .output()
103+ .with_context(|| "Failed to execute C compiler")?;
104+ if !output.status.success() {
105+ return Err(anyhow!(
106+ "Parser compilation failed.\nStdout: {}\nStderr: {}",
107+ String::from_utf8_lossy(&output.stdout),
108+ String::from_utf8_lossy(&output.stderr)
109+ ));
54110 }
55− build.compile(&format!("tree-sitter-{}-c", language));
111+
112+ Ok(())
113+}
114+fn needs_recompile(
115+ lib_path: &Path,
116+ parser_c_path: &Path,
117+ scanner_path: &Option<PathBuf>,
118+) -> Result<bool> {
119+ if !lib_path.exists() {
120+ return Ok(true);
121+ }
122+ let lib_mtime = mtime(lib_path)?;
123+ if mtime(parser_c_path)? > lib_mtime {
124+ return Ok(true);
125+ }
126+ if let Some(scanner_path) = scanner_path {
127+ if mtime(scanner_path)? > lib_mtime {
128+ return Ok(true);
129+ }
130+ }
131+ Ok(false)
56132}
57133
58−fn build_cpp(files: Vec<String>, language: &str) {
59− let mut build = cc::Build::new();
134+fn mtime(path: &Path) -> Result<SystemTime> {
135+ Ok(fs::metadata(path)?.modified()?)
136+}
60137
61− let flag = if build.get_compiler().is_like_msvc() {
62− "/std:c++17"
63− } else {
64− "-std=c++14"
65− };
138+// fn build_c(files: Vec<String>, language: &str) {
139+// let mut build = cc::Build::new();
140+// for file in files {
141+// build
142+// .file(&file)
143+// .include(PathBuf::from(file).parent().unwrap())
144+// .pic(true)
145+// .warnings(false);
146+// }
147+// build.compile(&format!("tree-sitter-{}-c", language));
148+// }
66149
67− for file in files {
68− build
69− .file(&file)
70− .include(PathBuf::from(file).parent().unwrap())
71− .pic(true)
72− .warnings(false)
73− .cpp(true)
74− .flag_if_supported(flag);
75− }
76− build.compile(&format!("tree-sitter-{}-cpp", language));
77−}
150+// fn build_cpp(files: Vec<String>, language: &str) {
151+// let mut build = cc::Build::new();
152+
153+// let flag = if build.get_compiler().is_like_msvc() {
154+// "/std:c++17"
155+// } else {
156+// "-std=c++14"
157+// };
158+
159+// for file in files {
160+// build
161+// .file(&file)
162+// .include(PathBuf::from(file).parent().unwrap())
163+// .pic(true)
164+// .warnings(false)
165+// .cpp(true)
166+// .flag_if_supported(flag);
167+// }
168+// build.compile(&format!("tree-sitter-{}-cpp", language));
169+// }
78170
79171fn build_dir(dir: &str, language: &str) {
80172 println!("Build language {}", language);
@@ -92,13 +184,9 @@ fn build_dir(dir: &str, language: &str) {
92184 eprintln!("You can fix in using 'git submodule init && git submodule update --recursive'.");
93185 std::process::exit(1);
94186 }
95− let (c, cpp) = collect_src_files(dir);
96− if !c.is_empty() {
97− build_c(c, language);
98− }
99− if !cpp.is_empty() {
100− build_cpp(cpp, language);
101− }
187+
188+ let path = Path::new("languages").join(dir).join("src");
189+ build_library(&path, language).unwrap();
102190}
103191
104192fn main() {
@@ -129,6 +217,6 @@ fn main() {
129217 // drop(tx);
130218 assert_eq!(rx.try_iter().sum::<usize>(), n_jobs);
131219
132− build_dir("tree-sitter-typescript/tsx", "tsx");
133− build_dir("tree-sitter-typescript/typescript", "typescript");
220+ // build_dir("tree-sitter-typescript/tsx", "tsx");
221+ // build_dir("tree-sitter-typescript/typescript", "typescript");
134222}
helix-syntax/src/lib.rs+30−85
@@ -1,94 +1,39 @@
1−use serde::{Deserialize, Serialize};
1+use anyhow::{Context, Result};
2+use libloading::{Library, Symbol};
23use tree_sitter::Language;
34
4−#[macro_export]
5−macro_rules! mk_extern {
6− ( $( $name:ident ),* ) => {
7− $(
8− extern "C" { pub fn $name() -> Language; }
9− )*
10− };
11−}
12−
13−#[macro_export]
14−macro_rules! mk_enum {
15− ( $( $camel:ident ),* ) => {
16− #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
17− #[serde(rename_all = "lowercase")]
18− pub enum Lang {
19− $(
20− $camel,
21− )*
5+fn replace_dashes_with_underscores(name: &str) -> String {
6+ let mut result = String::with_capacity(name.len());
7+ for c in name.chars() {
8+ if c == '-' {
9+ result.push('_');
10+ } else {
11+ result.push(c);
2212 }
23− };
13+ }
14+ result
2415}
16+#[cfg(unix)]
17+const DYLIB_EXTENSION: &str = "so";
2518
26−#[macro_export]
27−macro_rules! mk_get_language {
28− ( $( ($camel:ident, $name:ident) ),* ) => {
29− #[must_use]
30− pub fn get_language(lang: Lang) -> Language {
31− unsafe {
32− match lang {
33− $(
34− Lang::$camel => $name(),
35− )*
36− }
37− }
38− }
39− };
40−}
19+#[cfg(windows)]
20+const DYLIB_EXTENSION: &str = "dll";
4121
42−#[macro_export]
43−macro_rules! mk_get_language_name {
44− ( $( $camel:ident ),* ) => {
45− #[must_use]
46− pub const fn get_language_name(lang: Lang) -> &'static str {
47− match lang {
48− $(
49− Lang::$camel => stringify!($camel),
50− )*
51− }
52− }
53− };
54−}
22+pub fn get_language(runtime_path: &std::path::Path, name: &str) -> Result<Language> {
23+ let name = name.to_ascii_lowercase();
24+ let mut library_path = runtime_path.join("grammars").join(&name);
25+ // TODO: duplicated under build
26+ library_path.set_extension(DYLIB_EXTENSION);
5527
56−#[macro_export]
57−macro_rules! mk_langs {
58− ( $( ($camel:ident, $name:ident) ),* ) => {
59− mk_extern!($( $name ),*);
60− mk_enum!($( $camel ),*);
61− mk_get_language!($( ($camel, $name) ),*);
62− mk_get_language_name!($( $camel ),*);
28+ let library = unsafe { Library::new(&library_path) }
29+ .with_context(|| format!("Error opening dynamic library {:?}", &library_path))?;
30+ let language_fn_name = format!("tree_sitter_{}", replace_dashes_with_underscores(&name));
31+ let language = unsafe {
32+ let language_fn: Symbol<unsafe extern "C" fn() -> Language> = library
33+ .get(language_fn_name.as_bytes())
34+ .with_context(|| format!("Failed to load symbol {}", language_fn_name))?;
35+ language_fn()
6336 };
37+ std::mem::forget(library);
38+ Ok(language)
6439}
65−
66−mk_langs!(
67− // 1) Name for enum
68− // 2) tree-sitter function to call to get a Language
69− (Agda, tree_sitter_agda),
70− (Bash, tree_sitter_bash),
71− (Cpp, tree_sitter_cpp),
72− (CSharp, tree_sitter_c_sharp),
73− (Css, tree_sitter_css),
74− (C, tree_sitter_c),
75− (Elixir, tree_sitter_elixir),
76− (Go, tree_sitter_go),
77− // (Haskell, tree_sitter_haskell),
78− (Html, tree_sitter_html),
79− (Javascript, tree_sitter_javascript),
80− (Java, tree_sitter_java),
81− (Json, tree_sitter_json),
82− (Julia, tree_sitter_julia),
83− (Latex, tree_sitter_latex),
84− (Nix, tree_sitter_nix),
85− (Php, tree_sitter_php),
86− (Python, tree_sitter_python),
87− (Ruby, tree_sitter_ruby),
88− (Rust, tree_sitter_rust),
89− (Scala, tree_sitter_scala),
90− (Swift, tree_sitter_swift),
91− (Toml, tree_sitter_toml),
92− (Tsx, tree_sitter_tsx),
93− (Typescript, tree_sitter_typescript)
94−);
runtime/grammars/.gitkeepnew file

No changes to the content.