August 5, 2022
By: Kevin

org-mode中增加csharp的执行能力

  1. 扩展emacs
  2. 解决方案
  3. 结果&展望
  4. 使用release模式执行c#代码

扩展emacs

Emacs的扩展浩瀚如海, 基本能涵盖我们所需, 总有好心人先我们一步遇到并解决了问题.

但也不总是这样, 总有些足够特别(聪明)的需求, 需要自我满足一下.

对于非主流语言(是的, 在windows之外的世界, c#是个非主流语言), 扩展是不全的. C#有csharp-mode, 可以做自动格式化和语法高亮.

对于emacs29以后, csharp-mode已经内置, 但是缺乏对c#语言一些最新特性的支持(比如说多行字符串$@"")

但是org-mode中的代码执行(babel扩展)是不全的.

想在emacs里写笔记, 所以, 体验想和写python,lisp,shell一样, 能用org-babel执行我的C#代码.

这涉及到另一个问题: 一段c#代码执行, 需要有工程文件, main函数, 编译后再执行, 这个有点太烦琐了.


解决方案

  • dotnet可以执行后缀为csx的脚本, 接近交互式的使用c#, 需要安装一下: dotnet tool install -g dotnet-script 安装成功后可以执行下 dotnet script 进入一个可以执行c#的shell.
  • 自己写个babel的c#扩展 保存为一个独立文件, 放到emacs的可访问目录
    (require 'ob)
    (require 'csharp-mode)
    
    (add-to-list 'org-babel-tangle-lang-exts '("csharp" . "cs"))
    
    (defvar org-babel-default-header-args:csharp '())
    
    (defun ob-csharp--build-script-run-command (cmdline path)
    "Create run command according to the PATH."
    (format "dotnet script %s %s" path (or cmdline " ")))
    
    (defun org-babel-execute:csharp (body params)
    (let* ((processed-params (org-babel-process-params params))
           (cmpflag (or (cdr (assoc :cmpflag params)) ""))
           (cmdline (or (cdr (assoc :cmdline params)) ""))
           (src-temp (org-babel-temp-file "csharp-src-" ".csx")))
      (with-temp-file src-temp (insert body))
      (let ((results (org-babel-eval (ob-csharp--build-script-run-command cmdline src-temp) "")))
        (org-babel-reassemble-table
         (org-babel-result-cond (cdr (assoc :result-params params))
           (org-babel-read results)
           (let ((tmp-file (org-babel-temp-file "c-")))
             (with-temp-file tmp-file (insert results))
             (org-babel-import-elisp-from-file tmp-file)))
         (org-babel-pick-name
          (cdr (assoc :colname-names params)) (cdr (assoc :colnames params)))
         (org-babel-pick-name
          (cdr (assoc :rowname-names params)) (cdr (assoc :rownames params)))))))
    
    (provide 'ob-csharp)
    ;;; ob-csharp.el ends here
    
  • 在org-babel-load-languages中增加csharp
(org-babel-do-load-languages
 'org-babel-load-languages
 '((R . t)
   (csharp . t) ;; <= 增加在此
   (python . t)
   (js . t)
   (shell . t)
   (emacs-lisp . t)
   (lisp . t)
   (gnuplot . t)))

结果&展望

c#的执行结果

#+begin_src csharp :results pp
  Console.WriteLine("hello world!");
#+end_src

#+RESULTS:
: hello world!

我们可以借助这个方法在org-mode中执行任意想执行的语言, 比如java(提示:java11 引入了一个叫jshell的东西).

#+begin_src java :results pp
  System.out.println("hello world!");
#+end_src

#+RESULTS:
: hello world!

使用release模式执行c#代码

因为我需要对c#的某些函数做性能测试, 必须在release模式下执行代码, 这样使用dotnet-script的这个模式就不是合适了, 所以我对这个插件进行了扩展.

使用如下配置, 注意增加了:release yes启动release 模式执行, 可以使用 :nuget '("lib1" "lib2") 动态从nuget下载使用的库.

#+begin_src csharp :release yes :nuget '("BenchmarkDotNet")
// 要测试的program.cs内容
#+end_src

新的ob-csharp.el的代码如下

(require 'ob)
(require 'csharp-mode)

(add-to-list 'org-babel-tangle-lang-exts '("csharp" . "cs"))

(defvar org-babel-default-header-args:csharp
  '((:release . "no")
    (:nuget . nil))
  "Default arguments for C# Babel blocks.")

(defun ob-csharp--build-script-run-command (cmdline path)
  "Create run command according to the PATH."
  (format "dotnet script %s %s" (or cmdline "") path))

(defun ob-csharp--create-temp-project (nuget-deps code)
  "Create a temporary C# project with NUGET-DEPS and CODE.
Returns the path to the project directory."
  (let ((temp-dir (make-temp-file "ob-csharp-project-" t)))
    (let ((default-directory temp-dir))
      ;; Initialize new console project
      (unless (zerop (call-process "dotnet" nil nil nil "new" "console"))
        (error "Failed to create new dotnet console project"))
      ;; Add NuGet packages if any
      (when (and nuget-deps (listp nuget-deps))
        (dolist (dep nuget-deps)
          (let ((args (split-string dep)))
            (unless (zerop (apply 'call-process "dotnet" nil nil nil "add" "package" args))
              (error "Failed to add NuGet package: %s" dep)))))
      ;; Overwrite Program.cs with user code
      (let ((program-cs (expand-file-name "Program.cs" temp-dir)))
        (with-temp-file program-cs
          (insert code)))
      ;; Verify that the .csproj file exists
      (let ((csproj-files (directory-files temp-dir nil "\\.csproj$")))
        (unless csproj-files
          (error "Project file (.csproj) not found in %s" temp-dir)))
      temp-dir)))

(defun org-babel-execute:csharp (body params)
  "Execute a C# code block with BODY and PARAMS."
  (let* ((processed-params (org-babel-process-params params))
         (release (string= (or (cdr (assoc :release processed-params)) "no") "yes"))
         (nuget (cdr (assoc :nuget processed-params)))
         (cmdline (or (cdr (assoc :cmdline processed-params)) ""))
         (result "")
         (output-buffer "*ob-csharp-output*"))
    (if release
        ;; Release mode: create project, build, and run
        (let ((project-dir (ob-csharp--create-temp-project nuget body)))
          (unwind-protect
              (let ((default-directory project-dir))
                ;; Build the project in Release mode
                (unless (zerop (call-process "dotnet" nil output-buffer t "build" "-c" "Release"))
                  (error "Failed to build the C# project. See %s for details." output-buffer))
                ;; Run the project
                ;; 清空输出缓冲区
                (with-current-buffer (get-buffer-create output-buffer)
                  (erase-buffer))

                (unless (zerop (call-process "dotnet" nil output-buffer t "run" "-c" "Release"))
                  (error "Failed to run the C# project. See %s for details." output-buffer))
                ;; Capture the output
                (with-current-buffer output-buffer
                  (setq result (buffer-string))))
            ;; Clean up: optionally delete the temp project directory
            (delete-directory project-dir t)))
      ;; Script mode: existing behavior
      (let ((src-temp (org-babel-temp-file "csharp-src-" ".csx")))
        (with-temp-file src-temp (insert body))
        (setq result (org-babel-eval (ob-csharp--build-script-run-command cmdline src-temp) ""))))
    ;; Return the result, trimming any trailing whitespace
    (string-trim result)))

(provide 'ob-csharp)
;;; ob-csharp.el ends here
Tags: c# org-mode emacs