這是 R 擴充套件指南,說明建立 R 附加套件、撰寫 R 文件、R 系統和外國語言介面,以及 R API 的流程。
本手冊適用於 R 4.3.3 版(2024-02-29)。
版權所有 © 1999–2023 R Core Team
只要在所有副本上保留版權聲明和此許可聲明,即授予製作和散布本手冊逐字副本的許可。
只要整個衍生作品在與本聲明相同的許可聲明條款下散布,即授予在逐字複製條件下複製和散布本手冊修改版本的許可。
只要此許可聲明可以在 R Core Team 核准的翻譯中陳述,即授予在修改版本上述條件下將本手冊翻譯成另一種語言並複製和散布的許可,但此許可聲明可以陳述在 R Core Team 核准的翻譯中。
Saikat DebRoy(撰寫使用 .Call
和 .External
指南的第一個草稿)和 Adrian Trapletti(提供 C++ 介面資訊)對早期版本的本手冊的貢獻,深表感謝。
套件提供一種機制,可依需要載入選用程式碼、資料和文件。R 發行版本身包含約 30 個套件。
在以下內容中,我們假設您知道 library()
指令,包括其 lib.loc
參數,我們也假設您具備 R CMD INSTALL
工具的基本知識。否則,請在繼續閱讀前查看 R 的說明頁面
?library ?INSTALL
。
對於包含要編譯程式碼的套件,假設運算環境包含多個工具;「R 安裝和管理」手冊說明各個作業系統需要什麼。
建立原始碼套件後,必須使用指令 R CMD INSTALL
安裝該套件。請參閱 R 安裝和管理 中的 附加套件。
支援其他類型的擴充功能(但很少見):請參閱 套件類型。
一些關於術語的說明完成了本引言。這些說明有助於閱讀本手冊,並在尋求協助時準確地描述概念。
套件 是擴充 R 的檔案目錄、原始碼套件(套件的主檔案)、或包含原始碼套件檔案的 tarball,或已安裝套件,即在原始碼套件上執行 R CMD INSTALL
的結果。在某些平台(特別是 macOS 和 Windows)上,也有二進位套件,即包含已安裝套件檔案的 zip 檔案或 tarball,可以解壓縮,而不是從原始碼安裝。
套件不是1函式庫。後者在 R 文件中以兩種意義使用。
針對原始碼套件有許多明確定義的操作。
R CMD INSTALL
或 install.packages
將其安裝在函式庫中。
library()
來討論載入已安裝的套件是明確的,但自從套件命名空間出現以來,這就不再那麼清楚:現在人們常常討論載入套件的命名空間,然後附加套件,使其在搜尋路徑上可見。函式 library
執行這兩個步驟,但套件的命名空間可以在未附加套件的情況下載入(例如透過像 splines::ns
的呼叫)。
在多處提到程式碼或資料的延遲載入概念。這是安裝的一部分,總是會選取 R 程式碼,但資料則為選用。在使用時,套件的 R 物件會在安裝時建立,並儲存在已安裝套件的 R 目錄的資料庫中,在第一次使用時載入至工作階段。這使得 R 工作階段的啟動速度更快,並使用較少的(虛擬)記憶體。(有關技術詳細資料,請參閱R 內部中的延遲載入。)
CRAN 是 WWW 網站的網路,其中包含 R 散佈和貢獻的程式碼,特別是 R 套件。鼓勵 R 的使用者加入協作專案,並將他們自己的套件提交給 CRAN:目前的說明會從 https://CRAN.R-project.org/banner.shtml#submitting 連結。
R 套件的來源包括包含檔案 DESCRIPTION 和 NAMESPACE 的子目錄,以及子目錄 R、data、demo、exec、inst、man、po、src、tests、tools 和 vignettes(其中一些可以遺失,但不能為空)。套件子目錄可能還包含檔案 INDEX、configure、cleanup、LICENSE、LICENCE 和 NEWS。其他檔案,例如 INSTALL(用於非標準安裝說明)、README/README.md2 或 ChangeLog 將會被 R 忽略,但可能對最終使用者有用。工具程式 R CMD build
可以在 build 目錄中新增檔案(但不應將其用於其他用途)。
除非特別提到,3 套件不應包含 Unix 風格的「隱藏」檔案/目錄(即名稱以句點開頭的檔案/目錄)。
檔案 DESCRIPTION 和 INDEX 在以下小節中說明。檔案 NAMESPACE 在 套件命名空間 的章節中說明。
選用的檔案 configure 和 cleanup 分別是在類 Unix 系統上安裝前和(如果選項 --clean 已提供)安裝後執行的(Bourne)shell 程式碼,請參閱 Configure and cleanup。Windows 上的類比是 configure.win 和 cleanup.win。自 Windows 上的 R 4.2.0 起,支援 configure.ucrt 和 cleanup.ucrt,且優先於 configure.win 和 cleanup.win。因此,如果需要,它們可用於提供特定於 UCRT 或 Rtools42 及更新版本的內容,但當不再需要從 R 的舊版本中建置套件時,可能會移除對 .ucrt 檔案的支援,因此檔案可能會重新命名回 .win。
有關 GNU 專案中檔案 NEWS 和 ChangeLog 的慣例,請參閱 https://gnu.dev.org.tw/prep/standards/standards.html#Documentation。
套件子目錄應與套件同名。由於某些檔案系統(例如 Windows 上的檔案系統,以及 macOS 上的預設檔案系統)不區分大小寫,因此強烈建議不要使用大小寫區別來區分不同的套件,以維持可攜性。例如,如果您有一個名為 foo 的套件,請不要再建立一個名為 Foo 的套件。
為確保檔案名稱在不同的檔案系統和支援作業系統中皆有效,ASCII 控制字元以及字元「"」、「*」、「:」、「/」、「<」、「>」、「?」、「\」和「|」在檔案名稱中皆不允許使用。此外,名稱為「con」、「prn」、「aux」、「clock$」、「nul」、「com1」至「com9」,以及「lpt1」至「lpt9」的檔案在轉換為小寫並移除可能的「副檔名」後(例如「lpt5.foo.bar」),皆不允許使用。此外,同一個目錄中的檔案名稱不可僅大小寫不同(請參閱前一段落)。另外,「.Rd」檔案的基本檔名可能會用於 URL 中,因此必須為 ASCII 且不可包含 %
。為了達到最大的可攜性,檔案名稱應僅包含尚未排除的 ASCII 字元(即 A-Za-z0-9._!#$%&+,;=@^(){}'[]
— 我們排除空白,因為許多公用程式不接受檔案路徑中的空白):無法保證所有語言環境都支援非英文的字母字元。建議避免使用 shell 的特殊字元 (){}'[]$~
:~
也用於 Windows 中「8.3」檔案名稱的一部分。此外,套件通常以 tar 檔形式發行,且這些檔案對路徑長度有限制:為了達到最大的可攜性,限制為 100 位元組。
如果可能,原始套件不應包含二進位可執行檔:它們不可移植,而且如果它們具有適當的架構,則會構成安全風險。R CMD check
會警告它們4,除非它們列在套件頂層的 BinaryFiles 檔案中(每行一個檔案路徑)。請注意,即使列出了二進位檔案,CRAN 也不會接受包含二進位檔案的提交。
R 函式 package.skeleton
可以協助建立新套件的結構:有關詳細資訊,請參閱其說明頁面。
DESCRIPTION 檔案包含下列格式的套件基本資訊
Package: pkgname Version: 0.5-1 Date: 2015-01-01 Title: My First Collection of Functions Authors@R: c(person("Joe", "Developer", role = c("aut", "cre"), email = "Joe.Developer@some.domain.net"), person("Pat", "Developer", role = "aut"), person("A.", "User", role = "ctb", email = "A.User@whereever.net")) Author: Joe Developer [aut, cre], Pat Developer [aut], A. User [ctb] Maintainer: Joe Developer <Joe.Developer@some.domain.net> Depends: R (>= 3.1.0), nlme Suggests: MASS Description: A (one paragraph) description of what the package does and why it may be useful. License: GPL (>= 2) URL: https://www.r-project.org, http://www.another.url BugReports: https://pkgname.bugtracker.url
格式為「Debian 控制檔案」版本(請參閱「read.dcf」和 https://www.debian.org/doc/debian-policy/ch-controlfields.html 的說明:R 不需要 UTF-8 編碼,也不支援以「#」開頭的註解)。欄位以 ASCII 名稱開頭,後接冒號:值從冒號和空格後開始。換行(例如,用於長度超過一行的描述)以空格或標籤開頭。欄位名稱區分大小寫:R 使用的所有欄位名稱均為大寫。
為了最大化可攜性,DESCRIPTION 檔案應完全以 ASCII 編寫,如果無法做到,則必須包含「編碼」欄位(請見下方)。
數個選用欄位採用 邏輯值:這些值可以指定為「yes」、「true」、「no」或「false」:大寫值也可以接受。
「套件」、「版本」、「授權」、「說明」、「標題」、「作者」和「維護者」欄位為強制欄位,所有其他欄位為選用欄位。「作者」和「維護者」欄位可以從「Authors@R」自動產生,如果提供了後者,則可以省略:但是,如果它們不是 ASCII,我們建議提供它們。
強制欄位「套件」提供套件名稱。這應僅包含 (ASCII) 字母、數字和句點,至少有兩個字元,且以字母開頭,不以句點結尾。如果需要說明,應在「說明」欄位(而非「標題」欄位)中說明。
強制欄位「版本」提供套件版本。這是一系列至少 兩個(通常為三個)非負整數,由單一的「.」或「-」字元分隔。正規形式如範例所示,而「0.01」或「0.01.0」等版本將會被視為「0.1-0」。這 不是小數,因此,例如 0.9 < 0.75
,因為 9 < 75
。
強制欄位「授權」將在下一小節中討論。
必填的「標題」欄位應提供套件的簡短說明。有些套件清單可能會將標題截斷為 65 個字元。它應使用標題大小寫(亦即,將主要字詞大寫:tools::toTitleCase
可以協助您執行此操作),不使用任何標記,沒有任何延續行,且不以句點結尾(除非是 … 的一部分)。請勿重複套件名稱:它通常會使用名稱為前綴。請使用單引號來指稱其他套件和外部軟體,並使用雙引號來指稱書籍標題(和類似標題)。
必填的「說明」欄位應提供套件功能的完整說明。可以使用多個(完整的)句子,但只能有一個段落。所有預期的讀者群都應能理解(例如,對於 CRAN 套件,所有 CRAN 使用者都應能理解)。良好的做法是不以套件名稱、「此套件」或類似字詞開頭。與「標題」欄位一樣,應使用雙引號來表示引文(包括書籍和文章標題),並使用單引號來表示非英文用法,包括其他套件和外部軟體的名稱。必要時,此欄位也應說明套件名稱。網址應以尖括號括起來,例如「<https://www.r-project.org>」:另請參閱指定網址。
必填的「作者」欄位說明撰寫套件的人員。它是一個純文字欄位,供人類讀者閱讀,但不用於自動處理(例如,萃取所有列出貢獻者的電子郵件地址:請使用「Authors@R」)。請注意,所有重要的貢獻者都必須納入:如果您為包含在 src 目錄中的其他人的作品撰寫 R 封裝程式,您並非唯一的(甚至可能不是主要的)作者。
強制性的「維護者」欄位應提供一個單一名稱,後接一個以尖括號括住的有效(RFC 2822)電子郵件地址。它不應以句點或逗號結尾。此欄位是maintainer
函數所回報的,並由bug.report
使用。對於CRAN套件,它應為個人,而非郵件清單或公司實體:請確保它有效且在套件的生命週期內保持有效。
請注意,如果顯示名稱(尖括號中地址之前的部分)包含逗號或句點等非字母數字字元,則應以雙引號括住。(目前的標準 RFC 5322 允許句點,但 RFC 2822 沒有。)
如果提供了適當的「Authors@R」欄位,則「Author」和「Maintainer」欄位都可以省略。這個欄位可以用來提供套件「作者」的精緻且機器可讀的描述(特別是明確指定其精確的角色),透過適當的 R 程式碼。它應該透過呼叫 person
或一系列呼叫(每個「作者」一個)再串接 c()
來建立類別 "person"
的物件:請參閱上述範例 DESCRIPTION 檔案。角色可以包括「"aut"」(作者)表示完整作者、「"cre"」(建立者)表示套件維護者、「"ctb"」(貢獻者)表示其他貢獻者、「"cph"」(版權持有者,應該是機構或法人團體的合法名稱)等。請參閱 ?person
以取得更多資訊。請注意,預設不會假設任何角色。自動產生的套件引文資訊會利用這個規格。在建置5或安裝時,如果需要,會從中自動產生「Author」和「Maintainer」欄位。
如果版權持有者不是作者,可以使用選用的「Copyright」欄位。如果需要,這可以參照已安裝的檔案:慣例是使用檔案 inst/COPYRIGHTS。
選用的「日期」欄位提供套件目前版本的發行日期。強烈建議6使用符合 ISO 8601 標準的「yyyy-mm-dd」格式。
「依賴」、「匯入」、「建議」、「加強」、「連結至」和「其他儲存庫」欄位會在後面的子區段中討論。
R 系統外部的依賴項應列在「系統需求」欄位中,並可能在獨立的 README 檔案中擴充說明。這包括指定非預設的 C++ 標準和需要 GNU make
。
「網址」欄位可以提供一個 網址 清單,這些 網址 以逗號或空白分隔,例如作者的首頁或可以找到描述軟體的其他資訊的頁面。這些 網址 會轉換成 CRAN 套件清單中的主動超連結。請參閱 指定網址。
「錯誤回報」欄位可以包含一個 網址,用於提交有關套件的錯誤回報。這個 網址 將會由 bug.report
使用,而不是傳送電子郵件給維護人員。瀏覽器會開啟「http://」或「https://」網址。若要指定另一個電子郵件地址來接收錯誤回報,請改用「聯絡」:不過,bug.report
會嘗試從「錯誤回報」中擷取電子郵件地址(最好從「mailto:」網址或包含在尖括號中)。
基本和建議套件(即包含在 R 原始碼發行版中或可從 CRAN 取得,並建議包含在 R 的每個二進位發行版中的套件)有一個「優先順序」欄位,其值分別為「基本」或「建議」。其他套件不得使用這些優先順序。
當 R 程式碼檔案在套件安裝時進行處理時,可以使用「Collate」欄位來控制這些檔案的排序順序。預設會根據「C」地區設定進行排序。如果存在,排序規格必須列出套件中所有 R 程式碼檔案(考量到可能存在的作業系統特定子目錄,請參閱套件子目錄)作為相對於 R 子目錄的檔案路徑清單,清單中的項目以空白分隔。包含空白或引號的路徑需要加上引號。作業系統特定的排序欄位(「Collate.unix」或「Collate.windows」)會優先於「Collate」使用。
「LazyData」邏輯欄位控制 R 資料集是否使用延遲載入。在 2.14.0 之前的版本中使用「LazyLoad」欄位,但現在已忽略此欄位。
「KeepSource」邏輯欄位控制套件程式碼是否使用 keep.source = TRUE
或 FALSE
進行來源處理:對於專門設計為始終與 keep.source = TRUE
搭配使用的套件,可能需要例外地使用此欄位。
「ByteCompile」邏輯欄位控制套件 R 程式碼是否在安裝時進行位元組編譯:預設會進行位元組編譯。這可以使用旗標 --no-byte-compile 安裝來覆寫。
「UseLTO」邏輯欄位用於指出套件中的原始程式碼7是否要使用連結時間最佳化進行編譯(請參閱使用連結時間最佳化),前提是 R 已使用 --enable-lto(預設為 true)或 --enable-lto=R(預設為 false)安裝(或是在 Windows 上,如果 LTO_OPT
已在 MkRules 中設定)。這可以使用旗標 --use-LTO 和 --no-use-LTO 覆寫。據說 LTO 可以為大型且複雜(大量使用範本)的 C++ 專案提供最大的大小和效能改善。
「StagedInstall」邏輯欄位控制套件安裝是否為「分階段」,也就是安裝到暫時位置,並在成功完成後移到最終位置。此欄位在 R 3.6.0 中引入,且預設為 true:它被視為一項暫時措施,可能會在未來取消。
「ZipData」邏輯欄位自 R 2.13.0 起已被忽略。
「Biarch」邏輯欄位用於 Windows,以選取此套件的 INSTALL
選項 --force-biarch。
「BuildVignettes」邏輯欄位可以設定為 false 值,以停止 R CMD build
嘗試建置範例,並防止8 R CMD check
測試此範例。這只應在特殊情況下使用,例如 PDF 包含不屬於套件來源的大型圖形(因此僅限於沒有開放原始碼授權的套件)。
「VignetteBuilder」欄位名稱(以逗號分隔的清單)提供用於建立小插圖的引擎套件。這些套件可能包含目前的套件,或列於「Depends」、「Suggests」或「Imports」中的套件。utils 套件始終會隱含附加。有關詳細資訊,請參閱非 Sweave 小插圖。請注意,例如,如果小插圖的引擎為「knitr::rmarkdown」,則knitr 提供引擎,但使用它需要 knitr 和 rmarkdown,因此這兩個套件都需要在「VignetteBuilder」欄位中,且至少建議使用(因為 rmarkdown 僅由 knitr 建議使用,因此不會自動隨附)。許多使用 knitr 的套件也需要套件 formatR,它建議使用,因此使用者套件也需要這樣做,並將其包含在「VignetteBuilder」中。
如果 DESCRIPTION 檔案並非完全使用 ASCII,它應包含一個指定編碼的「Encoding」欄位。這用於 DESCRIPTION 檔案本身和 R 與 NAMESPACE 檔案的編碼,以及 .Rd 檔案的預設編碼。執行 R CMD check
時,範例假設使用此編碼,且此編碼用於 CITATION
檔案的編碼。已知只有 latin1
和 UTF-8
編碼名稱是可攜式的。(除非實際需要,否則請勿指定編碼:這樣做會讓套件 較不 可攜式。如果套件已指定編碼,您應在使用該編碼的區域設定中執行 R CMD build
等。)
如果套件包含需要編譯的原生程式碼,則「NeedsCompilation」欄位應設為 "yes"
,否則設為 "no"
(當套件可以在任何平台上從原始碼安裝,而無需其他工具時)。這由 R >= 2.15.2 中的 install.packages(type = "both")
在二進位套件為常態的平台上使用:通常由 R CMD build
或儲存庫設定,假設當且僅當套件具有 src 目錄時才需要編譯。
「OS_type」欄位指定套件的目標作業系統。如果存在,它應為 unix
或 windows
之一,並表示套件只能安裝在「.Platform$OS.type」具有該值的平台上。
「Type」欄位指定套件的類型:請參閱 套件類型。
可以使用欄位「分類/ACM」或「分類/ACM-2012」(使用電腦學會的電腦分類系統,https://www.acm.org/publications/class-2012;前者指 1998 年版本),「分類/JEL」(經濟文獻期刊分類系統,https://www.aeaweb.org/econlit/jelCodes.php),或「分類/MSC」或「分類/MSC-2010」(美國數學學會的數學主分類,https://mathscinet.ams.org/msc/msc2010.html;前者指 2000 年版本)為套件內容新增主題分類。主題分類應為各分類代碼的逗號分隔清單,例如「分類/ACM: G.4, H.2.8, I.5.1」。
可以使用「語言」欄位來表示套件文件不是英文:這應為標準(非私人使用或既得權益)IETF 語言標籤的逗號分隔清單,目前由 RFC 5646(https://www.rfc-editor.org/rfc/rfc5646)定義(另請參閱 https://en.wikipedia.org/wiki/IETF_language_tag),亦即,使用語言次標籤,其本質上為 2 個字母的 ISO 639-1(https://en.wikipedia.org/wiki/ISO_639-1)或 3 個字母的 ISO 639-3(https://en.wikipedia.org/wiki/ISO_639-3)語言代碼。
「RdMacros」欄位可用於儲存套件清單(以逗號分隔),目前套件將從中匯入 Rd 巨集定義。這些套件也應列在「Imports」(或「Depends」)中。這些套件中的巨集會在系統巨集之後匯入,順序依「RdMacros」欄位中所列,載入目前套件中的任何巨集定義之前。在 man 目錄中個別 .Rd 檔案中的巨集定義會在最後載入,且為該檔案後續部分的區域性定義。如有重複,將使用最後載入的定義。9 R CMD Rd2pdf
和 R CMD Rdconv
都有選用旗標 --RdMacros=pkglist。此選項也是套件名稱的逗號分隔清單,且優先於 DESCRIPTION 中所提供的值。使用 Rd 巨集的套件應相依於 R 3.2.0 或更新版本。
注意:不應有「Built」或「Packaged」欄位,因為這些欄位會由套件管理工具新增。
對於未在此處提及的其他欄位,並無使用限制(但使用這些欄位名稱的其他大小寫會造成混淆)。欄位 Note
、Contact
(用於聯絡作者/開發人員10)和 MailingList
為常見用法。部分儲存庫(包括 CRAN 和 R-forge)會新增自己的欄位。
針對可能發布的套件進行授權是一項重要但可能複雜的主題。
包含授權資訊非常重要!否則,其他人甚至可能在法律上無法正確地散布套件的副本,更不用說使用它了。
套件管理工具使用「自由或開放原始碼軟體」(FOSS,例如,https://en.wikipedia.org/wiki/FOSS) 授權的概念:其想法是 R 及其套件的一些使用者想要將自己限制在這種軟體中。其他人需要確保沒有任何限制阻止他們使用套件,例如禁止商業或軍事用途。FOSS 軟體的核心原則是對使用者或使用沒有任何限制。
請勿使用「授權」欄位來提供有關著作權持有人的資訊:如有需要,請使用「著作權」欄位。
在 DESCRIPTION 檔案中的強制性「授權」欄位應以標準化格式指定套件的授權。可透過直線符號 via 來表示其他選項。個別規格必須為下列其中一項
GPL-2 GPL-3 LGPL-2 LGPL-2.1 LGPL-3 AGPL-3 Artistic-2.0 BSD_2_clause BSD_3_clause MIT
如透過 https://www.R-project.org/Licenses/ via 提供,並包含在 R 來源或主目錄的 share/licenses 子目錄中。
縮寫 GPL
和 LGPL
含糊不清,通常11表示任何版本的授權:但最好不要使用它們。
多個授權可以使用「|」(由空格包圍)分隔指定,在這種情況下,使用者可以選擇任何替代方案。
如果套件授權限制基本授權(在允許的情況下,例如使用 GPL-3 或 AGPL-3 與歸屬條款),則應將附加條款放入檔案 LICENSE(或 LICENCE),並將字串「+ file LICENSE」(或「+ file LICENCE」,分別)附加到對應的個別授權規格(最好用空格包圍「+」)。請注意,幾個常用的授權不允許限制:這包括 GPL-2,因此任何包含它的規格。
標準化規格的範例包括
License: GPL-2 License: LGPL (>= 2.0, < 3) | Mozilla Public License License: GPL-2 | file LICENCE License: GPL (>= 2) | BSD_3_clause + file LICENSE License: Artistic-2.0 | AGPL-3 + file LICENSE
請特別注意,「公共領域」不是有效的授權,因為它在某些司法管轄區不被承認。
請確保您選擇的授權也涵蓋套件的任何依賴項(包括系統依賴項):特別重要的是,此類依賴項的使用限制應清楚顯示給正在閱讀您的 DESCRIPTION 檔案的人員。
欄位「License_is_FOSS」和「License_restricts_use」可以由無法從授權名稱計算資訊的儲存庫新增。「License_is_FOSS: yes」用於已知為 FOSS 的授權,「License_restricts_use」如果已知 LICENSE 檔案限制使用者或使用,或已知不限制,則其值可以為「yes」或「no」。例如,available.packages
篩選器會使用這些值。
選用的檔案 LICENSE/LICENCE 包含套件授權的副本。為避免混淆,僅在 DESCRIPTION 檔案的「License」欄位中參照到此類檔案時才包含此類檔案。
雖然您應該可以自由地在您的原始碼發行版中包含授權檔案,但請不要安排安裝 GNU COPYING 或 COPYING.LIB 檔案的另一個副本,而是參照 https://www.R-project.org/Licenses/ 上的副本,並包含在 R 發行版中(在目錄 share/licenses 中)。由於會安裝名為 LICENSE 或 LICENCE 的檔案,因此請勿將這些名稱用於標準授權檔案。若要包含有關授權的註解,而非授權主體,請使用類似 LICENSE.note 的檔案名稱。
一些「標準」授權書比較像是授權範本,需要額外的資訊才能透過「+ file LICENSE」(「+」前後有空格)完成。COPYRIGHT HOLDER 的附加資訊必須提供實際的法律實體(而非模糊的字眼,例如「套件作者姓名」):如果有多個,應依貢獻度遞減順序列出。
「Depends」欄位提供此套件相依的套件名稱,以逗號分隔。當呼叫 library
或 require
時,這些套件會在目前套件之前附加。每個套件名稱後方可選擇加上括號內的註解,以指定版本需求。註解應包含比較運算子、空白和有效的版本號碼,例如「MASS (>= 3.1-20)」。
「Depends」欄位也可以指定對特定 R 版本的相依性,例如,如果套件只能使用 R 版本 4.0.0 或更新版本,請在「Depends」欄位中包含「R (>= 4.0)」(如範例所示,尾數 0 可省略,建議省略)。您也可以要求 R-devel 或 R-patched 的特定 SVN 修訂版,例如「R (>= 2.14.0), R (>= r56550)」需要 2011 年 7 月底的 R-devel 之後版本(包括 2.14.0 的已發布版本)。
未指定版本明細就宣告依賴 R
,或依賴套件 base 沒有意義:這是 R 套件,而 base 套件總是可用。
套件或「R」可以在「Depends」欄位中出現多次,例如提供可接受版本的上下界限。
不建議使用補丁層級(第三位數)非零的 R 依賴關係。對其他依賴的套件執行此操作會導致其他套件在系列中較早版本中無法使用,例如版本 4.x.1 在北半球學年中廣泛使用。
library
和 R 套件檢查功能都使用此欄位:因此,使用不當語法或誤用「Depends」欄位來評論可能需要的其他軟體是錯誤的。R INSTALL
功能會檢查所使用的 R 版本是否足夠新以安裝套件,並且會在檢查版本需求後附加所指定的套件清單(在目前套件之前)。
「Imports」欄位列出從中匯入命名空間的套件(如 NAMESPACE 檔案中所指定),但不需要附加的套件。透過「::」和「:::」運算子存取的命名空間必須在此列出,或在「Suggests」或「Enhances」中列出(請見下方)。理想情況下,此欄位會包含所有使用的標準套件,且包含使用 S4 的套件非常重要(因為其類別定義可能會變更,而 DESCRIPTION 檔案用於決定在這種情況下重新安裝哪些套件)。在「Depends」欄位中宣告的套件不應同時出現在「Imports」欄位中。可以在載入命名空間時指定版本需求並進行檢查。
「Suggests」欄位使用與「Depends」相同的語法,並列出不一定要的套件。這包括僅在範例、測試或小插圖中使用的套件(請見 撰寫套件小插圖),以及在函式主體中載入的套件。例如,假設套件 foo 中的範例12 使用套件 bar 中的資料集。那麼,除非要執行所有範例/測試/小插圖,否則 bar 不需要使用 foo:擁有 bar 有用,但不是必要的。可以指定版本需求,但應由使用套件的程式碼進行檢查。
最後,「Enhances」欄位會列出由目前套件「增強」的套件,例如,提供這些套件中類別的方法,或處理這些套件中物件的方式(因此,許多套件會有「Enhances: chron」,因為即使它們偏好 R 的原生日期時間函式,它們也能處理 chron 中的日期時間物件)。可以指定版本需求,但目前並未使用。此類套件無法被要求檢查套件:任何使用它們的測試都必須以套件的存在為條件。(如果您的測試使用例如來自其他套件的資料集,它應該在「Suggests」中,而不是「Enhances」中。)
一般規則如下:
library(pkgname)
載入套件的套件,應列在「Imports」欄位中,而不是「Depends」欄位中。在 NAMESPACE 檔案中 import
或 importFrom
指令中列出的套件,幾乎都應該在「Imports」中,而不是「Depends」中。
library(pkgname)
成功載入套件的套件,必須列在「Depends」欄位中。
R CMD check
13。有條件執行範例或測試的套件(例如 透過 if(require(pkgname))
)應列在「Suggests」或「Enhances」中。(這讓檢查程式可以確保已安裝完整檢查所需的所有套件。)
特別是,提供範例或小品文「僅」資料的套件應列在「Suggests」中,而不是「Depends」,以利精簡安裝。
當 library
載入套件時,「Depends」和「Imports」欄位中的版本相依性會被使用,而 install.packages
會檢查「Depends」、「Imports」和(對於 dependencies = TRUE
)「Suggests」欄位的版本。
這些欄位中的資訊必須完整且正確非常重要:例如,它用於計算哪些套件相依於已更新的套件,以及哪些套件可以安全地並行安裝。
此架構是在所有套件都有命名空間之前(2011 年 10 月的 R 2.14.0)開發的,而一旦命名空間就位,良好的做法就改變了。
欄位「Depends」現今應少用,僅用於打算放置在搜尋路徑中,以提供其功能給最終使用者(而非套件本身)的套件:例如,套件 latticeExtra 的使用者會希望套件 lattice 的函數可用。
「Depends」中提到的套件幾乎都應該從 NAMESPACE 檔案中匯入:這可確保當其他套件匯入目前的套件時,這些套件的任何必要部分都可用。
「Imports」欄位不應包含未從 (經由 NAMESPACE 檔案或 ::
或 :::
算子) 匯入的套件,因為該欄位中列出的所有套件都必須安裝,目前的套件才能安裝。(這會由 R CMD check
檢查。)
套件中的 R 程式碼應僅在例外情況下呼叫 library
或 require
。對於列在「Depends」的套件,此類呼叫從不需要,因為它們已經在搜尋路徑中。過去常在使用其功能的函數中,對列在「Suggests」的套件使用 require
呼叫,但現今最好 經由 ::
呼叫存取此類功能。
希望使用其他套件中的標頭檔案來編譯其 C/C++ 程式碼的套件,需要在 DESCRIPTION 檔案的「LinkingTo」欄位中,將它們宣告為逗號分隔的清單。例如
LinkingTo: link1, link2
「LinkingTo」欄位可以有版本需求,這會在安裝時檢查。
如果「LinkingTo」中的套件是包含原始碼的 C/C++ 標頭,或在安裝時進行靜態連結,則指定「LinkingTo」中的套件就足夠了:這些套件不需要(而且通常不應該)列在「Depends」或「Imports」欄位中。這包括 CRAN 套件 BH,以及幾乎所有 RcppArmadillo 和 RcppEigen 的使用者。請注意,「LinkingTo」僅適用於安裝:如果套件希望使用標頭來編譯測試或範例中的程式碼,則提供這些標頭的套件需要列在「Suggests」或可能是「Depends」中。
有關「LinkingTo」的另一種用法,請參閱 連結到其他套件中的原生常式。
「Additional_repositories」欄位是一個逗號分隔的儲存庫 URL 清單,可以在其中找到其他欄位中命名的套件。目前 R CMD check
使用它來檢查是否可以找到這些套件,至少作為原始碼套件(可以在任何平台上安裝)。
請注意,想要執行範例/測試/小節的人可能沒有建議的套件(甚至可能無法為該平台安裝它)。建議的做法是透過 if(require("pkgname"))
讓它們的使用有條件:如果在範例/測試/小節中完成該條件,這沒問題,儘管如果可能,建議使用 if(requireNamespace("pkgname"))
。
但是,在套件程式碼中使用 require
進行條件設定並非良好的做法,因為它會改變該階段的搜尋路徑,並依賴於該套件中的函式不會被其他 require
或 library
呼叫所遮罩。建議使用類似以下的程式碼
if (requireNamespace("rgl", quietly = TRUE)) { rgl::plot3d(...) } else { ## do something else not involving rgl. }
請注意使用 rgl::
,因為該物件不一定可見(如果可見,它不必是來自該名稱空間:plot3d
出現在其他幾個套件中)。如果在建議的套件不可用時要給出錯誤,只需使用例如 rgl::plot3d
。
如果條件式程式碼產生 print
輸出,函式 withAutoprint
可能有用。
請注意,在測試中使用建議套件的建議也適用於用於管理測試套件的套件:一個惡名昭彰的範例是 testthat,在版本 1.0.0 中包含非法的 C++ 程式碼,因此無法在符合標準的平台上安裝。
有些人假設在「建議」中的「推薦」套件可以安全地無條件使用,但事實並非如此。(可以在沒有推薦套件的情況下安裝 R,而哪些套件是「推薦」的可能會改變。)
如上所述,在「加強」中的套件必須有條件地使用,因此其中的物件應始終透過 ::
存取。
在大部分系統中,R CMD check
僅能使用在「Depends」和「Imports」中宣告的套件執行,方法是設定環境變數 _R_CHECK_DEPENDS_ONLY_=true
,而設定 _R_CHECK_SUGGESTS_ONLY_=true
則允許建議的套件,但不允許「Enhances」中的套件,或未在 DESCRIPTION 檔案中提及的套件。建議使用這兩個設定檢查套件,以及不使用這兩個設定檢查套件。
警告:如果您執行會在安裝時執行的動作,而這些動作會根據建議的套件是否可用而有所不同,請務必小心執行,這包括 R 程式碼檔案中的頂層程式碼、.onLoad
函式,以及 S4 類別和方法的定義。問題在於,一旦載入建議套件的命名空間,對它的參照可能會擷取到已安裝的套件中(最常見於 S4 方法),但建議的套件在使用已安裝的套件時可能不可用(特別是對於二進位套件,可能在不同的機器上)。更糟的是,問題可能不限於您的套件,因為建議套件的命名空間也會在安裝任何匯入您套件的套件時載入,因此可能會擷取到該處。
選用的檔案 INDEX 包含套件中每個足夠有趣的物件的一行,提供其名稱和說明(通常不會明確呼叫的函式,例如列印方法,可能不會包含在內)。通常這個檔案不存在,而對應的資訊會在從原始碼安裝時自動從文件來源產生(使用 tools::Rdindex()
)。
這個檔案是 library(help = pkgname)
提供的資訊的一部分。
與其編輯此檔案,建議將套件的客製化資訊放入概觀說明頁面(請參閱 套件文件)和/或小插圖(請參閱 撰寫套件小插圖)。
R 子目錄僅包含 R 程式碼檔案。要安裝的程式碼檔案必須以 ASCII(小寫或大寫)字母或數字開頭,並具有下列其中一個副檔名14 .R、.S、.q、.r 或 .s。我們建議使用 .R,因為這個副檔名似乎不會被任何其他軟體使用。應該可以使用 source()
讀取檔案,因此 R 物件必須透過指定來建立。請注意,檔案名稱和它建立的 R 物件之間不需要有任何關聯。理想情況下,R 程式碼檔案應僅直接指定 R 物件,且絕對不應呼叫具有副作用的函數,例如 require
和 options
。如果需要運算來建立物件,這些運算可以使用套件中「較早」的程式碼(請參閱「Collate」欄位)加上「Depends」套件中的函數,前提是建立的物件不依賴這些套件,但透過命名空間匯入除外。
如果頂層運算依賴於其他套件的可用性,則需要特別小心。這特別適用於 setMethods
和 setClass
呼叫。它們也不應該依賴於外部資源的可用性,例如下載。
允許兩個例外:如果 R 子目錄包含檔案 sysdata.rda(一個或多個 R 物件的儲存影像:請使用 tools::resaveRdaFiles
建議的適當壓縮,並參閱「SysDataCompression」DESCRIPTION 欄位。)這將延遲載入到名稱空間環境中 - 這是針對系統資料集,不打算透過 data
讓使用者存取 via。此外,以「.in」結尾的檔案將允許在 R 目錄中,以允許 configure 指令碼產生合適的檔案。
在程式碼檔案中,只應使用 ASCII 字元(和控制字元 tab、換頁、LF 和 CR)。其他字元會在註解中接受15,但註解可能無法在例如 UTF-8 區域設定中讀取。當套件安裝時,物件名稱中的非 ASCII 字元通常會16失敗。任何位元組都可以在引號的字串中允許,但應使用「\uxxxx」跳脫字元來處理非 ASCII 字元。然而,非 ASCII 字串可能無法在某些區域設定中使用,並可能在其他區域設定中顯示不正確。
封裝中可使用各種 R 函數來初始化和清理。請參閱 載入掛鉤。
man 子目錄應(僅)包含封裝中物件的說明文件,格式為 R 說明文件 (Rd)。說明文件檔名必須以 ASCII(小寫或大寫)字母或數字開頭,並使用擴充功能 .Rd(預設)或 .rd。此外,名稱必須在「file://」URL 中有效,表示17它們必須完全是 ASCII,且不包含「%」。請參閱 撰寫 R 說明文件,以取得更多資訊。請注意,封裝中的所有使用者層級物件都應有說明文件;如果封裝 pkg 包含僅供「內部」使用的使用者層級物件,則應提供文件 pkg-internal.Rd,說明所有此類物件,並清楚說明這些物件不應由使用者呼叫。例如,請參閱 R 發行版中封裝 grid 的來源。請注意,廣泛使用內部物件的封裝不應從其命名空間匯出這些物件,如果不需要說明文件(請參閱 封裝命名空間)。
如果 man 目錄不包含說明文件,可能會導致安裝錯誤。
man 子目錄可能包含名為 macros 的子目錄;其中會包含使用者定義 Rd 巨集的來源。(請參閱 使用者定義巨集。)這些巨集使用 Rd 格式,但可能僅包含巨集定義、註解和空白。
子目錄 R 和 man 可能包含名為 unix 或 windows 的特定作業系統子目錄。
編譯程式碼的原始碼和標頭檔位於 src 中,另外還有檔案 Makevars 或 Makefile(或在 Windows 上使用,副檔名為 .win 或 .ucrt)。當使用 R CMD INSTALL
安裝套件時,make
用於控制編譯和連結,以載入 R 中的共用物件。有預設的 make
變數和規則(在 R 設定並記錄在 R_HOME/etcR_ARCH/Makeconf 中時決定),提供對 C、C++、固定或自由格式 Fortran、Objective C 和 Objective C++18 的支援,相關副檔名分別為 .c、.cc 或 .cpp、.f、.f90 或 .f95、19 .m 和 .mm。我們建議對標頭檔使用 .h,也可用於 C++20 或 Fortran 包含檔。(不再支援對 C++ 使用副檔名 .C。)src 目錄中的檔案不應隱藏(以點號開頭),某些 R 版本會忽略隱藏的檔案。
在單一套件中混合所有這些語言並非可攜式的(甚至可能完全不可行)。由於 R 本身使用它,我們知道 C 和固定格式 Fortran 可以一起使用,而混合 C、C++ 和 Fortran 通常適用於平台的原生編譯器。
如果您的程式碼需要依賴平台,則 C 或 C++ 中可以使用某些定義。在所有 Windows 組建(甚至 64 位元組組建)中,將定義「_WIN32」:在 64 位元組 Windows 組建中,也會定義「_WIN64」。在 macOS 中,定義「__APPLE__」21;對於「Apple Silicon」平台,請同時測試「__APPLE__」和「__arm64__」。
可以在檔案 src/Makevars 中設定巨集22 來調整預設規則(請參閱 使用 Makevars)。請注意,此機制應足夠通用,以消除對特定於套件的 src/Makefile 的需求。如果要發行此類檔案,則需要非常小心,使其足夠通用,才能在所有 R 平台上執行。如果它有任何目標,它應具有適當的第一個目標,名為「all」,以及一個(可能是空的)目標「clean」,它會移除執行 make
所產生的所有檔案(供「R CMD INSTALL --clean」和「R CMD INSTALL --preclean」使用)。Windows 上有特定於平台的檔案名稱:src/Makevars.win 優先於 src/Makevars,並且必須使用 src/Makefile.win。自 R 4.2.0 起,src/Makevars.ucrt 優先於 src/Makevars.win,src/Makefile.ucrt 優先於 src/Makefile.win。src/Makevars.ucrt 和 src/Makefile.ucrt 將被早期版本的 R 忽略,因此可用於提供特定於 UCRT 或 Rtools42 及更新版本的內容,但 .ucrt 檔案的支援可能會在未來移除,屆時將不再需要從舊版 R 的原始碼組建套件,因此檔案可能會重新命名回 .win。有些 make
程式需要 makefile 具有完整的最後一行,包括換行符。
一些套件使用 src 目錄來進行建立共用物件以外的目的(例如建立可執行檔)。此類套件應具有 src/Makefile 和 src/Makefile.win 或 src/Makefile.ucrt 檔案(除非僅供類 Unix 系統或僅供 Windows 使用)。請注意,在 Unix 系統上,此類 makefile 會在 R_HOME/etc/R_ARCH/Makeconf 之後包含,因此所有常見的 R 巨集和 make 規則都可用,例如 C 編譯預設會使用 R 設定的 C 編譯器和旗標。從 R 4.3.0 開始,這也適用於 Windows:打算與較早版本一起使用的套件應自行包含該檔案。
對於 沒有 src/Makefile 檔案的套件,makefile 的包含順序為
類 Unix 系統 | Windows |
---|---|
src/Makevars | src/Makevars.ucrt、src/Makevars.win |
R_HOME/etc/R_ARCH/Makeconf | R_HOME/etc/R_ARCH/Makeconf |
R_MAKEVARS_SITE 、R_HOME/etc/R_ARCH/Makevars.site | R_MAKEVARS_SITE 、R_HOME/etc/R_ARCH/Makevars.site |
R_HOME/share/make/shlib.mk | R_HOME/share/make/winshlib.mk |
R_MAKEVARS_USER 、 ~/.R/Makevars-platform、 ~/.R/Makevars | R_MAKEVARS_USER 、 ~/.R/Makevars.ucrt、 ~/.R/Makevars.win64、 ~/.R/Makevars.win |
對於有 src/Makefile 檔案的套件,包含順序為
R_HOME/etc/R_ARCH/Makeconf | R_HOME/etc/R_ARCH/Makeconf |
R_MAKEVARS_SITE 、R_HOME/etc/R_ARCH/Makevars.site | R_MAKEVARS_SITE 、R_HOME/etc/R_ARCH/Makevars.site |
src/Makefile | src/Makefile.ucrt、src/Makefile.win |
R_MAKEVARS_USER 、 ~/.R/Makevars-platform、 ~/.R/Makevars | R_MAKEVARS_USER 、 ~/.R/Makevars.ucrt、 ~/.R/Makevars.win64、 ~/.R/Makevars.win |
大寫項目為環境變數:以逗號分隔的項目為以所示順序尋找的替代方案。
在非常特殊的情況下,套件可能會在 src 目錄中建立共用物件/DLL 以外的二進位檔。這些檔案不會安裝在多架構設定中,因為 R CMD INSTALL --libs-only
用於合併多個子架構,而且它只會複製共用物件/DLL。如果套件想要安裝其他二進位檔(例如可執行程式),它應該提供 R 腳本 src/install.libs.R,這會在 src
建置目錄中作為安裝的一部分執行,而不是複製共用物件/DLL。腳本會在包含下列變數的個別 R 環境中執行:R_PACKAGE_NAME
(套件名稱)、R_PACKAGE_SOURCE
(套件來源目錄路徑)、R_PACKAGE_DIR
(套件目標安裝目錄路徑)、R_ARCH
(路徑的架構相依部分,通常為空)、SHLIB_EXT
(共用物件的副檔名)和 WINDOWS
(在 Windows 上為 TRUE
,在其他地方為 FALSE
)。使用下列 src/install.libs.R 檔案可以複製近似於預設行為的行為
files <- Sys.glob(paste0("*", SHLIB_EXT)) dest <- file.path(R_PACKAGE_DIR, paste0('libs', R_ARCH)) dir.create(dest, recursive = TRUE, showWarnings = FALSE) file.copy(files, dest, overwrite = TRUE) if(file.exists("symbols.rds")) file.copy("symbols.rds", dest, overwrite = TRUE)
另一方面,可執行程式可以沿著下列方式安裝
execs <- c("one", "two", "three") if(WINDOWS) execs <- paste0(execs, ".exe") if ( any(file.exists(execs)) ) { dest <- file.path(R_PACKAGE_DIR, paste0('bin', R_ARCH)) dir.create(dest, recursive = TRUE, showWarnings = FALSE) file.copy(execs, dest, overwrite = TRUE) }
注意在需要時使用 bin 的特定於架構的子目錄。(可執行檔應該安裝在 bin 目錄下,而不是 libs 下。建議檢查它們是否可以作為安裝腳本的一部分執行,這樣就不會安裝損壞的套件。)
data 子目錄是資料檔:請參閱 套件中的資料。
demo 子目錄是 R 範例腳本(用於透過 demo()
執行),用來示範套件的一些功能。範例可能是互動式的,而且不會自動檢查,所以如果要進行測試,請使用 tests 目錄中的程式碼來達成此目的。範例腳本檔必須以(大小寫)字母開頭,並具有 .R 或 .r 的其中一個副檔名。如果存在,demo 子目錄也應該有一個 00Index 檔,針對每個範例有一行,提供其名稱和說明,並以 tab 或至少三個空格分隔。(此索引檔不會自動產生。)請注意,範例沒有指定編碼,因此應該是 ASCII 檔(請參閱 編碼問題)。如果有一個套件編碼,函式 demo()
會使用該編碼,但這主要適用於非 ASCII 註解。
將 inst 子目錄的內容遞迴複製到安裝目錄中。 inst 的子目錄不應與 R 使用的目錄衝突(目前為 R、data、demo、exec、libs、man、help、html 和 Meta,而較早的版本使用 latex、R-ex)。inst 的複製發生在 src 建立之後,因此其 Makefile 可以建立要安裝的檔案。若要排除檔案不予安裝,可以在頂層原始碼目錄中的 .Rinstignore 檔案中指定排除模式清單。這些模式應為 Perl 式正規表示式(有關精確的詳細資訊,請參閱 R 中 regexp
的說明),每一行一個,不區分大小寫地與檔案和目錄路徑進行比對,例如 doc/.*[.]png$ 會根據副檔名排除 inst/doc 中的所有 PNG 檔案。
請注意,除了 INDEX、LICENSE/LICENCE 和 NEWS 之外,封裝頂層的資訊檔案不會安裝,因此 Windows 和 macOS 編譯封裝的使用者不會知道這些檔案(以及那些在 tarball 上使用 R CMD INSTALL
或 install.packages()
的使用者也看不到這些檔案)。因此,您希望最終使用者看到的任何資訊檔案都應包含在 inst 中。請注意,如果這些已命名的例外也出現在 inst 中,則 inst 中的版本將會出現在已安裝的封裝中。
您可能想要新增到 inst 的項目包括供 citation
函數使用的 CITATION 檔案,以及供 news
函數使用的 NEWS.Rd 檔案。請參閱其說明頁面,以取得 NEWS.Rd 檔案的特定格式限制。
在 inst 中有時需要的另一個檔案是 AUTHORS 或 COPYRIGHTS,用於指定作者或版權持有者,當這部分過於複雜而無法放入 DESCRIPTION 檔案時。
子目錄 tests 用於其他特定於套件的測試程式碼,類似於 R 發行版附帶的特定測試。測試程式碼可以透過 .R (或 .r,自 R 3.4.0 起) 檔案直接提供,或透過包含程式碼的 .Rin 檔案提供,該程式碼會進一步建立對應的 .R 檔案 (例如,透過收集套件中的所有函數物件,然後使用最奇怪的引數呼叫它們)。執行 .R 檔案的結果會寫入 .Rout 檔案。如果有一個對應的23 .Rout.save 檔案,這兩個檔案會進行比較,並報告差異,但不會造成錯誤。目錄 tests 會複製到檢查區域,並以複製版本作為工作目錄,以及設定 R_LIBS
來執行測試,以確保在測試期間安裝的套件複製版本會被 library(pkg_name)
找到。請注意,特定於套件的測試會在沒有設定亂數種子的基本 R 會話中執行,因此使用亂數的測試需要設定種子才能取得可重製的結果 (而且在所有情況下這麼做都有幫助,以避免在執行測試時偶爾發生失敗)。
如果目錄 tests 有子目錄 Examples 包含檔案 pkg-Ex.Rout.save
,這會與執行範例的輸出檔案進行比較,後者在檢查時會被檢查。參考輸出應在未設定 --timings 選項的情況下產生(並注意 --as-cran 會設定它)。
如果範例、測試或小插圖包含參考輸出,請務必確保它完全可重製,因為它會與檢查執行中產生的輸出逐字比較,除非使用「IGNORE_RDIFF」標記。讓維護人員絆倒的事情包括從載入其他套件顯示的版本號碼、將數值結果列印到無法重製的高精度,以及列印計時。另一個陷阱是實際上從零捨入錯誤的小值:考慮使用 zapsmall
。
子目錄 exec 可能包含套件需要的其他可執行腳本,通常是殼層、Perl 或 Tcl 等直譯器的腳本。注意:只有 exec 下的檔案(而不是目錄)會被安裝(而名稱以點開頭的檔案會被忽略),而且它們在 POSIX 平台上都被標記為可執行(模式 755
,由「umask」調整)。另請注意,這不適用於可執行 程式,因為某些平台(包括 Windows)使用相同的已安裝套件目錄支援多種架構。
子目錄 po 用於與 在地化 相關的檔案:請參閱 國際化。
子目錄 tools 是組態期間所需輔助檔案的首選位置,也是重新建立腳本所需的來源(例如 autoconf
的 M4 檔案:有些人喜歡將它們放在 tools 的 m4 子目錄中)。
data 子目錄用於資料檔案,可透過延遲載入取得,或使用 data()
載入。(選擇方式為 DESCRIPTION 檔案中的「LazyData」欄位:預設為不這麼做。)它不應使用於套件所需的其它資料檔案,而慣例是使用 inst/extdata 目錄來存放這些檔案。
資料檔案可以有下列三種類型,由其副檔名表示:純 R 程式碼 (.R 或 .r)、表格 (.tab、.txt 或 .csv,檔案格式請參閱 ?data
,請注意 .csv 並非 標準24 CSV 格式) 或 save()
影像 (.RData 或 .rda)。這些檔案不應隱藏(名稱以點號開頭)。請注意,R 程式碼應盡可能「自給自足」,且不使用套件提供的額外功能,以便資料檔案也可以在未載入套件或其命名空間的情況下使用:它應盡可能靜默執行,且不透過附加套件或其它環境來變更 search()
路徑。
影像(副檔名 .RData25 或 .rda)可以包含用於建立它們的套件命名空間的參考。資料檔案中最好不要有這些參考,而且在任何情況下,它們只能是 Depends
和 Imports
欄位中所列的套件,否則可能無法安裝套件。若要檢查這些參考,請將所有影像載入到原始 R 會話中,對所有資料集執行 str()
,並查看 loadedNamespaces()
的輸出。
當資料集或其元件之一為 S4 類別時,需要特別注意,特別是如果類別是在不同的套件中定義的。首先,包含類別定義的套件必須可用才能對資料集執行有用的操作,因此該套件必須列在 Imports
或 Depends
中(即使這會產生關於未使用的匯入的檢查警告)。其次,S4 類別的定義可能會改變,而且通常在具有不同作者的套件中不會被注意到。因此,使用 .R 形式並在需要時使用它來建立資料集物件可能會比較明智(載入套件名稱空間,但不要透過使用 requireNamespace(pkg, quietly = TRUE)
和使用 pkg::
來參照名稱空間中的物件)。
如果您沒有使用「LazyData」,而且您的資料檔案很大,或者您使用 data/foo.R 腳本來產生資料,載入您的名稱空間,您可以透過在 data 子目錄中提供 datalist 檔案來加速安裝。這應該針對 data()
會找到的主題,每行一個,格式為「foo」,如果 data(foo)
提供「foo」,或者「foo: bar bah」,如果 data(foo)
提供「bar」和「bah」。R CMD build
會自動將 datalist 檔案新增到超過 1Mb 的 data 目錄,使用函式 tools::add_datalist
。
表格(.tab、.txt 或 .csv 檔案)可以用 gzip
、bzip2
或 xz
壓縮,也可以選擇使用額外的副檔名 .gz、.bz2 或 .xz。
如果您的套件要進行散布,請考慮大型資料集對您的使用者所造成的資源影響:它們會讓套件下載非常緩慢,並使用大量儲存空間,而且載入時會花費數秒。一般來說,最好將大型資料集散布為 .rda 映像,由 save(, compress = TRUE)
(預設值)準備。使用 bzip2
或 xz
壓縮通常會縮小套件 tarball 和已安裝套件的大小,在某些情況下會縮小兩倍以上。
套件 tools 有幾個函式可以協助處理資料映像:checkRdaFiles
會報告映像儲存的方式,而 resaveRdaFiles
會使用不同的壓縮類型重新儲存,包括為特定映像選擇最佳類型。
許多使用「LazyData」的套件會受益於在已安裝的延遲載入資料庫中使用 gzip
以外的壓縮形式。這可以使用 R CMD INSTALL
的 --data-compress 選項或使用 DESCRIPTION 檔案中的「LazyDataCompression」欄位來選擇。有用的值包括 bzip2
、xz
和預設值 gzip
:值 none
也被接受。找出哪個值最佳的方法是全部嘗試看看,並查看 pkgname/data/Rdata.rdb 檔案的大小。一個可以做到這件事的函式(以 KB 為單位引述大小)是
CheckLazyDataCompression <- function(pkg) { pkg_name <- sub("_.*", "", pkg) lib <- tempfile(); dir.create(lib) zs <- c("gzip", "bzip2", "xz") res <- integer(3); names(res) <- zs for (z in zs) { opts <- c(paste0("--data-compress=", z), "--no-libs", "--no-help", "--no-demo", "--no-exec", "--no-test-load") install.packages(pkg, lib, INSTALL_opts = opts, repos = NULL, quiet = TRUE) res[z] <- file.size(file.path(lib, pkg_name, "data", "Rdata.rdb")) } ceiling(res/1024) }
(應用於沒有任何「LazyDataCompression」欄位的原始套件)。如果 R CMD check
發現一個超過 5MB 的 pkgname/data/Rdata.rdb 檔案,且未設定「LazyDataCompression」,就會發出警告。如果看到此警告,請執行 CheckLazyDataCompression()
並設定欄位,在不太可能的情況下26,設定為 gzip
是最佳選擇。
sysdata.rda 的類比是「SysDataCompression」欄位:預設值為檔案大於 1MB 時為 xz
,否則為 gzip
。
對於非常大的資料集(序列化時超過 2GB,也就是 32 位元平台格式的限制),不支援延遲載入。
需要編譯的程式碼 (C、C++、Fortran …) 包含在 src 子目錄中,並在本文檔的其他地方討論。
子目錄 exec 可用於殼層、BUGS、JavaScript、Matlab、Perl、php (amap)、Python 或 Tcl (Simile) 或甚至是 R 的腳本。不過,使用 inst 目錄似乎更為常見,例如 WriteXLS/inst/Perl、NMF/inst/m-files、RnavGraph/inst/tcl、RProtoBuf/inst/python 以及 emdbook/inst/BUGS 和 gridSVG/inst/js。
Java 程式碼是一個特殊案例:除了非常小的程式外,.java 檔案應編譯成位元組碼(編譯成 .class 檔案),並作為 .jar 檔案的一部分進行發布:.jar 檔案的慣例位置是 inst/java。最好(且在開放原始碼授權下是必要的)讓 Java 原始檔可用:這最適合在套件中的頂層 java 目錄中進行,不應安裝原始檔。
如果您的套件需要這些解釋器或延伸模組之一,則應在 DESCRIPTION 檔案的「SystemRequirements」欄位中宣告這一點。(Java 使用者最常透過 rJava 進行,當依賴/匯入時,除非套件中的 Java 程式碼有版本需求,否則這就足夠了。)
Windows 和 Mac 使用者應注意 Tcl 擴充套件「BWidget」和「Tktable」(有時會包含在 Windows27 和 macOS R 安裝程式中)是擴充套件,需要宣告(而且「Tktable」的取得管道比以前少,包括不在主要 Linux 散佈版的存放庫中)。「BWidget」需要使用者在其他作業系統中安裝。這很容易:首先找到 Tcl 搜尋路徑
library(tcltk) strsplit(tclvalue('auto_path'), " ")[[1]]
然後從 https://sourceforge.net/projects/tcllib/files/BWidget/ 下載原始程式碼,並在終端機執行類似下列指令
tar xf bwidget-1.9.14.tar.gz sudo mv bwidget-1.9.14 /usr/local/lib
如果需要,請將 Tcl 搜尋路徑中的位置替換為 /usr/local/lib。(如果搜尋路徑中沒有可寫入的位置,您每次使用 BWidget 搭配 tcltk::addTclPath()
時,都需要新增一個。)
要(無聲地)測試「Tktable」是否存在,可以使用
library(tcltk) have_tktable <- !isFALSE(suppressWarnings(tclRequire('Tktable')))
安裝「Tktable」需要 C 編譯器和 Tk 標頭檔(不一定會與 Tcl/Tk 一起安裝)。在撰寫本文時,最新的原始程式碼(2008 年)可從 https://sourceforge.net/projects/tktable/files/tktable/2.10/Tktable2.10.tar.gz/download 取得,但需要為目前的 Tk(8.6.11,但不是 8.6.10)進行修補,可以在 https://www.stats.ox.ac.uk/pub/bdr/Tktable/ 找到修補程式。對於 Tk 的系統安裝,您可能需要以「root」安裝 Tktable,因為在例如 Fedora 中,auto_path
上的所有位置都屬於「root」所有。
套件文件中的許多位置的 URL 會在至少部分的呈現中轉換為可按一下的超連結。因此,需要小心它們的形式是否正確且可攜式。
應提供完整的 URL,包括 scheme(通常為「http://」或「https://」)和目錄參考的最後一個「/」。
URL 中的空格不可攜式,其處理方式會因 HTTP 伺服器和用戶端而異。在「http://」URL 的主機部分不應有空格,且其餘部分的空格應編碼,每個空格都替換為「%20」。
其他字元可能受益於編碼:請參閱 URLencode()
的說明。
CRAN 套件的標準 URL 為
https://r-cran.dev.org.tw/package=pkgname
而不是從「https://r-cran.dev.org.tw/web/packages/pkgname」開頭的版本。
請注意,本節的大部分內容特定於類 Unix 系統:稍後會針對 R 的 Windows 移植發表評論。
如果您的套件在安裝前需要一些系統相依的組態,您可以在套件中包含一個可執行檔 (Bourne28 shell script configure,如果存在的話,會在執行任何其他動作之前由 R CMD INSTALL
執行。這可以是 Autoconf 機制建立的指令碼,但也可以是您自己撰寫的指令碼。使用這個來偵測是否有任何非標準函式庫,以便可以在安裝時停用套件中的對應程式碼,而不是在編譯或使用套件時傳出錯誤訊息。總之,您的延伸套件可以使用 Autoconf 的所有功能 (包括變數替換、搜尋函式庫等)。有關 Autoconf 和相關工具 (包括以下所述的 pkg-config
) 的背景和實用秘訣,請參閱 https://autotools.info/。
configure
指令碼是在環境中執行的,該環境已設定 R 會話的所有環境變數 (請參閱 R_HOME/etc/Renviron),加上 R_PACKAGE_NAME
(套件名稱)、R_PACKAGE_DIR
(套件目標安裝目錄的路徑,暫存安裝的暫時位置) 和 R_ARCH
(路徑中與架構相關的部分,通常為空)。
僅在類 Unix 系統中,如果給定選項 --clean,cleanup
可執行檔 (Bourne shell) 指令碼會由 R CMD INSTALL
在最後執行,而 R CMD build
會在從來源建置套件時執行。
舉例來說,假設我們想要使用(C 或 Fortran)函式庫 foo
所提供的功能。使用 Autoconf,我們可以建立一個 configure 腳本,用來檢查函式庫,如果找到就將變數 HAVE_FOO
設為 TRUE
,否則設為 FALSE
,然後將這個值代換到輸出檔案中(以 HAVE_FOO
的值取代輸入檔案中 ‘@HAVE_FOO@’ 的實例)。例如,如果一個名為 bar
的函式要透過連結函式庫 foo
(也就是使用 -lfoo)來提供,我們可以使用
AC_CHECK_LIB(foo, fun, [HAVE_FOO=TRUE], [HAVE_FOO=FALSE]) AC_SUBST(HAVE_FOO) ...... AC_CONFIG_FILES([foo.R]) AC_OUTPUT
在 configure.ac 中(假設 Autoconf 2.50 或更新版本)。
在 foo.R.in 中對應 R 函式的定義可以是
foo <- function(x) { if(!@HAVE_FOO@) stop("Sorry, library ‘foo’ is not available") ...
從這個檔案,configure
會建立實際的 R 原始碼檔案 foo.R,看起來像
foo <- function(x) { if(!FALSE) stop("Sorry, library ‘foo’ is not available") ...
如果函式庫 foo
沒有找到(具有所需的函式)。在這種情況下,上述 R 程式碼會有效地停用這個函式。
我們也可以分別針對可用的函式和遺失的函式使用不同的檔案片段。
你很可能會需要確保在 configure 測試中使用與編譯 R 或套件時相同的 C 編譯器和編譯器旗標。在類 Unix 系統中,你可以透過在 configure.ac 的前面包含以下片段來達成(在呼叫 AC_PROG_CC
或任何呼叫它的東西之前)
: ${R_HOME=`R RHOME`} if test -z "${R_HOME}"; then echo "could not determine R_HOME" exit 1 fi CC=`"${R_HOME}/bin/R" CMD config CC` CFLAGS=`"${R_HOME}/bin/R" CMD config CFLAGS` CPPFLAGS=`"${R_HOME}/bin/R" CMD config CPPFLAGS`
(使用 ‘${R_HOME}/bin/R’ 而不是 ‘R’ 是必要的,才能在將腳本作為 R CMD INSTALL
的一部分執行時使用正確版本的 R,而引號是因為 ‘${R_HOME}’ 可能包含空白。)
如果您的程式碼載入檢查(例如,檢查函式庫中的進入點或執行程式碼),則您還需要
LDFLAGS=`"${R_HOME}/bin/R" CMD config LDFLAGS`
以 C++ 編寫的套件需要擷取 C++ 編譯器的詳細資訊,並透過類似的程式碼將目前的語言切換為 C++
CXX=`"${R_HOME}/bin/R" CMD config CXX` if test -z "$CXX"; then AC_MSG_ERROR([No C++ compiler is available]) fi CXXFLAGS=`"${R_HOME}/bin/R" CMD config CXXFLAGS` CPPFLAGS=`"${R_HOME}/bin/R" CMD config CPPFLAGS` AC_LANG(C++)
後者很重要,例如 C 標頭可能無法提供給 C++ 程式,或可能未撰寫為避免 C++ 名稱混淆。請注意,R 安裝不需要有 C++ 編譯器,因此 'CXX' 可能為空。如果套件指定非預設的 C++ 標準,請使用與標準相符的 config
變數名稱(例如 CXX17
),但仍設定 CXX
和 CXXFLAGS
。
您可以使用 R CMD config
取得基本設定變數的值,以及連結前端可執行程式與 R 所需的標頭和函式庫旗標,請參閱 R CMD config --help 以取得詳細資訊。如果您這樣做,您必須同時使用指令和適當的旗標,例如 'CC' 必須始終與 'CFLAGS' 一起使用,而(對於要連結到共用函式庫的程式碼)'CPICFLAGS'。對於 Fortran,請務必使用 'FC FFLAGS FPICFLAGS' 來處理固定格式 Fortran,並使用 'FC FCFLAGS FPICFLAGS' 來處理自由格式 Fortran。
從 R 4.3.0 開始,變數
CC CFLAGS CXX CXXFLAGS CPPFLAGS LDFLAGS FC FCFLAGS
會在從 R CMD INSTALL
呼叫 configure
時設定在環境中(如果尚未設定),以防指令碼忘記如上所述設定它們。這包括使用所選的 C 標準(但不包括 C++ 標準,因為這是由 R CMD SHLIB
在稍後階段選取的)。
若要使用官方 Autoconf 巨集檔案29中的 AX_BLAS
巨集檢查外部 BLAS 函式庫,可以使用
FC=`"${R_HOME}/bin/R" CMD config FC` FCLAGS=`"${R_HOME}/bin/R" CMD config FFLAGS` AC_PROG_FC FLIBS=`"${R_HOME}/bin/R" CMD config FLIBS` AX_BLAS([], AC_MSG_ERROR([could not find your BLAS library], 1))
請注意,必須使用 R 所決定的 FLIBS
,以確保 Fortran 程式碼可在所有 R 平台上執行。
注意:若 configure
腳本會建立檔案,例如 src/Makevars,則需要一個 cleanup
腳本來移除這些檔案。否則,R CMD build
可能會傳送已建立的檔案。例如,套件 RODBC 有
#!/bin/sh rm -f config.* src/Makevars src/config.h
如這個範例所示,configure
通常會建立工作檔案,例如 config.log。如果您使用手動建立的腳本,而非 autoconf
建立的腳本,強烈建議您將其動作記錄到檔案 config.log。
如果您的 configure 腳本需要輔助檔案,建議您將它們傳送到 tools 目錄(如同 R 本身所做的那樣)。
您應牢記,組態指令碼不會用於 Windows 系統。如果您的套件要公開提供,請提供足夠的資訊,讓非類 Unix 平台上的使用者手動組態,或提供要在該平台上使用的 configure.win 指令碼(或 configure.ucrt)。(選擇性地,可以有 cleanup.win 指令碼(或 cleanup.ucrt)。兩者都應該是 shell 指令碼,由 ash
執行,它是 Bourne 風格 sh
的精簡版。從 R 4.2.0 開始,使用 bash
。執行 configure.win(或 configure.ucrt)時,環境變數 R_HOME
(使用「/」作為檔案分隔符號)、R_ARCH
和 R_ARCH_BIN
會設定。使用 R_ARCH
來判斷這是否為 64 位元建置(其值為「/x64」),並將 DLL 安裝到正確的位置(${R_HOME}/libs${R_ARCH})。使用 R_ARCH_BIN
來找出 bin 目錄下的正確位置,例如 ${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe。如果 configure.win 指令碼進行編譯(包括呼叫 R CMD SHLIB
),則上述大部分考量事項都適用。
由於 Windows 上的指令碼會執行為 sh ./configure.win
和類似的指令,因此任何「shebang」第一行(例如 #! /bin/bash
)都視為註解。
In some rare circumstances, the configuration and cleanup scripts need
to know the location into which the package is being installed. An
example of this is a package that uses C code and creates two shared
object/DLLs. Usually, the object that is dynamically loaded by R
is linked against the second, dependent, object. On some systems, we
can add the location of this dependent object to the object that is
dynamically loaded by R. This means that each user does not have to
set the value of the LD_LIBRARY_PATH
(or equivalent) environment
variable, but that the secondary object is automatically resolved.
Another example is when a package installs support files that are
required at run time, and their location is substituted into an R
data structure at installation time.
The names of the top-level library directory (i.e., specifiable
via the ‘-l’ argument) and the directory of the package
itself are made available to the installation scripts via the two
shell/environment variables R_LIBRARY_DIR
and R_PACKAGE_DIR
.
Additionally, the name of the package (e.g. ‘survival’ or
‘MASS’) being installed is available from the environment variable
R_PACKAGE_NAME
. (Currently the value of R_PACKAGE_DIR
is
always ${R_LIBRARY_DIR}/${R_PACKAGE_NAME}
, but this used not to
be the case when versioned installs were allowed. Its main use is in
configure.win (or configure.ucrt) scripts for the installation path of external
software’s DLLs.) Note that the value of R_PACKAGE_DIR
may
contain spaces and other shell-unfriendly characters, and so should be
quoted in makefiles and configure scripts.
較棘手的任務之一可能是尋找外部軟體的標頭和函式庫。一個在類 Unix 上越來越普及的工具(但 macOS 上預設沒有30)是 pkg-config
。configure 指令碼需要測試指令本身是否存在31(例如套件 tiff),如果存在,可以詢問軟體是否已安裝、是否為合適的版本,以及編譯/連結旗標,例如:
$ pkg-config --exists ‘libtiff-4 >= 4.1.0’ --print-errors # check the status $ pkg-config --modversion libtiff-4 4.3.0 $ pkg-config --cflags libtiff-4 -I/usr/local/include $ pkg-config --libs libtiff-4 -L/usr/local/lib -ltiff $ pkg-config --static --libs libtiff-4 -L/usr/local/lib -ltiff -lwebp -llzma -ljpeg -lz
請注意,pkg-config --libs
提供連結到該函式庫預設版本32所需資訊(通常是動態版本),如果要使用靜態函式庫,可能需要 pkg-config --static --libs
。
靜態函式庫通常用於 macOS(和 Windows)以利將外部軟體與套件的二進位發行版綑綁在一起。這表示可攜式(原始碼)套件需要允許這麼做。使用 pkg-config --static --libs
並不安全,因為這通常會包含其他函式庫,而這些函式庫不一定安裝在使用者的系統上(或可能只安裝了版本化的函式庫,例如 libjbig.so.2.1,而不是 libjbig.so,而後者有時會包含在 pkg-config --static --libs libtiff-4
中,且為必要使用項目)。
另一個問題是 pkg-config --exists
可能不可靠。它不僅檢查「模組」是否可用,還會檢查所有相依性,包括原則上靜態連結所需的相依性。(XQuartz 2.8.x 只發行動態函式庫,而不發行 --exists
所需的某些 .pc 檔案。)
有時,軟體在 pkg-config
中已知的名稱並非預期中的名稱(例如,即使是 2.9.x,也為「libxml-2.0」)。若要取得完整清單,請使用
pkg-config --list-all | sort
有些外部軟體提供 -config 指令,以執行類似於 pkg-config
的工作,包括
curl-config freetype-config gdal-config geos-config gsl-config iodbc-config libpng-config nc-config pcre-config pcre2-config xml2-config xslt-config
(curl-config
是針對 libcurl
,而不是 curl
。 nc-config
是針對 netcdf
。)大多數都有使用靜態函式庫的選項。
注意:這些指令會指出需要哪些標頭路徑和函式庫,但並不會消除檢查它們提供的配方是否實際運作的必要性。(這對於使用靜態連結的平台來說尤其必要。)
如果使用 Autoconf,建議將所有 Autoconf 來源包含在套件中(開放原始碼套件的必要條件,並由 R CMD check --as-cran
測試)。這將包含套件頂層目錄中的檔案 configure.ac33。如果需要以 m4
編寫的擴充功能,應將這些擴充功能包含在目錄 tools 中,並從 configure.ac 中包含這些擴充功能,例如,
m4_include([tools/ax_pthread.m4])
或者,可以要求 Autoconf 搜尋目錄中的所有 .m4 檔案,方法是包含類似34
AC_CONFIG_MACRO_DIR([tools/m4])
此類擴充功能的來源之一是「Autoconf Archive」(https://gnu.dev.org.tw/software/autoconf-archive/)。假設使用者電腦已安裝此擴充功能並不安全,因此應隨套件附上此擴充功能(注意遵守其授權條款)。
有時,可以透過提供檔案 Makevars 來避免撰寫自己的 configure 程式碼:configure 程式碼最常見的用途之一也是從 Makevars.in 建立 Makevars。
一個 Makevars 檔案是一個 makefile,且由 R CMD SHLIB
(由 R CMD INSTALL
呼叫以編譯 src 目錄中的程式碼)當作多個 makefile 之一使用。它應該盡可能以可攜式的方式撰寫,特別是(除了 Makevars.win 和 Makevars.ucrt)不使用 GNU 擴充功能。
Makevars 檔案最常見的用法是設定 C/C++ 檔案的額外預處理器選項(例如包含路徑和定義),透過 PKG_CPPFLAGS
,以及透過設定 PKG_CFLAGS
、PKG_CXXFLAGS
或 PKG_FFLAGS
,分別針對 C、C++ 或 Fortran 設定額外的編譯器旗標(請參閱 建立共用物件)。
注意:包含路徑是預處理器選項,而不是編譯器選項,且必須在 PKG_CPPFLAGS
中設定,否則特定於平台的路徑(例如「-I/usr/local/include」)將優先。 PKG_CPPFLAGS
應包含「-I」、「-D」、「-U」,以及(在支援的情況下)「-include」和「-pthread」選項:其他所有內容都應該是編譯器旗標。旗標的順序很重要,且在 PKG_CFLAGS
或 PKG_CXXFLAGS
中使用「-I」已導致難以偵錯的特定於平台的錯誤。
Makevars 也可用於設定連結器的旗標,例如「-L」和「-l」選項,透過 PKG_LIBS
。
撰寫套件的 Makevars 檔案時,請務必確保它並非針對您的編譯器:例如 -O2 -Wall -pedantic(以及所有其他 -W 旗標:對於 Oracle 編譯器,這些旗標用於將參數傳遞給編譯器階段)等旗標都是針對 GCC(以及目標選項相容於它的編譯器,例如 clang
)。
此外,請勿設定變數,例如 CPPFLAGS
、CFLAGS
等:這些變數應允許使用者(網站)透過適當的個人(全站)Makevars 檔案設定。請參閱 R 安裝與管理 中的 自訂套件編譯。
有一些巨集35是在設定 R 本身的建置時設定的,並儲存在 R_HOME/etcR_ARCH/Makeconf 中。該 makefile 會在 Makevars[.win] 之後 以 Makefile 的形式包含,而且它定義的巨集可以在後者的巨集指定和 make 命令列中使用。這些巨集包括
FLIBS
¶一個包含連結 Fortran 程式碼所需的函式庫集合的巨集。這可能需要包含在 PKG_LIBS
中:如果套件在 src 目錄中包含 Fortran 原始檔,通常會自動包含它。
BLAS_LIBS
¶在建置 R 時使用的 BLAS 函式庫的巨集。這可能需要包含在 PKG_LIBS
中。請注意,如果它是空的,則 R 可執行檔將包含所有雙精度和雙複數 BLAS 常式,但沒有單精度或複數常式。如果包含 BLAS_LIBS
,則 FLIBS
也需要36包含在後面,因為大多數 BLAS 函式庫至少部分是用 Fortran 編寫的。不過,如果套件包含 Fortran 原始碼,則可以省略它,因為那會將 FLIBS
加入連結線中。
LAPACK_LIBS
¶建置 R 時使用的 LAPACK 函式庫(以及適當的路徑)的巨集。這可能需要包含在 PKG_LIBS
中。它可能會指向包含主要雙精度 LAPACK 常式以及建置 R 所需的雙複數 LAPACK 常式的動態函式庫 libRlapack
,或者它可能會指向外部 LAPACK 函式庫,或者如果外部 BLAS 函式庫也包含 LAPACK,則可能是空的。
[libRlapack
包含 2003 年現有的所有雙精度 LAPACK 常式和一些較新的常式:包含哪些常式的清單在檔案 src/modules/lapack/README 中。請注意,外部 LAPACK/BLAS 函式庫不必這麼做,因為有些常式在 2015 年底的 LAPACK 3.6.0 中已「不建議使用」(且預設不編譯)。]
為了可攜性,巨集 BLAS_LIBS
和 FLIBS
應始終包含在 之後 LAPACK_LIBS
(並按此順序)。
SAFE_FFLAGS
¶包含迴避過度最佳化 FORTRAN 程式碼所需的旗標的巨集:在使用 gfortran
的 ix86
平台上,它可能是 '-g -O2 -ffloat-store' 或 '-g -O2 -msse2 -mfpmath=sse'。請注意,這 不是 要用作 PKG_FFLAGS
一部分的附加旗標,而是 FFLAGS
的替換。請參閱本節後面的範例。
在 Makevars 中設定某些巨集將防止 R CMD SHLIB
設定它們:特別是如果 Makevars 設定 'OBJECTS',它將不會在 make
命令列上設定。這可以與隱式規則結合使用,以允許編譯其他類型的原始碼並包含在共用物件中。它也可以用來控制已編譯的檔案集,方法是排除 src 中的一些檔案或包含子目錄中的某些檔案。例如
OBJECTS = 4dfp/endianio.o 4dfp/Getifh.o R4dfp-object.o
請注意,Makevars 通常不應包含目標,因為它包含在預設 makefile 之前,而 make
將呼叫第一個目標,預計在預設 makefile 中為 all
。如果您真的需要迴避這個問題,請在 Makevars.[win] 中的任何實際目標之前使用合適的(虛假的)目標 all
:例如套件 fastICA 曾經有
PKG_LIBS = @BLAS_LIBS@ SLAMC_FFLAGS=$(R_XTRA_FFLAGS) $(FPICFLAGS) $(SHLIB_FFLAGS) $(SAFE_FFLAGS) all: $(SHLIB) slamc.o: slamc.f $(FC) $(SLAMC_FFLAGS) -c -o slamc.o slamc.f
需要確保 LAPACK 常式在沒有無限迴圈的情況下找到一些常數。Windows 等效項為
all: $(SHLIB) slamc.o: slamc.f $(FC) $(SAFE_FFLAGS) -c -o slamc.o slamc.f
(由於其他巨集在該平台上都是空的,且未曾使用 R 的內部 BLAS)。請注意,Makevars 中的第一個目標將被呼叫,但為了向後相容性,最好將其命名為 all
。
如果您想要建立並連結至函式庫,例如使用子目錄中的程式碼,請使用類似下列的內容
.PHONY: all mylibs all: $(SHLIB) $(SHLIB): mylibs mylibs: (cd subdir; $(MAKE))
請務必建立所有必要的相依性,因為無法保證 all
的相依性將會以特定順序執行 (且部分 CRAN 建置機器使用多個 CPU 和平行建置)。特別是
all: mylibs
並 不足夠。GNU make 允許建構
.NOTPARALLEL: all all: mylibs $(SHLIB)
但這並不可移植。dmake
和 pmake
允許類似的 .NO_PARALLEL
,同樣不可移植:部分 pmake
變體接受 .NOTPARALLEL
作為 .NO_PARALLEL
的別名。
請注意,在 Windows 上,Makevars[.win, .ucrt] 必須建立 DLL:這是必要的,因為這是確保建立 DLL 成功唯一的可靠方式。如果您想要將 src 目錄用於建立 DLL 以外的其他目的,請使用 Makefile.win 或 Makefile.ucrt 檔案。
有時在 Makevars、Makevars.win 或 Makevars.ucrt 中有一個目標「clean」會很有用:R CMD build
會使用它來清除套件來源(的副本)。當它由 build
執行時,它會設定較少的巨集,特別是沒有 $(SHLIB)
,也沒有 $(OBJECTS)
,除非在檔案本身中設定。也可以將任務新增到目標「shlib-clean」,它會由 R CMD INSTALL
和 R CMD SHLIB
執行,並加上選項 --clean 和 --preclean。
避免在 makefile 中使用預設(也稱為「隱含」規則),因為它們是 make
特有的。即使 POSIX 規定,GNU make
也不會遵守,這會導致套件安裝中斷。
一個很常見的錯誤是
all: $(SHLIB) clean
它要求 make
在編譯程式碼的同時進行清除。這不僅會導致難以偵錯的安裝錯誤,還會清除所有錯誤的證據(無論是否為並行 make)。最好讓最終使用者使用前一段中的功能來進行清除。
如果您要在 Makevars 中執行 R 程式碼,例如尋找組態資訊,請務必使用正確的 R
或 Rscript
副本:路徑中可能完全沒有副本,或者可能是錯誤的版本或架構。正確的方法是透過
"$(R_HOME)/bin$(R_ARCH_BIN)/Rscript" filename "$(R_HOME)/bin$(R_ARCH_BIN)/Rscript" -e ‘R expression’
其中 $(R_ARCH_BIN)
目前只在 Windows 上需要。
環境或 make 變數可用於為 32 位元和 64 位元程式碼選擇不同的巨集,例如(GNU make
語法,Windows 上允許)
ifeq "$(WIN)" "64" PKG_LIBS = value for 64-bit Windows else PKG_LIBS = value for 32-bit Windows endif
在 Windows 中,通常可以在連結至匯入函式庫或直接連結至 DLL 之間進行選擇。在可能的情況下,後者會可靠許多:匯入函式庫會繫結至特定工具鏈,特別是在 64 位元 Windows 中,通常會使用兩種不同的慣例。因此,例如,可以使用
PKG_LIBS = -L$(XML_DIR)/lib -lxml2
來取代
PKG_LIBS = -L$(XML_DIR)/bin -lxml2
因為在 Windows 中,-lxxx
會依序尋找
libxxx.dll.a xxx.dll.a libxxx.a xxx.lib libxxx.dll xxx.dll
其中第一個和第二個慣例上是匯入函式庫,第三個和第四個通常是靜態函式庫(.lib
專門用於 Visual C++),但可能是匯入函式庫。例如,請參閱 https://sourceware.org/binutils/docs-2.20/ld/WIN32.html#WIN32。
美中不足的是,DLL 可能不會命名為 libxxx.dll,而且事實上在 32 位元 Windows 中有一個 libxml2.dll,而在 64 位元 Windows 的其中一個建置中,DLL 稱為 libxml2-2.dll。使用匯入函式庫可以涵蓋這些差異,但可能會造成相等的困難。
如果可以使用靜態函式庫,它們可以解決許多 DLL 執行時期尋找的問題,特別是在要散布二進位套件時,更是在這些套件支援兩種架構時。在無法避免使用 DLL 的情況下,我們通常會安排(透過 configure.win 或 configure.ucrt)將它們與套件 DLL 放在同一個目錄中。
有些支援套件希望使用 OpenMP37。 make
巨集
SHLIB_OPENMP_CFLAGS SHLIB_OPENMP_CXXFLAGS SHLIB_OPENMP_FFLAGS
可在 src/Makevars、src/Makevars.win 或 Makevars.ucrt 中使用。在 PKG_CFLAGS
、PKG_CXXFLAGS
等中包含適當的巨集,並在 PKG_LIBS
中包含(但請參閱下方有關 Fortran 的資訊)。需要根據 OpenMP 使用情況設定條件的 C/C++ 程式碼可以在 #ifdef _OPENMP
內部使用:請注意,某些用於 R 的工具鏈(包括 Apple 的 macOS38 和一些使用 clang
39 的工具鏈)完全不支援 OpenMP,甚至連 omp.h 都不支援。
例如,一個針對 OpenMP 編寫 C 程式碼的套件應該在 src/Makevars 中包含下列程式碼行
PKG_CFLAGS = $(SHLIB_OPENMP_CFLAGS) PKG_LIBS = $(SHLIB_OPENMP_CFLAGS)
請注意,巨集 SHLIB_OPENMP_CXXFLAGS
適用於預設的 C++ 編譯器,而不一定適用於 C++17/20/23 編譯器:後者的使用者應執行自己的 configure
檢查。如果您使用自己的檢查,請編譯並連結一個使用 OpenMP 的程式,以確保 OpenMP 支援完整:在某些平台上,執行時期函式庫是選用的,而在其他平台上,該函式庫依賴於其他選用函式庫。
當編譯器來自可能使用不同 OpenMP 執行時期的不同系列時,需要特別小心(例如 clang
vs GCC,包括 gfortran
,儘管通常可以使用 clang
執行時期搭配 GCC,但 反之則不然:不過 gfortran
>= 9 可能會產生 clang
執行時期中不存在的呼叫)。對於使用 OpenMP 的 Fortran 程式碼套件,適當的程式碼行如下
PKG_FFLAGS = $(SHLIB_OPENMP_FFLAGS) PKG_LIBS = $(SHLIB_OPENMP_CFLAGS)
因為 C 編譯器將用於連結套件程式碼。有些平台無法執行此操作 (對於某些使用 OpenMP 的程式碼),安裝會失敗。由於 R >= 3.6.2,對於僅使用 OpenMP 的 Fortran 來源的套件,最佳的替代方案是使用
USE_FC_TO_LINK = PKG_FFLAGS = $(SHLIB_OPENMP_FFLAGS) PKG_LIBS = $(SHLIB_OPENMP_FFLAGS)
在 src/Makevars、src/Makevars.win 或 Makevars.ucrt 中。不過請注意,當使用此項時,$(FLIBS)
不應包含在 PKG_LIBS
中,因為它是用於由 C 編譯器連結 Fortran 編譯的程式碼。
常見的平台可能會內嵌所有 OpenMP 呼叫,因此容許從 PKG_LIBS
省略 OpenMP 旗標,但這通常會導致使用不同的編譯器或編譯旗標時安裝失敗。因此,請交叉檢查安裝記錄中的連結行中是否出現 -fopenmp
等字樣。
在單一套件中使用 OpenMP 搭配 C、C++ 和 Fortran 並不可移植,因為編譯器來自不同系列的情況並不少見。
為了可移植性,任何使用 omp_*
函式的 C/C++ 程式碼都應包含 omp.h 標頭:有些編譯器(但並非全部)會在開啟 OpenMP 模式時包含它(例如透過旗標 -fopenmp)。
對於支援哪個 OpenMP 版本,並40沒有任何說明:最近版本的 Linux 和 Windows 平台支援版本 4.0(以及 4.5 或 5.0 的大部分),但可移植套件無法假設最終使用者具有最新版本。macOS 上的 Apple clang
不支援 OpenMP。 https://www.openmp.org/resources/openmp-compilers-tools/ 提供了一些關於哪些編譯器支援哪些版本的資訊。請注意,對 Fortran 編譯器的支援通常較不新穎,而該頁面建議不要依賴 3.1 之後的版本。這引入了 Fortran OpenMP 模組,因此 OpemMP 的 Fortran 使用者應包含
use omp_lib
在 Linux 上使用 OpenMP 搭配 clang
時,很少會產生 libatomic
中的呼叫,導致載入類似以下的訊息
undefined symbol: __atomic_compare_exchange undefined symbol: __atomic_load
解決方法是連結 -latomic
(檢查它是否存在)。
OpenMP 的效能會因平台而異。Windows 實作有大量的額外負擔,因此只有在並行執行相當大量的任務時才有益處。此外,在 Windows 上會以預設41 FPU 控制字元來啟動新的執行緒,因此在 OpenMP 執行緒上執行的運算不會使用預設為主要程序的延伸精度運算。
除非程式碼確實會使用 OpenMP(可能透過包含外部標頭來使用 C++),否則請勿包含這些巨集:這樣可能會連結 OpenMP 執行時間、啟動執行緒等。
呼叫執行緒化程式碼中的任何 R API 是「僅限專家」的行為,強烈建議不要這麼做。R API 中的許多函式會修改 R 內部資料結構,如果從多個執行緒同時呼叫,可能會損毀這些資料結構。大多數 R API 函式可以發出錯誤訊號,這只能在 R 主執行緒上發生。此外,外部函式庫(例如 LAPACK)可能不是執行緒安全的。
套件並非獨立的程式,R 程序可能包含多個已啟用 OpenMP 的套件,以及其他使用 OpenMP 的元件(例如最佳化的 BLAS)。因此,需要仔細考量資源使用。OpenMP 會搭配並行區域使用,而大多數實作的預設值是使用與「CPU」數量一樣多的執行緒。並行區域可以巢狀,儘管通常只會在第一層以下使用單一執行緒。偵測到的「CPU」數量正確性,以及 R 程序有權使用所有「CPU」的假設,這兩個假設都令人懷疑。限制資源的方法之一,就是限制 R 程序中 OpenMP 可用的執行緒總數:可以在已實作的地方,透過 環境變數 OMP_THREAD_LIMIT
來執行此動作。42 或者,也可以透過環境變數 OMP_NUM_THREADS
或 API 呼叫 omp_set_num_threads
來限制每個區域的執行緒數量,或更理想的做法,是在程式碼中將區域當成規格的一部分。例如,R 使用43
#pragma omp parallel for num_threads(nthreads) ...
這樣一來,您只會控制自己的程式碼,而不是其他 OpenMP 使用者的程式碼。
請注意,設定環境變數來控制 OpenMP 是依實作而定,而且可能需要在 R 程序外部或在任何 OpenMP 使用之前(可能由其他程序或 R 本身)執行。此外,特定於實作的變數(例如 KMP_THREAD_LIMIT
)可能會優先。
沒有對 POSIX 執行緒(更常稱為 pthreads
)的直接支援:當我們考慮加入它時,有幾個套件已經無條件地使用它,因此它似乎現在在 POSIX 作業系統(因此不是 Windows)上普遍可用。
對於相當新的 gcc
和 clang
版本,正確的規格是
PKG_CPPFLAGS = -pthread PKG_LIBS = -pthread
(而複數版本也已在某些系統/版本中被接受)。對於其他平台,規格是
PKG_CPPFLAGS = -D_REENTRANT PKG_LIBS = -lpthread
(請注意函式庫名稱是單數)。這是 -pthread 在所有已知當前平台上執行的動作(儘管 OpenBSD 的早期版本使用不同的函式庫名稱)。
有關教學課程,請參閱 https://hpc-tutorials.llnl.gov/posix/。
POSIX 執行緒通常不用於 Windows,Windows 有自己的執行緒原生概念。但是,有兩個專案在 Windows 上實作 pthreads
,pthreads-w32
和 winpthreads
(MinGW-w64 專案的一部分)。
Windows 工具鏈是否實作 pthreads
取決於工具鏈提供者。make
變數 SHLIB_PTHREAD_FLAGS
可用於 src/Makevars.win 或 Makevars.ucrt:這應包含在 PKG_CPPFLAGS
(或 Fortran 編譯器旗標)和 PKG_LIBS
中。
無法在未進行自我測試的情況下明確確定是否有可用的 pthreads
實作:不過,如果在 C/C++ 程式碼中定義了「_REENTRANT」44,則是一個良好的跡象。
請注意,並非所有 pthreads
實作都相同,因為有些部分是選用的(請參閱 https://pubs.opengroup.org/onlinepubs/009695399/basedefs/pthread.h.html):例如,macOS 缺少「Barriers」選項。
另請參閱 OpenMP 中關於執行緒安全性與效能的說明:在所有已知的 R 平台上,OpenMP 都是透過 pthreads
實作,而已知的效能問題出在後者。
套件作者相當常會想要將程式碼整理到 src 的子目錄中,例如,如果他們要包含一個外部軟體的獨立部分,而這是 R 介面。
一個簡單的方法就是將 OBJECTS
設定為所有需要編譯的物件,包括子目錄中的物件。例如,CRAN 套件 RSiena 有
SOURCES = $(wildcard data/*.cpp network/*.cpp utils/*.cpp model/*.cpp model/*/*.cpp model/*/*/*.cpp) OBJECTS = siena07utilities.o siena07internals.o siena07setup.o siena07models.o $(SOURCES:.cpp=.o)
這種方法有一個問題,除非使用 GNU make 擴充功能,否則需要列出原始檔並保持最新狀態。如下列 CRAN 套件 lossDev 所示
OBJECTS.samplers = samplers/ExpandableArray.o samplers/Knots.o \ samplers/RJumpSpline.o samplers/RJumpSplineFactory.o \ samplers/RealSlicerOV.o samplers/SliceFactoryOV.o samplers/MNorm.o OBJECTS.distributions = distributions/DSpline.o \ distributions/DChisqrOV.o distributions/DTOV.o \ distributions/DNormOV.o distributions/DUnifOV.o distributions/RScalarDist.o OBJECTS.root = RJump.o OBJECTS = $(OBJECTS.samplers) $(OBJECTS.distributions) $(OBJECTS.root)
如果子目錄是具有適當 makefile 的獨立程式碼,則最佳方法類似於
PKG_LIBS = -LCsdp/lib -lsdp $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS) $(SHLIB): Csdp/lib/libsdp.a Csdp/lib/libsdp.a: @(cd Csdp/lib && $(MAKE) libsdp.a \ CC="$(CC)" CFLAGS="$(CFLAGS) $(CPICFLAGS)" AR="$(AR)" RANLIB="$(RANLIB)")
請注意引號:巨集可以包含空白,例如 CC = "gcc -m64 -std=gnu99"
。許多作者忘記並行建置:子目錄中的靜態函式庫必須在共用物件 ($(SHLIB)
) 之前建置,因此後者必須依賴前者。其他人則忘記位置獨立程式碼的需求45。
我們真的不建議使用 src/Makefile 代替 src/Makevars,而且如上方的範例所示,這並非必要。
提供使用 configure 指令碼建立 src/Makevars 檔案的延伸範例可能有所幫助:這基於 RODBC 套件中的範例。
configure.ac 檔案如下:configure 是透過在頂層套件目錄(包含 configure.ac)中執行 autoconf
而建立的。
AC_INIT([RODBC], 1.1.8) dnl package name, version dnl A user-specifiable option odbc_mgr="" AC_ARG_WITH([odbc-manager], AC_HELP_STRING([--with-odbc-manager=MGR], [specify the ODBC manager, e.g. odbc or iodbc]), [odbc_mgr=$withval]) if test "$odbc_mgr" = "odbc" ; then AC_PATH_PROGS(ODBC_CONFIG, odbc_config) fi dnl Select an optional include path, from a configure option dnl or from an environment variable. AC_ARG_WITH([odbc-include], AC_HELP_STRING([--with-odbc-include=INCLUDE_PATH], [the location of ODBC header files]), [odbc_include_path=$withval]) RODBC_CPPFLAGS="-I." if test [ -n "$odbc_include_path" ] ; then RODBC_CPPFLAGS="-I. -I${odbc_include_path}" else if test [ -n "${ODBC_INCLUDE}" ] ; then RODBC_CPPFLAGS="-I. -I${ODBC_INCLUDE}" fi fi dnl ditto for a library path AC_ARG_WITH([odbc-lib], AC_HELP_STRING([--with-odbc-lib=LIB_PATH], [the location of ODBC libraries]), [odbc_lib_path=$withval]) if test [ -n "$odbc_lib_path" ] ; then LIBS="-L$odbc_lib_path ${LIBS}" else if test [ -n "${ODBC_LIBS}" ] ; then LIBS="-L${ODBC_LIBS} ${LIBS}" else if test -n "${ODBC_CONFIG}"; then odbc_lib_path=`odbc_config --libs | sed s/-lodbc//` LIBS="${odbc_lib_path} ${LIBS}" fi fi fi dnl Now find the compiler and compiler flags to use : ${R_HOME=`R RHOME`} if test -z "${R_HOME}"; then echo "could not determine R_HOME" exit 1 fi CC=`"${R_HOME}/bin/R" CMD config CC` CFLAGS=`"${R_HOME}/bin/R" CMD config CFLAGS` CPPFLAGS=`"${R_HOME}/bin/R" CMD config CPPFLAGS` if test -n "${ODBC_CONFIG}"; then RODBC_CPPFLAGS=`odbc_config --cflags` fi CPPFLAGS="${CPPFLAGS} ${RODBC_CPPFLAGS}" dnl Check the headers can be found AC_CHECK_HEADERS(sql.h sqlext.h) if test "${ac_cv_header_sql_h}" = no || test "${ac_cv_header_sqlext_h}" = no; then AC_MSG_ERROR("ODBC headers sql.h and sqlext.h not found") fi dnl search for a library containing an ODBC function if test [ -n "${odbc_mgr}" ] ; then AC_SEARCH_LIBS(SQLTables, ${odbc_mgr}, , AC_MSG_ERROR("ODBC driver manager ${odbc_mgr} not found")) else AC_SEARCH_LIBS(SQLTables, odbc odbc32 iodbc, , AC_MSG_ERROR("no ODBC driver manager found")) fi dnl for 64-bit ODBC need SQL[U]LEN, and it is unclear where they are defined. AC_CHECK_TYPES([SQLLEN, SQLULEN], , , [# include <sql.h>]) dnl for unixODBC header AC_CHECK_SIZEOF(long, 4) dnl substitute RODBC_CPPFLAGS and LIBS AC_SUBST(RODBC_CPPFLAGS) AC_SUBST(LIBS) AC_CONFIG_HEADERS([src/config.h]) dnl and do substitution in the src/Makevars.in and src/config.h AC_CONFIG_FILES([src/Makevars]) AC_OUTPUT
其中 src/Makevars.in 會很簡單地
PKG_CPPFLAGS = @RODBC_CPPFLAGS@ PKG_LIBS = @LIBS@
然後可以建議使用者透過選項(斷行以方便閱讀)指定 ODBC 驅動程式管理員檔案的位置
R CMD INSTALL \ --configure-args='--with-odbc-include=/opt/local/include \ --with-odbc-lib=/opt/local/lib --with-odbc-manager=iodbc' \ RODBC
或透過設定環境變數 ODBC_INCLUDE
和 ODBC_LIBS
。
R 假設副檔名為 .f 的原始檔是固定格式的 Fortran 90(包含 Fortran 77),並將它們傳遞給巨集 ‘FC’ 所指定的編譯器。Fortran 編譯器也會接受副檔名為 .f90 或(大多數46).f95 的自由格式 Fortran 90/95 程式碼。
固定格式和自由格式 Fortran 程式碼(具有不同的副檔名和可能不同的旗標)使用相同的編譯器。巨集 PKG_FFLAGS
可用於特定套件的旗標:對於未遇到的情況,兩個都包含在單一套件中,且兩種格式需要不同的旗標,巨集 PKG_FCFLAGS
也可用於自由格式 Fortran。
用於建置 R 的程式碼允許選擇 ‘Fortran 90’ 編譯器為 ‘FC’,因此可能會遇到僅支援 Fortran 90 的平台。但是,所有已知的平台都支援 Fortran 95。
「FC」指定的編譯器大多會接受 Fortran 2003、2008 或 2018 程式碼:此類程式碼仍應使用檔案副檔名 .f90。大多數目前的平台使用 gfortran
,您可能需要在 PKG_FFLAGS
或 PKG_FCFLAGS
中包含 -std=f2003、-std=f2008 或(從版本 8 開始)-std=f2018:預設為「GNU Fortran」,目前為 Fortran 2018(但在 gfortran
8 之前為 Fortran 95),並帶有非標準擴充功能。目前使用的其他編譯器(LLVM 的 flang-new
和 Intel 的 ifx
)預設為 Fortran 2018。
建議在 DESCRIPTION 的「SystemRequirements」欄位中描述 Fortran 版本需求。
Fortran 的現代版本支援模組,編譯一個原始檔會建立一個模組檔,然後包含在其他檔案中。(模組檔通常具有 .mod 副檔名:它們取決於所使用的編譯器,因此絕不應包含在套件中。)這會建立一個 make
不會知道的依賴關係,而且通常會導致並行 make 安裝失敗。因此,有必要在 src/Makevars 中新增明確的依賴關係,以告知 make
編譯順序的限制。例如,如果檔案 iface.f90 建立一個由檔案 cmi.f90 和 dmi.f90 使用的模組「iface」,則 src/Makevars 需要包含類似下列內容
cmi.o dmi.o: iface.o
請注意,在多個原始檔中定義同名的模組並非可攜式的(儘管有些平台接受它)。
R 可以不使用 C++ 編譯器進行建置,儘管在所有已知的 R 平台上都提供 C++ 編譯器(但不一定已安裝)。從 R 4.0.0 開始,只有符合 2011 年標準(「C++11」)的 C++ 編譯器才會被選取。2014 年 12 月發布了一個次要更新47(「C++14」),如果支援的話,從 R 4.1.0 開始預設使用。自此之後,又陸續發布了「C++17」(2017 年 12 月)和「C++20」(2020 年 12 月,新增許多新功能)。預計下一個版本「C++23」將於 2023/4 年推出,目前已有許多編譯器已廣泛支援目前的草案。
如果支援的話,R 4.3.0 中編譯 R 套件的預設標準已變更為 C++17(對於較舊的編譯器,預設會使用 C++14 甚至 C++11)。
很難確定 C++ 編譯器旨在支援哪個標準:__cplusplus
的值48 可能有所幫助,但有些編譯器使用它來表示部分支援的標準,有些則表示(幾乎)完全支援的最新標準。在類 Unix 系統上,configure
會嘗試為每個標準識別編譯器和旗標:這在很大程度上依賴於 __cplusplus
的報告值。
網頁 https://en.cppreference.com/w/cpp/compiler_support 提供了一些關於已知支援最新 C++ 功能的編譯器的資訊。
C++ 標準已棄用並移除了一些功能。請注意,一些目前的編譯器在 C++17 模式下仍接受已移除的功能,例如 std::unary_function
(在 C++11 中已棄用,在 C++17 中已移除)。
不同版本的 R 使用不同的預設 C++ 標準,因此為了最大可攜性,套件應指定其所需的標準。若要使用 Makevars 檔案(或 Windows 上的 Makevars.win 或 Makevars.ucrt)在套件中指定 C++14 程式碼,應包含下列程式碼行
CXX_STD = CXX14
然後將使用 C++14 編譯器(如果有)進行編譯和連結。其他標準亦同(詳情如下)。另一方面,當程式碼在 C++14 或 C++17 下有效時,指定 C++1149 會降低未來的可攜性。
沒有 src/Makevars 或 src/Makefile 檔案的套件可以在 DESCRIPTION 檔案的「SystemRequirements」欄位中包含類似「C++14」的內容,為 src 目錄中的程式碼指定 C++ 標準,例如:
SystemRequirements: C++14
如果套件確實有 src/Makevars[.win] 檔案,建議同時設定 make 變數「CXX_STD」,因為它允許 R CMD SHLIB
在套件的 src 目錄中正確運作。
C++17 或更新版本的必要條件應始終在「SystemRequirements」欄位(以及 src/Makevars 或 src/Makefile)中宣告,因此這會顯示在 CRAN 或類似網站上的套件摘要頁面。這對於 C++14 的必要條件來說也是個好習慣。請注意,只有 R 3.4.0 才支援 C++14 或 C++17,因此如果套件有 R 版本必要條件,則需要考慮這一點。
基本上,GCC 5、LLVM clang
3.4 和目前使用的 Apple clang
版本提供完整的 C++14 支援。
需要 C++14 功能的程式碼可以透過「SD-6 功能測試」50來檢查它們是否存在。這樣的檢查可以是
#include <memory> // header where this is defined #if defined(__cpp_lib_make_unique) && (__cpp_lib_make_unique >= 201304) using std::make_unique; #else // your emulation #endif
C++17(從 R 3.4.0 開始)、C++20(從 R 4.0.0 開始)和 C++23(從 R 4.3.0 開始)可以用類似的方式指定(用 17
、20
或 23
)取代 14
),但編譯器/作業系統支援取決於平台。從 R 4.0.0 開始,macOS 和 Windows 上的 R 預設建置提供了一些 C++17 和 C++20 支援。g++
對 C++17 的大部分支援需要 7 或更新的版本:這比一些仍然是目前版本的 Linux 發行版更新,但通常可以取得更新編譯器的套件:對於 RHEL/Centos 7,請尋找「devtoolset」。
請注意,C++17 或更新的「支援」並不表示完整的支援:請使用功能測試以及 https://en.cppreference.com/w/cpp/compiler_support、https://gcc.gnu.org/projects/cxx-status.html 和 https://clang.llvm.org/cxx_status.html 等資源,以查看您想要使用的功能是否廣泛實作。
嘗試指定未知的 C++ 標準會被靜默忽略:最近版本的 R 會為 C++98 以及未偵測到編譯器+旗標的已知標準擲回錯誤。
如果使用 C++ 的套件有 configure
指令碼,則指令碼必須透過類似下列內容來選擇正確的 C++ 編譯器和標準
CXX17=`"${R_HOME}/bin/R" CMD config CXX17` if test -z "$CXX17"; then AC_MSG_ERROR([No C++17 compiler is available]) fi CXX17STD=`"${R_HOME}/bin/R" CMD config CXX17STD` CXX="${CXX17} ${CXX17STD}" CXXFLAGS=`"${R_HOME}/bin/R" CMD config CXX17FLAGS` ## for an configure.ac file AC_LANG(C++)
如果指定了 C++17,但使用
CXX=`"${R_HOME}/bin/R" CMD config CXX` CXXFLAGS=`"${R_HOME}/bin/R" CMD config CXXFLAGS` ## for an configure.ac file AC_LANG(C++)
如果未指定標準。
如果您要在子目錄中編譯 C++ 程式碼,請務必傳遞巨集來指定適當的編譯器,例如在 src/Makevars 中
sublibs: @(cd libs && $(MAKE) \ CXX="$(CXX17) $(CXX17STD)" CXXFLAGS="$(CXX17FLAGS) $(CXX17PICFLAGS)")
上述討論是關於編譯 C++ 的標準 R 方式:它不適用於使用 src/Makefile 的套件,或是在未設定 C++ 標準的子目錄中建置。而且編譯器的預設 C++ 標準差異很大,而且供應商經常變更,例如 Apple clang 14 預設為 C++98,LLVM clang 14–15 為 C++14,LLVM clang 16 為 C++17,g++
11–13 為 C++17。
對於具有 src/Makefile(或 Windows 類比)的套件,可以透過包含類似以下內容來選擇非預設 C++ 編譯器
CXX14 = `"${R_HOME}/bin/R" CMD config CXX14` CXX14STD = `"${R_HOME}/bin/R" CMD config CXX14STD` CXX = ${CXX14} ${CXX14STD} CXXFLAGS = `"${R_HOME}/bin/R" CMD config CXX14FLAGS` CXXPICFLAGS = `"${R_HOME}/bin/R" CMD config CXX14PICFLAGS` SHLIB_LD = "${R_HOME}/bin/R" CMD config SHLIB_CXX14LD` SHLIB_LDFLAGS = "${R_HOME}/bin/R" CMD config SHLIB_CXX14LDFLAGS`
並確保在相關編譯中使用這些值,在檢查它們是否非空後。 src/Makefile 的常見用途是編譯可執行檔,例如(例如對於 C++14)
CXX14 = `"${R_HOME}/bin/R" CMD config CXX14` CXX14STD = `"${R_HOME}/bin/R" CMD config CXX14STD` CXX = ${CXX14} ${CXX14STD} CXXFLAGS = `"${R_HOME}/bin/R" CMD config CXX14FLAGS`
就夠了。在 Unix(以及從 R 4.3.0 開始的 Windows)上,這可以簡化為
CXX = ${CXX14} ${CXX14STD} CXXFLAGS = ${CXX14FLAGS}
在類 Unix 的 C++ 編譯中,從 R 3.6.0 預設為 C++11,從 R 4.1.0 預設為 C++14,從 R 4.3.0 預設為 C++17。然而,僅在「可用時」,因此使用非常舊版作業系統的平台可能已使用先前的預設值。甚至更舊版本的 R 預設為編譯器的預設值,對於同年代的編譯器來說幾乎可以確定是 C++98。
在 Windows 上,預設值已從 R 3.6.2 中的 C++98 變更為 R 4.2.3 中的 C++11,再變更為 R 4.3.0 中的 C++17。
C++11 標準可以從 R 3.1.0 指定,C++14 或 C++17 可以從 R 3.4.0 指定,C++20 可以從 R 4.0.0 指定,C++23 可以從 R 4.3.0 指定(儘管它們可能不受使用的編譯器支援)。C++11 支援在 R 4.0.0 中已成為強制性。
套件中的 .so/.dll 可能需要由 C++ 編譯器連結,如果它或任何它連結的函式庫包含已編譯的 C++ 程式碼。動態連結通常會引入 C++ 執行時期函式庫(通常為 libstdc++
,但也可以是 libc++
等),但靜態連結(如用於 Windows 和 macOS 的外部函式庫)則不會。R CMD INSTALL
會在 src 中有任何頂層 C++ 檔案時使用 C++ 編譯器連結,但如果這些檔案都在子目錄中,則不會。強制使用 C++ 編譯器連結的最簡單方法是在 src 中包含一個空的 C++ 檔案。
C 已有 C89/C90、C99、C11、C17(也稱為 C18)標準,而 C23 處於最後草案階段,預計將於 2024 年初發布。C11 是對 C99 的小幅變更,引入了幾個新功能並使其他功能變為選用,而 C17 是對 C11 的「錯誤修正」更新。另一方面,C23 進行了廣泛的變更,包括將 bool
、true
和 false
設為保留字,最終不允許使用 K&R 風格的函式宣告,並澄清以前已棄用的函式宣告含空參數清單的含意,表示零個參數。(還有許多其他新增功能:例如,請參閱 https://en.cppreference.com/w/c/23。)
最近版本的 R 中的 configure
程式碼旨在選擇支援 C11 的 C 編譯器:由於 gcc
、LLVM clang
和 Apple clang
的最新版本中預設為 C17,因此可能會選擇 C17。另一方面,直到 R 4.3.0 為止,Windows 建置的 makefile 指定 C99。它們現在使用建議編譯器的編譯器預設值,即 C17。
套件可能想要避免或採用 C23 中的變更,並且可以透過在套件的 DESCRIPTION 檔案的「SystemRequirements」欄位中指定「USE_Cnn」來達成,其中 nn 為 17、23、90 或 99,視「R (>= 4.3.0)」而定。使用 configure
程式碼的人員應設定對應的編譯器和旗標,例如使用
CC=`"${R_HOME}/bin/R" CMD config CC23` CFLAGS=`"${R_HOME}/bin/R" CMD config C23FLAGS` CPPFLAGS=`"${R_HOME}/bin/R" CMD config CPPFLAGS` LDFLAGS=`"${R_HOME}/bin/R" CMD config LDFLAGS`
巨集 __STDC_VERSION__
可以檢查正在使用的(宣稱的)C 標準。這在 C89/C90 中未定義,並且對於 C99、C11 和 C17 應具有值 199901L
、201112L
和 201710L
。由於 C23 尚未發布,因此尚未有明確的值:編譯器目前使用 202000L
。C23 具有類似於 C++「功能測試」的巨集,可針對其許多變更使用,例如 __STDC_VERSION_LIMITS_H__
。
不過,請注意「宣稱的」一詞,因為沒有任何編譯器具有 100% 的相容性,而且最好使用 configure
來測試您想要使用的功能,而不是以 __STDC_VERSION__
的值為條件。特別是,C11 對齊功能(例如 _Alignas
和 aligned_alloc
)並未在 Windows 上實作。
使用者可以透過類似 R CMD INSTALL --use-C17
的指令來指定標準。這會覆寫「SystemRequirements」欄位,但不會對任何 configure
檔案生效。
cmake
¶套件通常會希望包含其他軟體的原始碼,並編譯以包含在他們的 .so 或 .dll 中,這通常是透過包含(或解壓縮)原始碼在 src 的子目錄中來完成,如上所述。
當外部軟體使用其他建置系統(例如 cmake
)時,會產生進一步的問題,主要是為了確保編譯器、包含和載入路徑等的所有設定都已完成。本節已經提到至少需要設定
CC CFLAGS CXX CXXFLAGS CPPFLAGS LDFLAGS
CFLAGS
和 CXXFLAGS
將需要分別包含 CPICFLAGS
和 CXXPICFLAGS
,除非(如下所示)要求 cmake
產生 PIC 程式碼。
將這些(和更多)設定為環境變數會控制 cmake
的行為(https://cmake.dev.org.tw/cmake/help/latest/manual/cmake-env-variables.7.html#manual:cmake-env-variables(7)),但可能需要將這些轉換為原生設定,例如
CMAKE_C_COMPILER CMAKE_C_FLAGS CMAKE_CXX_COMPILER CMAKE_CXX_FLAGS CMAKE_INCLUDE_PATH CMAKE_LIBRARY_PATH CMAKE_SHARED_LINKER_FLAGS_INIT CMAKE_OSX_DEPLOYMENT_TARGET
而且通常需要透過
-DBUILD_SHARED_LIBS:bool=OFF -DCMAKE_POSITION_INDEPENDENT_CODE:bool=ON
來確保建置 PIC 程式碼的靜態函式庫。
為了修正想法,考慮一個套件,其原始碼為 myLib 函式庫,位於 src/libs 下。已使用兩種方法。通常最方便的做法是在目錄中建置外部軟體,而不是其原始碼(特別是在開發期間,可以在建置之間移除建置目錄,而不是嘗試清理原始碼) – 這在第一種方法中說明。
PKG_CPPFLAGS = -Ilibs/include PKG_LIBS = build/libmyLib.a
(-Lbuild -lmyLib
也可以使用,但此明確指定可以避免與同名的動態函式庫混淆。)
configure 腳本需要包含類似以下內容(對於 C 程式碼)
: ${R_HOME=`R RHOME`} if test -z "${R_HOME}"; then echo "could not determine R_HOME" exit 1 fi CC=`"${R_HOME}/bin/R" CMD config CC` CFLAGS=`"${R_HOME}/bin/R" CMD config CFLAGS` CPPFLAGS=`"${R_HOME}/bin/R" CMD config CPPFLAGS` LDFLAGS=`"${R_HOME}/bin/R" CMD config LDFLAGS` cd src mkdir build && cd build cmake ../libs \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS:bool=OFF \ -DCMAKE_POSITION_INDEPENDENT_CODE:bool=ON ${MAKE}
PKG_CPPFLAGS = -Ilibs/include PKG_LIBS = libs/libmyLib.a $(SHLIB): mylibs mylibs: (cd libs; \ CC="$(CC)" CFLAGS="$(CFLAGS)" \ CPPFLAGS="$(CPPFLAGS)" LDFLAGS="$(LDFLAGS)" \ cmake . \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS:bool=OFF \ -DCMAKE_POSITION_INDEPENDENT_CODE:bool=ON; \ $(MAKE))
編譯器和其他設定已由 INSTALL
在 src/Makevars 之前包含的 R makefile 設定為 Make 變數。
一個複雜的情況是,在 macOS 上 cmake
(如果已安裝)通常不在路徑中,而是在 /Applications/CMake.app/Contents/bin/cmake。解決此問題的方法之一是讓套件的 configure 腳本包含
if test -z "$CMAKE"; then CMAKE="`which cmake`"; fi if test -z "$CMAKE"; then CMAKE=/Applications/CMake.app/Contents/bin/cmake; fi if test -f "$CMAKE"; then echo "no ‘cmake’ command found"; exit 1; fi
並讓第二種方法將 CMAKE
替換為 src//Makevars。
在使用這些工具之前,請檢查您的套件是否可以安裝。 R CMD check
將會 inter alia 執行此動作,但您可能會在直接安裝時取得更詳細的錯誤訊息。
如果您的套件在 DESCRIPTION 檔案中指定編碼,您應該在使用該編碼的區域設定中執行這些工具:它們可能完全無法在其他區域設定中執行,或可能無法正確執行(儘管 UTF-8 區域設定很可能會執行)。
注意:
R CMD check
和R CMD build
執行 R 程序時會使用 --vanilla,其中不會讀取任何使用者的啟動檔案。如果您需要設定R_LIBS
(在非標準函式庫中尋找套件),您可以在環境中設定它:您也可以使用檢查和建置環境檔案(如環境變數R_CHECK_ENVIRON
和R_BUILD_ENVIRON
所指定;如果未設定,則檔案51 ~/.R/check.Renviron 和 ~/.R/build.Renviron 會在使用這些公用程式時用於設定環境變數。
Windows 使用者注意: 如果存在且在您的路徑中,
R CMD build
可能會使用 Windows 工具集(請參閱「R 安裝和管理」手冊),而需要它的套件(包括具有 configure.win、cleanup.win、configure.ucrt 或 cleanup.ucrt 腳本或 src 目錄的套件)以及例如需要建置小插圖時,則需要它。您可能需要設定環境變數
TMPDIR
以指向一個合適的可寫入目錄,其路徑不包含空格 – 使用正斜線作為分隔符號。此外,目錄需要位於區分大小寫的檔案系統上(某些網路掛載的檔案系統不是)。
使用 R 套件檢查器 R CMD check
,可以測試原始碼 R 套件是否運作正常。可以在一個或多個目錄,或壓縮套件 tar
檔案(副檔名為 .tar.gz、.tgz、.tar.bz2 或 .tar.xz)上執行。
強烈建議在由 R CMD build
準備的 tar
檔案上執行最後檢查。
這會執行一系列檢查,包括
file
52。(可能會出現罕見的誤判。)
R_LIBS
。)其中一項檢查是套件名稱不能是標準套件的名稱,也不能是已廢止的標準套件的名稱(「ctest」、「eda」、「lqs」、「mle」、「modreg」、「mva」、「nls」、「stepfun」和「ts」)。另一項檢查是 library
或 require
中提到的所有套件,或 NAMESPACE 檔案從中匯入或 透過 ::
或 :::
呼叫的套件,都必須列出(在「Depends」、「Imports」、「Suggests」中):這並非對實際匯入的完整檢查。
若要允許 configure 程式碼產生適當的檔案,將允許 R 目錄中以「.in」結尾的檔案。
對於看起來像 R 套件檢查目錄的目錄名稱,會發出警告,許多已提交至 CRAN 的套件都包含這些目錄。
library.dynam
。檢查套件啟動函數的引數清單是否正確,以及是否 (不正確地) 呼叫會修改搜尋路徑或不適當地產生訊息的函數。使用 codetools 檢查 R 程式碼是否有潛在問題。此外,檢查 S3 方法是否具有對應泛函的所有引數,以及替換函數的最後一個引數是否稱為「value」。測試所有外部函數呼叫 (.C
、.Fortran
、.Call
和 .External
呼叫) 是否有 PACKAGE
引數,如果沒有,是否可以從套件的命名空間推斷出適當的 DLL。報告任何其他呼叫。(檢查很寬鬆,使用者可能想要透過檢查 tools::checkFF("mypkg", verbose=TRUE)
的輸出結果來補充這一點,特別是在打算總是使用 PACKAGE
引數的情況下)
\name
、\alias
、\title
和 \description
)。檢查 Rd 名稱和標題是否非空,並檢查是否有遺失的交叉參照 (連結)。
\usage
區段中給定的所有函數引數是否在對應的 \arguments
區段中記錄。
已編譯的程式碼會檢查對應於函數的符號,這些函數可能會終止 R 或寫入 stdout/stderr,而不是寫入主控台。請注意,後者可能會產生誤報,因為符號可能會透過外部函式庫引入,但永遠不會被呼叫。Windows54 使用者應注意,Fortran 和 C++ 執行時期函式庫就是此類外部函式庫的範例。
qpdf
),則會檢查 PDF 文件的最小大小。
\examples
建立可執行範例程式碼的資訊。)如果有一個檔案 tests/Examples/pkg-Ex.Rout.save,則會將執行範例的輸出與該檔案進行比較。
當然,已發佈的套件應該至少能夠執行自己的範例。每個範例都會在「乾淨的」環境中執行(因此無法假設已執行較早的範例),而且會重新定義變數 T
和 F
,以產生錯誤,除非它們在範例中已設定:請參閱 R 簡介 中的 邏輯向量。
--test-dir=foo
可用於指定非標準位置的測試。例如,異常緩慢的測試可以放置在 inst/slowTests 中,然後使用 R CMD check --test-dir=inst/slowTests
來執行它們。其他建議的名稱例如,需要安裝 Oracle 的測試為 inst/testWithOracle,使用隨機值且偶爾可能會因機率而失敗的測試為 inst/randomTests,等等。)
如果在範例 foo.ext 中執行 R 程式碼時發生錯誤55,則會在檢查目錄中建立記錄檔 foo.ext.log。範例會在檢查目錄的 vign_test 子目錄中套件來源的副本中重新製作,因此有關錯誤的進一步資訊請查看目錄 pkgname/vign_test/vignettes。(僅在有錯誤或環境變數 _R_CHECK_CLEAN_VIGN_TEST_
設為 false 值時才會保留。)
R CMD check --as-cran
)可選擇建立並檢查是否符合 HTML5 標準。這需要「HTML Tidy」的最新版本56,可放在路徑中或由環境變數 R_TIDYCMD
指定位置。可從 http://binaries.html-tidy.org/ 安裝最新版本。
所有這些測試都會在校對設定為 C
區域設定的情況下執行,範例和測試則使用環境變數 LANGUAGE=en
:這是為了將不同平台之間的差異降到最低。
使用 R CMD check --help 來取得有關 R 套件檢查器用法的更多資訊。可透過新增命令列選項來選取檢查步驟的子集。它也允許透過設定環境變數 _R_CHECK_*_
來自訂,如 R 內部 中的 工具 所述:可透過選項 --as-cran 來選取一組自訂設定,類似 CRAN 所使用的設定(如果可使用網際網路,效果最佳)。某些 Windows 使用者可能需要將環境變數 R_WIN_NO_JUNCTIONS
設定為非空值。循環宣告57 的測試在 DESCRIPTION 檔案中需要設定儲存庫(包含 CRAN):在 ~/.Rprofile 中執行此動作,例如:
options(repos = c(CRAN="https://r-cran.dev.org.tw"))
一個可能揭露問題的檢查自訂設定是
_R_CHECK_CODETOOLS_PROFILE_="suppressLocalUnused=FALSE"
報告未使用的區域指派。這不僅指出因結果未使用的計算是不必要的,還能揭露錯誤。(例如,兩個這樣的錯誤是打算透過指派值來更新物件,但錯打名稱或在錯誤的範圍內指派,例如使用 <-
,而打算使用 <<-
。)這可能會產生假陽性,最常見的原因是公式的非標準評估,以及打算在函數的環境中傳回物件以供稍後使用。
要完整檢查包含檔案 README.md 的套件,需要安裝相當新的 pandoc
版本:請參閱 https://pandoc.org/installing.html。
如果您包含非 ASCII 字元,則需要確保在適當的區域設定中檢查套件。此類套件可能會在 C
區域設定中無法通過某些檢查,而 R CMD check
會在發現問題時發出警告。您應該可以在 UTF-8 區域設定中檢查任何套件(如果有的話)。請注意,儘管 C
區域設定很少在主控台中使用,但如果遠端登入或進行批次作業,它可能是預設值。
通常 R CMD check
需要諮詢 CRAN 儲存庫來檢查已解除安裝套件的詳細資料。這通常預設為 CRAN 主要網站,但可以透過將環境變數 R_CRAN_WEB
和(很少需要)R_CRAN_SRC
設定為 CRAN 鏡像網站的 URL,來指定鏡像網站。
套件可以「tarball」(.tar.gz 檔案)的形式以原始碼形式發行,或以二進制形式發行。原始碼形式可以使用適當的工具安裝在所有平台上,而且是類 Unix 系統的常見形式;二進制形式是特定於平台的,而且是 Windows 和 macOS 平台較常見的發行形式。
使用 R 套件建構器 R CMD build
,可以從其原始碼(例如,供後續發行)建立 R 套件 tarball。建議使用 R 的目前發行版本或「r-patched」來建立套件以供發行,以避免無意間選取 R 開發版本的新功能。
在標準的 gzip 壓縮 tar 檔案格式中實際建立套件之前,會執行一些診斷檢查和清理。特別會測試物件索引是否存在,以及是否可以假設為最新,並測試 src 目錄中的 C、C++ 和 Fortran 原始碼檔案以及相關的 makefile,並在必要時將其轉換為 LF 行尾。
在呼叫最後的建立程序之前,應該使用 R CMD check
執行執行時間檢查,以檢查套件是否正確運作。
若要排除檔案不放入套件中,可以在頂層原始碼目錄的 .Rbuildignore 檔案中指定一列排除模式。這些模式應為 Perl 式正規表示法(有關精確的詳細資訊,請參閱 R 中 regexp
的說明),每行一個,以不分大小寫的方式與頂層套件原始碼目錄相對應的檔案和目錄名稱進行比對。此外,預設會排除來自原始碼控制系統58 或 eclipse
59 的目錄,名稱為 check、chm 或結尾為 .Rcheck、Old 或 old 的目錄,以及檔案 GNUMakefile60、Read-and-delete-me 或基本名稱以「.#」開頭,或以「#」開頭和結尾,或以「~」、「.bak」或「.swp」結尾的檔案61。此外,同一個套件的 tarball(來自先前的建置)及其二進位形式會從頂層目錄中排除,而 R、demo 和 man 目錄中的那些檔案會被 R CMD check
標記為名稱無效。
使用 R CMD build --help 以取得有關 R 套件建置器的使用方式的更多資訊。
除非使用 --no-build-vignettes 選項呼叫 R CMD build(或套件的 DESCRIPTION 含有「BuildVignettes: no」或類似內容),否則它會嘗試(重新)建置套件中的範例(請參閱 撰寫套件範例)。為此,它會將目前的套件安裝到暫時函式庫樹狀結構中,但任何相依套件都需要安裝到可用的函式庫樹狀結構中(請參閱本節最上方的附註:)。
類似地,如果 .Rd 文件包含任何 \Sexpr
巨集(請參閱 動態頁面),套件將暫時安裝以執行它們。包含建置時間巨集的那些頁面的執行後二進位副本將儲存在 build/partial.rdb 中。如果存在任何安裝時間或呈現時間巨集,套件手冊的 .pdf 版本將建置並安裝在 build 子目錄中。(這允許 CRAN 或其他儲存庫顯示手冊,即使它們無法安裝套件。)這可以透過選項 --no-manual 或如果套件的 DESCRIPTION 包含 ‘BuildManual: no’ 或類似內容來取消。
R CMD build
執行的其中一項檢查是針對空的來源目錄。在大多數(但並非全部)情況下,這些都是無意的,如果它們是有意的,請使用選項 --keep-empty-dirs(或將環境變數 _R_BUILD_KEEP_EMPTY_DIRS_
設定為 ‘TRUE’,或在 DESCRIPTION 檔案中有一個具有真值的 ‘BuildKeepEmpty’ 欄位)。
選項 --resave-data 允許在 data 目錄中儲存的影像(.rda 和 .RData 檔案)針對大小進行最佳化。它也會壓縮表格檔案,並將 .R 檔案轉換為儲存的影像。它可以接受的值為 no
、gzip
(如果未提供此選項,則為預設值,可透過設定環境變數 _R_BUILD_RESAVE_DATA_
來變更)和 best
(等於不提供值,會選擇最有效的壓縮方式)。如果任何檔案選用 bzip2
或 xz
壓縮,則使用 best
會在 DESCRIPTION 檔案中加入對 R (>= 2.10)
的依賴關係。如果這被認為是不理想的,--resave-data=gzip(如果未提供該選項,則為預設值)將使用 gzip
執行它所能執行的壓縮。套件可以透過在 DESCRIPTION 檔案中提供「BuildResaveData」欄位(包含本段落前面提到的其中一個值)來控制其資料的重新儲存方式。
選項 --compact-vignettes 會對 inst/doc(及其子目錄)中的 PDF 檔案執行 tools::compactPDF
以無損失地壓縮它們。這並非預設啟用(可透過環境變數 _R_BUILD_COMPACT_VIGNETTES_
選擇),且需要 qpdf
(https://qpdf.sourceforge.io/) 可用。
在已建置的 tarball 上執行 R CMD check --check-subdirs=yes
作為對內容的最終檢查會很有用。
如果使用不使用執行權限的非 POSIX 檔案系統,則需要小心處理權限。這適用於 Windows 和例如 FAT 格式化磁碟機和 SMB 掛載的其他作業系統上的檔案系統。tarball 中記錄的檔案「模式」將會是 file.info()
回傳的任何內容。在 Windows 上,這只會將目錄記錄為具有執行權限,而在其他作業系統上,所有檔案可能會報告「模式」0777
。一個特殊的問題是在 Windows 上建置的套件,其中包含可執行腳本,例如 configure 和 cleanup:R CMD build
確保這兩個檔案以執行權限記錄。
套件來源的目錄 build 保留供 R CMD build
使用:它包含在安裝套件時可能無法輕鬆建立的資訊,包括小冊子的索引資訊,以及較少見的說明頁面資訊,可能還有 PDF 參考手冊的副本(請參閱上方)。
二進位套件是已安裝套件版本的壓縮副本。它們包含已編譯的共用函式庫,而不是 C、C++ 或 Fortran 原始碼,且 R 函式包含在它們已安裝的形式中。格式和檔名取決於平台;例如,Windows 的二進位套件通常提供為 .zip 檔案,而 macOS 平台的預設二進位套件檔案副檔名為 .tgz。
建置二進位套件的建議方式是使用
R CMD INSTALL --build pkg
其中 pkg 是原始 tarball 的名稱(採用一般的 .tar.gz 格式)或要建置的套件原始碼目錄的位置。這會先安裝套件,然後將已安裝的二進位檔打包到特定平台適用的二進位套件檔案中。
預設情況下,R CMD INSTALL --build
會嘗試將套件安裝到 R 本機安裝的預設函式庫樹狀結構中。這有兩個含意
若要防止變更目前的作業安裝,或提供具有寫入存取權限的安裝位置,請建立一個具有寫入存取權限的適當位置目錄,並使用 -l
選項在所選位置建置套件。用法如下
R CMD INSTALL -l location --build pkg
其中 location 是具有寫入存取權限的所選目錄。套件會安裝為 location 的子目錄,而且套件二進位檔會在目前的目錄中建立。
使用 R CMD INSTALL --help
可以找到 R CMD INSTALL
的其他選項,而且特定案例的平台特定詳細資料會在平台特定的常見問答集中討論。
最後,至少有一項網路服務可用於從(已檢查的)原始碼建置二進位套件:WinBuilder(請參閱 https://win-builder.R-project.org/)能夠建置 Windows 二進位檔。請注意,這是針對其他平台上沒有 Windows 存取權限,但希望提供 Windows 平台二進位檔的開發人員。
除了 Rd 格式的說明檔案外,R 套件允許包含其他任意格式的文件。這些文件的標準位置是原始碼套件的 inst/doc 子目錄,當套件安裝時,內容會複製到 doc 子目錄。會自動建立從套件說明索引到已安裝文件的指標。 inst/doc 中的文件可以採用任意格式,但我們強烈建議提供 PDF 格式,以便幾乎所有平台上的使用者都能輕鬆閱讀。為確保可以從瀏覽器存取(因為會提供 HTML 索引),檔案名稱應以 ASCII 字母開頭,且完全由 ASCII 字母、數字、連字號或底線組成。
特殊情況是套件說明文件。說明文件是以 PDF 或 HTML 格式取得的純文字原始檔案,R 知道如何從中擷取 R 程式碼並建立輸出(以 PDF/HTML 或中間 LaTeX)。說明文件引擎會執行這項工作,分別使用「tangle」和「weave」函數。由 R 發行版提供的 Sweave 是預設引擎。除了 Sweave 之外,還支援其他說明文件引擎;請參閱非 Sweave 說明文件。
套件說明文件的來源位於套件來源的子目錄 vignettes 中。請注意,說明文件來源的位置只會影響 R CMD build
和 R CMD check
:由 R CMD build
建立的 tarball 包含在 inst/doc 中,打算安裝的組件。
Sweave 說明文件來源通常給予檔案副檔名 .Rnw 或 .Rtex,但基於歷史原因,副檔名62 .Snw 和 .Stex 也被辨識。Sweave 允許整合 LaTeX 文件:請參閱 R 中的 Sweave
說明頁面和套件 utils 中的 Sweave
說明文件,以取得來源文件格式的詳細資訊。
套件範例由 R CMD check
執行所有 R 程式碼區塊(除了標記為非評估的,例如 Sweave 的選項 eval=FALSE
)來進行測試。所有範例測試的 R 工作目錄在 R CMD check
中是範例來源目錄的 副本。請確定所有用於執行範例中 R 程式碼所需檔案(資料集等)都可以存取,方法是將它們放置在來源套件的 inst/doc 階層中,或使用呼叫 system.file()
。所有用於重新製作範例所需的其他檔案(例如 LaTeX 樣式檔案、BibTeX 輸入檔案和任何未透過執行範例中程式碼而建立的圖形檔案)都必須在範例來源目錄中。R CMD check
會透過比較 inst/doc 中輸出檔案的修改時間與 vignettes 中的來源,來檢查範例製作是否成功。
R CMD build
會自動63為套件來源建立 inst/doc 中的範例(PDF 或 HTML 版本),以便與套件來源一起發行。透過將範例輸出包含在套件來源中,因此不需要在安裝時重新建置這些輸出,也就是說,套件作者可以使用只有在他們的機器上可用的私人 R 套件、螢幕擷取畫面和 LaTeX 擴充功能。64
預設 R CMD build
會對 vignettes 中所有 Sweave 範例原始檔執行 Sweave
。如果在範例原始檔目錄中找到 Makefile,則 R CMD build
會嘗試在執行 Sweave
之後執行 make
,否則會對每個產生的 .tex 檔執行 texi2pdf
。
Makefile 中的第一個目標應同時負責建立 PDF/HTML 檔和之後的清理(包括在 Sweave
之後),亦即刪除所有不應出現在最終套件封存檔中的檔案。請注意,如果 make
步驟執行 R,則它需要小心遵循 R_LIBS
和 R_HOME
65 的環境值。最後,如果有一個 Makefile 且它有一個「clean:」目標,則會執行 make clean
。
所有關於包含 Makefile 的常見注意事項都適用。它必須是可攜式的(沒有 GNU 延伸),使用 LF 行尾,且必須與並行的 make
正確運作:太多作者寫了類似
## BAD EXAMPLE all: pdf clean pdf: ABC-intro.pdf ABC-details.pdf %.pdf: %.tex texi2dvi --pdf $* clean: rm *.tex ABC-details-*.pdf
的東西,這會在 pdflatex
運作時開始移除原始檔。
可以在原始檔中放置元資料行,最好放在前言的 LaTeX 註解中。其中一個是表單為 \VignetteIndexEntry
的
%\VignetteIndexEntry{Using Animal}
您可能會看到的其他內容包括 \VignettePackage
(目前已忽略)、\VignetteDepends
(以逗號分隔的套件名稱清單)和 \VignetteKeyword
(已取代 \VignetteKeywords
)。這些內容會在套件安裝時處理,以建立已儲存的資料框 Meta/vignette.rds。\VignetteEngine
陳述說明於 非 Sweave 小節 中。小節的元資料可以使用 tools::vignetteInfo
從來源檔案中萃取。
在安裝時,會自動從 \VignetteIndexEntry
陳述建立所有套件小節的 HTML 索引,除非在目錄 inst/doc 中存在檔案 index.html。此索引會從套件的 HTML 說明索引連結。如果您提供 inst/doc/index.html 檔案,它應該只包含已安裝 doc 目錄下檔案的相對連結,或(實際上並非索引)連結到 HTML 說明檔案或 DESCRIPTION 檔案,並透過 W3C 標記驗證服務 或 Validator.nu 確認為有效的 HTML。
Sweave/Stangle 允許文件指定 split=TRUE
選項,以針對每個程式碼區塊建立單一 R 檔案:這不適用於小節,因為假設每個小節來源會產生一個單一檔案,其中小節副檔名會取代為 .R。
請注意 PDF 不能太大,CRAN 套件中的其中一個 PDF 有 72MB!這通常是因為包含過於詳細的圖形,這些圖形在 PDF 檢視器中無法順利呈現。有時候,產生相當高解析度的位圖(PNG、JPEG)圖形並將其包含在 PDF 文件中會好得多。
當 R CMD build
建立範例時,它會將這些範例和範例來源從目錄 範例 複製到 inst/doc。若要從 範例 目錄安裝任何其他檔案,請包含一個檔案 範例/.install_extras,其中指定這些檔案為一或多行的 Perl 類似正規表示式。(有關完整詳細資料,請參閱 .Rinstignore 檔案的說明。)
範例通常會包含說明性文字、R 輸入、R 輸出和數字、LaTeX 包含檔案和書目參考。由於其中任何一個都可能包含非 ASCII 字元,因此編碼的處理可能會變得非常複雜。
範例來源檔案應撰寫為 ASCII 或包含編碼宣告(請參閱下方)。這甚至適用於來源檔案內的註解,因為範例引擎會處理註解以尋找選項和元資料行。當引擎的編織和糾纏函數在範例來源上被呼叫時,它會被轉換為當前 R 工作階段的編碼。
Stangle()
會產生一個 R 程式碼檔案,使用目前區域設定的編碼:對於非 ASCII 的範例,會在檔案最上方的一則註解中記錄編碼。
Sweave()
會產生一個 .tex 檔案,使用目前的編碼,或是在宣告時使用 UTF-8。非 ASCII 編碼需要透過類似下列的程式碼宣告給 LaTeX
\usepackage[utf8]{inputenc}
(也可以使用較新的 LaTeX 套件「inputenx」。)對於不需要這行程式碼的檔案(例如包含在較大型文件主體中的章節,或非 Sweave 範例),可以使用類似下列的註解宣告編碼
%\VignetteEncoding{UTF-8}
如果編碼是 UTF-8,也可以使用下列宣告
%\SweaveUTF8
如果範例中沒有給定宣告,會假設使用套件宣告的編碼。如果在任一位置都沒有宣告編碼,則在範例中使用非 ASCII 字元會產生錯誤。
無論如何,請注意 LaTeX 可能需要「usepackage」宣告。
Sweave()
也會解析並評估每個區塊中的 R 程式碼。R 輸出也會使用目前的區域設定(或在宣告時使用 UTF-8),並且應該包含在「inputenc」宣告中。人們常忘記的一件事是,R 輸出可能不是 ASCII,即使對於 ASCII R 來源也是如此,原因可能有很多。一個常見的原因是使用「花俏」的引號:請參閱 R 說明中的 sQuote
:請仔細注意,宣告 UTF-8 或 CP1252 來涵蓋此類引號並不可移植,因為其編碼會取決於用於執行 Sweave()
的區域設定:這可以用在範例中設定 options(useFancyQuotes="UTF-8")
來解決。
最後一個問題是圖形的編碼,這只適用於 PDF 圖形,不適用於 PNG 等。PDF 圖形會包含其編碼的宣告,但 Sweave 選項 pdf.encoding
可能需要適當地設定:請參閱 pdf()
圖形裝置的說明。
作為複雜性的實際範例,請考慮 fortunes 套件版本「1.4-0」。該套件沒有宣告編碼,且其範例在 ASCII 中。不過,它顯示的資料會從 UTF-8 CSV 檔讀取,並假設使用目前的編碼,因此 fortunes.tex 在任何區域設定中都將使用 UTF-8。如果已告知 read.table
資料是 UTF-8,則 fortunes.tex 將會使用區域設定的編碼。
以 Sweave 以外格式呈現的範例透過「範例引擎」獲得支援。例如 knitr 版本 1.1 或更新版本可以從 Sweave 格式的變體建立 .tex 檔,並從「標記」格式的變體建立 .html 檔。這些引擎會將 Sweave()
函式替換為其他函式,以將範例原始檔轉換成 LaTeX 檔,以便處理成 .pdf,或直接轉換成 .pdf 或 .html 檔。 Stangle()
函式會替換為從範例中擷取 R 原始碼的函式。
R 使用引擎指定的檔名副檔名來辨識非 Sweave 範例。例如,knitr 套件支援副檔名 .Rmd(代表「R 標記」)。使用者在範例原始碼中使用 \VignetteEngine
行來指出範例引擎,例如
%\VignetteEngine{knitr::knitr}
這會指定一個套件和一個引擎的名稱,用於處理範例中的 Sweave。由於 Sweave
是 R 散佈中提供的唯一引擎,因此提供任何其他引擎的套件必須在套件 DESCRIPTION 檔案的「VignetteBuilder」欄位中指定,並在「Suggests」、「Imports」或「Depends」欄位中指定(因為它的命名空間必須可用於建置或檢查您的套件)。如果指定多個套件作為建置器,它們將按照指定的順序進行搜尋。utils 套件始終會隱含附加到建置器套件清單中,但可能會在前面包含以變更搜尋順序。
請注意,具有非 Sweave 範例的套件應始終在 DESCRIPTION 檔案中有一個「VignetteBuilder」欄位,因為這是 R CMD check
辨識有範例需要檢查的方式:檢查套件時需要列出的套件。
範例引擎可以產生 .tex、.pdf 或 .html 檔案作為輸出。如果它產生 .tex 檔案,R 將呼叫 texi2pdf
將它們轉換為 .pdf 以供使用者顯示(除非 vignettes 目錄中有 Makefile)。
想要提供範例引擎的套件撰寫人員需要在套件 .onLoad
函式中註冊這些引擎。例如,該函式可以呼叫
tools::vignetteEngine("knitr", weave = vweave, tangle = vtangle, pattern = "[.]Rmd$", package = "knitr")
(knitr 中的實際註冊比較複雜,因為它支援其他輸入格式。)有關引擎註冊的詳細資訊,請參閱 ?tools::vignetteEngine
說明主題。
R 有一個用於套件中程式碼的名稱空間管理系統。這個系統允許套件撰寫者指定套件中哪些變數應匯出以供套件使用者使用,以及哪些變數應從其他套件匯入。
套件的名稱空間由頂層套件目錄中的 NAMESPACE 檔案指定。此檔案包含描述名稱空間匯入和匯出的名稱空間指令。其他指令會註冊要載入的任何共用物件和提供的任何 S3 樣式方法。請注意,儘管該檔案看起來像 R 程式碼(且通常有 R 樣式的註解),但它並未被處理為 R 程式碼。僅實作 if
陳述式的非常簡單的條件式處理。
套件會透過呼叫 library
或 require
載入並附加到搜尋路徑。只有匯出的變數會置於附加的框架中。載入從其他套件匯入變數的套件也會導致載入這些其他套件(除非它們已載入),但這些隱式載入不會將它們置於搜尋路徑中。因此,套件中的程式碼只能依賴於其自己的名稱空間和匯入(包括 base 名稱空間)中的物件是可見的66。
名稱空間一旦載入就會密封。密封表示無法變更匯入和匯出,且無法變更內部變數繫結。密封允許名稱空間機制有更簡單的實作策略,並允許程式碼分析和編譯工具準確地識別函式主體中全域變數參考對應的定義。
命名空間控制套件中函數所使用的變數的搜尋策略。如果在本地找不到,R 會先搜尋套件命名空間,然後是匯入,然後是基本命名空間,然後是正常的搜尋路徑(因此基本命名空間會在正常搜尋之前,而不是在最後)。
使用 NAMESPACE 檔案中的 export
指令指定匯出。格式為
export(f, g)
指定變數 f
和 g
要匯出。(請注意變數名稱可以加上引號,保留字和非標準名稱(例如 [<-.fractions
)必須加上引號。)
對於具有許多要匯出變數的套件,使用 exportPattern
搭配正規表示式來指定要匯出的名稱可能會更方便。指令
exportPattern("^[^\\.]")
匯出所有不以句點開頭的變數。但是,不建議在生產程式碼中使用如此廣泛的模式:最好列出所有匯出或使用定義範圍較窄的群組。(此模式適用於 S4 類別。)小心包含以句點開頭的名稱的模式:其中一些是僅限內部的變數,且不應匯出,例如「.__S3MethodsTable__.」(且載入會排除已知案例)。
套件會隱含匯入基礎名稱空間。需要從具有名稱空間的其他套件匯出的變數,必須使用指令 import
和 importFrom
明確匯入。import
指令會從指定的套件匯入所有匯出的變數。因此,指令
import(foo, bar)
指定要從套件 foo 和 bar 匯入所有匯出的變數。如果只需要的匯出變數只有幾個,則可以使用 importFrom
匯入。指令
importFrom(foo, f, g)
指定要從套件 foo 匯入匯出的變數 f
和 g
。有選擇性地使用 importFrom
而不是 import
是良好的做法,特別是在從匯出超過十幾個變數的套件(尤其是別人寫的套件,因為它們的匯出內容未來可能會變更)匯入時建議這麼做。
若要從套件匯入每個符號,但有少數例外,請將 except
參數傳遞給 import
。指令
import(foo, except=c(bar, baz))
從 foo 匯入每個符號,但 bar
和 baz
除外。except
的值應評估為可強制轉換為字元向量的內容,在將每個符號替換為其對應的字串後。
可以從已從其他名稱空間匯入的名稱空間匯出變數:這必須明確執行,而不是 透過 exportPattern
。
如果套件僅需要其他套件中的幾個物件,它可以在程式碼中使用完全限定的變數參照,而不是正式匯入。套件 foo 中函數 f
的完全限定參照形式為 foo::f
。這比正式匯入稍微沒那麼有效率,而且也失去了在 NAMESPACE 檔案中記錄所有依賴項的優點(但它們仍需要記錄在 DESCRIPTION 檔案中)。評估 foo::f
將導致套件 foo 被載入,但未附加,如果它尚未載入,這可能是延遲載入很少使用的套件的優點。但是,如果 foo 僅列在「Suggests」或「Enhances」中,這也延遲了檢查它是否已安裝:建議有條件地使用此類匯入(例如 via requireNamespace("foo", quietly = TRUE)
)。
當套件需要使用多個命名空間中同名的函數時,將需要使用 foo::f
形式。
使用 foo:::f
而不是 foo::f
可以存取未匯出的物件。通常不建議這樣做,因為未匯出物件的存在或語意可能會在例行維護中被套件作者變更。
S3 類型的 UseMethod
調度標準方法可能無法找到已匯入但未附加至搜尋路徑的套件中定義的方法。為確保這些方法可用,定義方法的套件應確保已匯入泛型,並使用 S3method
指令註冊方法。如果套件定義函式 print.foo
,打算用作類別 foo
的 print
方法,則指令
S3method(print, foo)
確保已註冊方法並可供 UseMethod
調度使用,且函式 print.foo
不需要匯出。由於泛型 print
定義於 base 中,因此不需要明確匯入。
(請注意,函式和類別名稱可以加上引號,保留字和非標準名稱(例如 [<-
和 function
)必須加上引號。)
可以為 S3method 指定第三個引數,作為方法使用的函式,例如
S3method(print, check_so_symbols, .print.via.format)
當不需要 print.check_so_symbols
時。
從 R 3.6.0 開始,也可以使用 S3method()
指令執行延遲註冊。使用
if(getRversion() >= "3.6.0") { S3method(pkg::gen, cls) }
函式 gen.cls
將僅在載入 pkg
的命名空間時,註冊為類別 cls
和泛型 gen
的 S3 方法。這可用於處理不需要「立即」使用的方法的情況,並且為了執行立即註冊而必須預先載入 pkg
(及其所有強依賴項)的命名空間,這被認為太過繁瑣。
在載入、附加、分離和卸載套件時,會呼叫許多掛鉤。請參閱 help(".onLoad")
以取得更多詳細資料。
由於載入和附加是不同的操作,因此會為每個操作提供個別的掛鉤。這些掛鉤函數稱為 .onLoad
和 .onAttach
。它們都接受參數67 libname
和 pkgname
;它們應該定義在命名空間中,但不要匯出。
當對套件呼叫 detach
時,套件可以使用 .onDetach
或 .Last.lib
函數(如果後者從命名空間匯出)。它會呼叫一個單一參數,即已安裝套件的完整路徑。還有一個掛鉤 .onUnload
,它會在命名空間卸載時呼叫(經由呼叫 unloadNamespace
,可能由 detach(unload = TRUE)
呼叫)參數為已安裝套件目錄的完整路徑。函數 .onUnload
和 .onDetach
應該定義在命名空間中,但不要匯出,但 .Last.lib
需要匯出。
套件不太可能需要 .onAttach
(除了可能用於啟動橫幅);設定選項和載入共用物件的程式碼應該放在 .onLoad
函數中,或使用下面說明的 useDynLib
指令。
使用者層級的掛鉤也可用:請參閱函數 setHook
的說明。
這些掛鉤通常使用不正確。人們忘記匯出 .Last.lib
。編譯的程式碼應該載入到 .onLoad
(或經由 useDynLb
指令:請參閱下方)並在 .onUnload
中卸載。請記住,套件的命名空間可以在未附加命名空間的情況下載入(例如,由 pkgname::fun
),而且套件可以在其命名空間保持載入的狀態下卸載並重新附加。
這些函數保持安靜是很好的做法。任何訊息都應該使用 packageStartupMessage
,以便使用者(包括檢查腳本)可以在需要時抑制它們。
NAMESPACE 檔案可以包含一個或多個 useDynLib
指令,允許載入需要共用物件。68 指令
useDynLib(foo)
註冊共用物件 foo
69 以 library.dynam
載入。註冊物件的載入會在套件程式碼載入後、執行載入掛勾函數前發生。只需要載入掛勾函數來載入共用物件的套件可以使用 useDynLib
指令。
useDynLib
指令也接受要透過 .C
、.Call
、.Fortran
和 .External
介面函數在 R 中使用的原生常式的名稱。這些名稱會作為指令的附加引數提供,例如:
useDynLib(foo, myRoutine, myOtherRoutine)
在 useDynLib
指令中指定這些名稱時,原生符號會在載入套件時解析,而且識別這些符號的 R 變數會以這些名稱新增到套件的命名空間。這些變數可以在 .C
、.Call
、.Fortran
和 .External
呼叫中用來取代常式的名稱和 PACKAGE
引數。例如,我們可以使用程式碼從 R 呼叫常式 myRoutine
.Call(myRoutine, x, y)
而不是
.Call("myRoutine", x, y, PACKAGE = "foo")
這種方法至少有兩個好處。首先,符號查詢僅對每個符號執行一次,而不是每次呼叫常式時執行。其次,這消除了解析可能存在於多個 DLL 中的符號的任何歧義。但是,這種方法現在已被棄用,取而代之的是提供註冊資訊(見下文)。
在某些情況下,套件中已經有一個 R 變數與原生符號同名。例如,我們可能在套件中有一個名為 myRoutine
的 R 函數。在這種情況下,有必要將原生符號對應到不同的 R 變數名稱。這可以在 useDynLib
指令中透過使用命名參數來完成。例如,要將原生符號名稱 myRoutine
對應到 R 變數 myRoutine_sym
,我們將使用
useDynLib(foo, myRoutine_sym = myRoutine, myOtherRoutine)
然後,我們可以使用指令從 R 呼叫該常式
.Call(myRoutine_sym, x, y)
沒有明確名稱的符號會指定給具有該名稱的 R 變數。
在某些情況下,最好不要在套件的命名空間中建立識別原生常式的 R 變數。如果許多這些常式不太可能被使用,則在載入套件時,為許多常式計算這些常式可能會太昂貴。在這種情況下,仍然可以使用 DLL 正確執行符號解析,但在每次呼叫常式時執行此操作。假設 DLL 作為 R 變數(例如 dll
)被引用,我們可以使用表達式呼叫常式 myRoutine
.Call(dll$myRoutine, x, y)
$
函數會使用對 getNativeSymbol
的呼叫,在 DLL 中解析具有指定名稱的常式。這是與上述相同的運算,我們在套件載入時解析符號。唯一的不同是,在 dll$myRoutine
的情況中,每次都會執行此操作。
為了使用此動態方法(例如,dll$myRoutine
),需要將 DLL 的參照作為套件中的 R 變數。可以透過使用上述用於將符號對應到 R 變數的 variable = dllName
格式,將 DLL 指定給變數。例如,如果我們想要將上述範例中 DLL foo
的 DLL 參照指定給變數 myDLL
,我們會在 NAMESPACE 檔案中使用下列指令
myDLL = useDynLib(foo, myRoutine_sym = myRoutine, myOtherRoutine)
然後,R 變數 myDLL
會在套件的命名空間中,而且可以使用呼叫(例如 myDLL$dynRoutine
)來存取在載入時未明確解析的常式。
如果套件有註冊資訊(請參閱 註冊原生常式),則我們可以直接使用它,而不用在 NAMESPACE 檔案的 useDynLib
指令中再次指定符號清單。註冊資訊中的每個常式會透過提供常式要指定的符號名稱,以及常式的位址和任何有關參數數量和類型的資訊,來指定。使用 useDynLib
的 .registration
參數,我們可以指示命名空間機制為這些符號建立 R 變數。例如,假設我們有下列針對名為 myDLL
的 DLL 的註冊資訊
static R_NativePrimitiveArgType foo_t[] = { REALSXP, INTSXP, STRSXP, LGLSXP }; static const R_CMethodDef cMethods[] = { {"foo", (DL_FUNC) &foo, 4, foo_t}, {"bar_sym", (DL_FUNC) &bar, 0}, {NULL, NULL, 0, NULL} }; static const R_CallMethodDef callMethods[] = { {"R_call_sym", (DL_FUNC) &R_call, 4}, {"R_version_sym", (DL_FUNC) &R_version, 0}, {NULL, NULL, 0} };
然後,NAMESPACE 檔案中的指令
useDynLib(myDLL, .registration = TRUE)
會導致載入 DLL,而且也會在套件的命名空間中定義 R 變數 foo
、bar_sym
、R_call_sym
和 R_version_sym
。
請注意,R 變數的名稱取自註冊資訊中的項目,不需要與原生例程的名稱相同。這允許註冊資訊的建立者將原生符號對應到 R 中不衝突的變數名稱,例如 R_version
對應到 R_version_sym
,以便在 R 函數中使用,例如
R_version <- function() { .Call(R_version_sym) }
使用參數 .fixes
可以將自動前綴新增到已註冊的符號,這在使用現有套件時很有用。例如,套件 KernSmooth 有
useDynLib(KernSmooth, .registration = TRUE, .fixes = "F_")
這會建立對應到 Fortran 符號 F_bkde
等的 R 變數,並避免與命名空間中的 R 程式碼衝突。
注意:對於未註冊原生符號的套件,使用這些參數只會減慢套件的載入速度(儘管許多 CRAN 套件都已這麼做)。一旦符號註冊後,請檢查對應的 R 變數是否不會意外地透過 NAMESPACE 檔案中的模式匯出。
舉例來說,考慮兩個名為 foo 和 bar 的套件。套件 foo 在檔案 foo.R 中的 R 程式碼為
x <- 1 f <- function(y) c(x,y) foo <- function(x) .Call("foo", x, PACKAGE="foo") print.foo <- function(x, ...) cat("<a foo>\n")
一些 C 程式碼定義一個 C 函數,編譯到 DLL foo
(具有適當的副檔名)。此套件的 NAMESPACE 檔案為
useDynLib(foo) export(f, foo) S3method(print, foo)
第二個套件 bar 擁有程式碼檔案 bar.R
c <- function(...) sum(...) g <- function(y) f(c(y, 7)) h <- function(y) y+9
以及 NAMESPACE 檔案
import(foo) export(g, h)
呼叫 library(bar)
會載入 bar 並將其外銷附加至搜尋路徑。套件 foo 也會載入,但不會附加至搜尋路徑。呼叫 g
會產生
> g(6) [1] 1 13
這與兩個設定中 c
的定義一致:在 bar 中,函式 c
定義為等同於 sum
,但在 foo 中,變數 c
參考 base 中的標準函式 c
。
對於使用正式 (S4 風格) 類別和方法的套件,需要一些額外的步驟 (除非這些純粹是內部使用)。套件應在其 DESCRIPTION 中具有 Depends: methods
以及 import(methods)
或 importFrom(methods, ...)
,加上需要外銷的任何類別和方法,需要在 NAMESPACE 檔案中宣告。例如,stats4 套件具有
export(mle) # exporting methods implicitly exports the generic importFrom("stats", approx, optim, pchisq, predict, qchisq, qnorm, spline) ## For these, we define methods or (AIC, BIC, nobs) an implicit generic: importFrom("stats", AIC, BIC, coef, confint, logLik, nobs, profile, update, vcov) exportClasses(mle, profile.mle, summary.mle) ## All methods for imported generics: exportMethods(coef, confint, logLik, plot, profile, summary, show, update, vcov) ## implicit generics which do not have any methods here export(AIC, BIC, nobs)
所有在套件外使用的 S4 類別都需要列在 exportClasses
指令中。或者,它們可以使用 exportClassPattern
70 指定,其樣式與 exportPattern
相同。若要從其他套件匯出泛型的函式,可以使用 exportMethods
指令。
請注意,在命名空間中匯出泛型函式也會匯出泛型,而在命名空間中匯出泛型也會匯出其函式。如果泛型函式不屬於此套件,可能是因為它作為泛型函式匯入,或因為非泛型版本已變成泛型,僅為了新增 S4 函式(例如上述範例中的 coef
函式),它可以透過 export
或 exportMethods
(或兩者)宣告,但後者較為清楚(且用於上述 stats4 範例中)。特別是,對於原始函式,沒有泛型函式,因此 export
會匯出原始函式,這沒有意義。另一方面,如果泛型屬於此套件,使用 export()
匯出函式本身較為自然,而且必須執行此動作,如果建立隱含泛型而未設定任何函式(例如 stats4 中的 AIC
)。
非本機泛型函式僅匯出以確保呼叫函式會從此套件調度函式(當函式為原始函式時,不會執行或不需要此動作)。因此,您不需要記錄此類隱含建立的泛型函式,而套件 tools 中的 undoc
也不會報告它們。
如果套件使用 S4 類別和從其他套件匯出的方法,但未匯入其他套件的整個命名空間71,則需要使用指令明確匯入類別和方法
importClassesFrom(package, ...) importMethodsFrom(package, ...)
分別列出類別和具有方法的函數。假設我們有兩個小型套件 A 和 B,其中 B 使用 A。然後它們可以有 NAMESPACE
檔案
export(f1, ng1) exportMethods("[") exportClasses(c1)
和
importFrom(A, ng1) importClassesFrom(A, c1) importMethodsFrom(A, f1) export(f4, f5) exportMethods(f6, "[") exportClasses(c1, c2)
分別。
請注意,importMethodsFrom
也會匯入在這些方法的命名空間中定義的任何泛型。
如果您匯出 S4 方法,則對應的泛型可用非常重要。例如,您可能需要從 stats 匯入 coef
,以顯示要轉換為其隱式泛型的函數。但建議使用 stats4 匯出的泛型,因為這允許多個套件在這些泛型上明確設定方法。
本節包含有關編寫套件的建議,以便在多個平台上使用或進行分發(例如提交到套件儲存庫,例如 CRAN)。
可攜式套件應具有簡單的檔案名稱:僅使用英數字 ASCII 字元和句點 (.
),並避免在 Windows 下不允許的名稱(請參閱 套件結構)。
許多圖形裝置是特定於平台的:即使 X11()
(又稱 x11()
)在 Windows 上模擬,但可能無法在類 Unix 系統上使用(且不是 OS X 上偏好的螢幕裝置)。套件程式碼或範例很少需要開啟新裝置,但如果必要,72 請使用 dev.new()
。
使用 R CMD build
建立發行 .tar.gz 檔案。
R CMD check
提供一組基本檢查,但當人們嘗試安裝和使用提交給 CRAN 的套件時,通常會出現更多問題 – 其中許多涉及已編譯的程式碼。以下是您可以執行的其他一些檢查,以使您的套件更具可移植性。
ifeq
等),${shell ...}
、${wildcard ...}
等,以及使用 +=
74 和 :=
。此外,除了在隱含規則中使用 $<
外,使用 $^
巨集也是 GNU 擴充功能。使用 .PHONY
也是(有些其他 make 會忽略它)。很不幸的是,使用 GNU 擴充功能的 makefile 常常會在其他平台上執行,但不會有預期的結果。
請注意,make
的 -C 旗標未包含在 POSIX 規格中,且未由某些已與 R 一起使用的 make
實作。然而,它比 GNU 特有的 --directory= 更常被實作(例如 FreeBSD make
)。
你不應依賴內建/預設的 make
規則,即使 POSIX 有指定,因為有些 make
沒有 POSIX 規則,而其他則已修改它們。
使用反引號可以避免使用 ${shell ...}
,例如
PKG_CPPFLAGS = `gsl-config --cflags`
這適用於所有已知的 make
版本75,可與 R 一起使用。
如果你真的需要 GNU make,請在 DESCRIPTION 檔案中宣告,方法如下
SystemRequirements: GNU make
並確保在腳本中使用環境變數 MAKE
的值(而不要只使用 make
)。(在某些平台上,GNU make 可用於 gmake
等名稱,並在那裡 SystemRequirements
用於設定 MAKE
。)
如果你只需要 GNU make 來處理套件中很少需要的部分(例如在 vignettes 下建立書目檔案),請使用名為 GNUmakefile 的檔案,而不是 Makefile,因為 GNU make(僅)會使用前者。
macOS 已使用 GNU make 多年(之前使用 BSD make),但版本已凍結在 3.81(從 2006 年開始)。
由於 Windows 唯一可行的 make 是 GNU make,因此可以在 Makevars.win、Makevars.ucrt、Makefile.win 或 Makefile.ucrt 檔案中使用 GNU 擴充功能。
make
。請參閱 使用 Makevars。
pkg/libpkg.a: (cd pkg && $(MAKE) -f make_pkg libpkg.a \ CXX="$(CXX)" CXXFLAGS="$(CXXFLAGS) $(CXXPICFLAGS) $(C_VISIBILITY)" \ AR="$(AR)" RANLIB="$(RANLIB)")
R CMD build
執行,例如在 cleanup
腳本或「clean」目標中。
R CMD config
,這需要類似於(對於 C++)
RBIN = `"${R_HOME}/bin/R"` CXX = `"${RBIN}" CMD config CXX` CXXFLAGS = `"${RBIN}" CMD config CXXFLAGS` `"${RBIN}" CMD config CXXPICFLAGS`
make
程式感到混淆,應避免使用。
ash
(https://en.wikipedia.org/wiki/Almquist_shell,一種「內建函式較少的輕量級殼層」)或衍生產品,例如 dash
。請注意,不要假設所有 POSIX 命令列公用程式都可用,特別是在 Windows 上,因為只提供一個子集(已隨著 Rtools 版本而變更)供與 R 搭配使用。一個特別的問題是使用 echo
,它允許兩種行為(https://pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html),而且兩者都已在 R 平台上預設發生:可攜式應用程式不應使用 -n(作為第一個引數)或跳脫序列。建議用 printf
命令取代 echo -n
。另一個常見問題是結構
export FOO=value
這是 bash
特有的(先設定變數,再依名稱匯出)。
在 shell 指令碼中使用 test -e
(或 [ -e ]
)並非完全可移植78:通常要使用 -f
。標記 -a 和 -o 現今已由 POSIX 宣告為過時,不應使用。
使用「大括號展開」,例如
rm -f src/*.{o,so,d}
不可移植。
在 shell 指令碼中,set
的 -o 標記在 POSIX 中是選擇性的,且不支援 R 使用的所有平台。
變數「OSTYPE」是 shell 特有的,其值相當不可預測,且可能包含版本,例如「darwin19.0」:通常要使用 `uname`
(常見值為「Darwin」、「Linux」和「SunOS」)。
在 macOS 上,shell /bin/sh 呼叫的是使用者和平台相關的:可能是 bash
版本 3.2、dash
或 zsh
(新帳戶為 zsh
,從 Mojave 或更早版本移植的帳戶通常為 bash
)。
gcc
、clang
和 gfortran
79 可以搭配選項 -Wall -pedantic 使用,以提醒您潛在問題。這對 C++ 來說特別重要,因為 g++ -Wall -pedantic
會提醒您使用某些 GNU 擴充功能,這些功能在其他大多數 C++ 編譯器上無法編譯。如果 R 沒有適當地設定,可以透過個人 Makevars 檔案來達成此目的。請參閱 R 安裝和管理 中的 自訂套件編譯,
可攜式的 C++ 程式碼需要遵循 2011、2014 和 2017 年的全部標準(包括不使用已棄用/已移除的功能),或在可用的地方指定 C+11/14/17/20/23(並非所有 R 平台都適用)。目前,C++20 支援在 R 平台上並不完整。
如果使用 GNU 編譯器搭配 Fortran,請使用旗標 -std=f95 -Wall -pedantic,它會拒絕大多數 GNU 擴充功能和後續標準中的功能。(儘管 R 只需要 Fortran 90,gfortran
沒有辦法指定該標準。)另外,請考慮 -std=f2008,因為某些最近的編譯器將 Fortran 2008 甚至 2018 作為最低支援的標準。
並非所有常見的 R 平台都符合預期的標準,例如 C 程式碼的 C99。常見的問題領域之一是 *printf
函數,其中 Windows 不支援 %lld
、%Lf
和類似的格式(且有其自己的格式,例如 64 位元整數的 %I64d
)。輸出此類型的需求非常罕見,且 64 位元整數通常可轉換為雙精度浮點數以進行輸出。然而,C11 標準(第 7.8.1 節)包含 C 標頭 inttypes.h 中的 PRIxNN
巨集80(例如 PRId64
),因此可攜式的方法是測試這些巨集,若不可用則在套件中提供模擬。
從 macOS 11(2020 年底)開始,其 C 編譯器預設設定旗標 -Werror=implicit-function-declaration,強制更嚴格地符合 C99。這可以用於其他平台,搭配 gcc
或 clang
。如果您的套件有(autoconf
產生的)configure script
,請嘗試在使用此旗標時安裝它,並通讀 config.log 檔案 — 編譯警告和錯誤可能會導致無法偵測到現有的功能。(如果可能,請在多個平台上執行此操作。)
R CMD check
會在 src/Makevars 中執行一些非可攜式編譯器/連結器旗標檢查。然而,它無法檢查此類旗標的意義,且有些旗標通常被接受,但具有編譯器特定的意義。還有其他未檢查的非可攜式旗標,以及 src/Makefile 檔案和子目錄中的 makefile。程式碼中的註解說明
除了 -I* 和 -D* 之外,很難想到任何對一般用途來說是安全的…
儘管 -pthread 非常接近可攜式。(選項 -U 是可攜式的,但在命令列上幾乎沒有用,因為它只會取消內建定義(不可攜式)和命令列上先前定義的定義(R 沒有使用任何定義)。)
GBU 選項 -pipe 過去廣泛為 C/C++/Fortran 編譯器所接受,但在 flang-new
18 中已被移除。無論如何,不應在分散式程式碼中使用,因為它可能導致過度使用記憶體。
人們已使用 configure
來自訂 src/Makevars,包括特定編譯器。這由於數個原因而不安全。首先,非預期的編譯器可能會符合檢查,例如,除了 GCC 以外的數個編譯器將自身標示為「GCC」,但僅部分相符。其次,編譯器的未來版本可能會表現不同(包括相當舊系列的更新),因此,例如 -Werror(及專門化)可能會使套件在未來版本中無法安裝。第三,使用標記來抑制診斷訊息可能會隱藏套件維護者未測試平台上進行偵錯的重要資訊。(R CMD check
可選擇報告所使用的非安全標記。)
避免使用 -march,特別是 -march=native。這允許編譯器產生僅在特定類別的 CPU(「native」的編譯機器)上執行的程式碼。人們假設這是「最小」的 CPU 規格,但這並非 gcc
的文件記載方式(clang
接受它,但顯然未記載它精確執行的動作,且其他編譯器可能會接受並忽略它)。(對於個人使用,-mtune 較安全,但仍不足以移植到公開套件中使用。)甚至 gcc
也並非對所有 CPU 都支援「native」,如果它發現比其版本更新的 CPU,可能會執行令人驚訝的動作。
long
將為 32 位元,但在大多數現代 Unix 和 Linux 平臺上將為 64 位元。C 程式碼中 long
的使用不太可能經過深思熟慮:如果您需要比 int
更長的類型,您應該對 C99/C++11 類型(例如 int_fast64_t
,如果失敗,則為 long long
)執行組態測試並定義您自己的類型,或使用其他適當的類型(例如 size_t
,但請注意它是未簽署的,而 ssize_t
不可移植)。
假設 long
和指標類型大小相同並不安全,而且它們在 64 位元 Windows 上也不相同。如果您需要將指標轉換為整數或從整數轉換為指標,請使用 C99/C++11 整數類型 intptr_t
和 uintptr_t
(在標頭 <stdint.h>
和 <cstdint>
中:標準不需要實作它們,但 R 本身在 C 程式碼中使用它們)。
請注意,在所有 R 平臺上,Fortran 中的 integer
對應於 C 中的 int
。Fortran logical
沒有這樣的保證,最近的 gfortran
在大多數平臺上將其對應到 int_least32_t
。
abort
或 exit
81:這些會終止使用者的 R 程序,很可能會遺失所有未儲存的工作。一個可能呼叫 abort
的用法是 C 或 C++ 函數中的 assert
巨集,這在生產程式碼中永遠不應處於活動狀態。確保這一點的正常方式是定義巨集 NDEBUG
,而 R CMD INSTALL
在編譯旗標中會這樣做。小心包含標頭(包括來自其他套件的標頭),這些標頭現在或在未來版本中可能會取消定義它。如果您希望在開發期間使用 assert
。您可以在 PKG_CPPFLAGS
中包含 -UNDEBUG
或在標頭或程式碼檔案中 #undef 它。請注意,您自己的 src/Makefile 或子目錄中的 makefile 可能也需要定義 NDEBUG
。
這不僅適用於您自己的程式碼,也適用於您編譯或連結的任何外部軟體。
rand
、drand48
和 random
82,而應使用 亂數產生 中描述的 R RNG 介面。特別是,如果多個套件初始化一個系統 RNG(例如透過 srand
),它們將會互相干擾。這也適用於 Fortran 90 的 random_number
和 random_seed
,以及 Fortran 2018 的 random_init
。以及 GNU Fortran 的 rand
、irand
和 srand
。除了 drand48
之外,這些函數使用的 PRNG 取決於實作。
C++11 亂數函式庫或其他第三方亂數產生器(例如 GSL 中的那些)也不應使用。
sprintf
和 vsprintf
被視為潛在的安全風險,並在某些平台上發出警告。83 如果找到任何呼叫,R CMD check
會報告。
nm -pg mypkg.so
並檢查任何標記為 U
的符號是否意外,是避免此問題的好方法。
libz
(特別是那些已經連結到 R 的副本)。在這個案例中,進入點 gzgets
有時會針對編譯到套件中的舊版本解析,有時會針對編譯到 R 中的副本解析,有時會針對系統動態函式庫解析。唯一的安全解決方案是重新命名套件中副本中的進入點。我們甚至看過進入點名稱 myprintf
的問題,這是某些 Linux 系統上的系統進入點84。
相關問題是作為套件安裝一部分而建置的函式庫的命名。macOS 和 Windows 具有不區分大小寫的檔案系統,因此使用
-L. -lLZ4
在 PKG_LIBS
中會比對 liblz4
。而 -L.
只會附加到搜尋位置清單,而 liblz4
可能會在較早搜尋的位置中找到(而且已經找到)。唯一安全的方法是提供明確路徑,例如
./libLZ4.a
nm -pg
檢查),並使用與套件明確相關的名稱(如果發生任何問題,這也有助於使用者)。請注意,以 R_
開頭的符號名稱被視為 R 名稱空間的一部分,不應在套件中使用。
R_init_pkgname
85,這將完全避免符號衝突。
.Internal
、.C
、.Fortran
、.Call
或 .External
在 R 或其他套件中呼叫已編譯的程式碼並非可移植的,因為此類介面可能會在沒有通知的情況下變更,並且可能會導致您的程式碼終止 R 程序。
R CMD build
會用副本取代它們。
library
、require
或 attach
,而且這通常無法如預期般運作。有關替代方案,請參閱 建議的套件 和 with()
。
example
,也可以在檢查時以批次模式執行。因此,它們在兩種情況下都應該有適當的行為,透過 interactive()
條件化需要運算子或觀察者的部分。例如,進度條86 僅適用於互動式使用,如同顯示說明頁面或呼叫 View()
(請參閱下方)。
PKG_LIBS
等巨集中條目的順序。有些連結器會重新排列條目,而且動態和靜態函式庫的行為可能有所不同。通常 -L 選項應先於87 從這些目錄中找到的函式庫(通常由 -l 選項指定),而且函式庫會依指定順序搜尋一次。並非所有連結器都允許 -L 後面有空格。
LinkingTo
時需要小心。這會將一個或多個目錄放在包含搜尋路徑中,在系統標頭之前,但(在 R 3.4.0 之前)在 R 建置的 CPPFLAGS
巨集中指定的標頭之後(通常包含 -I/usr/local/include
,但大多數平台會忽略它並將其包含在系統標頭中)。
在以套件命名的目錄中擁有 LinkingTo
標頭,可以避免任何混淆。在任何情況下,都應避免套件 include 目錄下的標頭和目錄名稱衝突,無論是在套件之間,還是在套件與系統和第三方軟體之間。
ar
工具通常用於建立靜態函式庫。它的修改器 u
由 POSIX 定義,但在某些使用「確定模式」的 Linux distribution 中,GNU ar
中已停用。建立靜態函式庫最安全的方法是先移除任何現有同名檔案,然後使用 $(AR) -cr
,再視需要使用 $(RANLIB)
(這取決於系統:在大部分系統中88 ar
始終會維護符號表)。POSIX 標準指出選項應以連字元開頭(如 -cr),儘管大部分作業系統都接受不帶連字元的選項。請注意,在某些系統中,ar -cr
必須至少指定一個檔案。
X/OPEN 要求使用 s
修改器(以取代對 ranlib
的個別呼叫),但 POSIX 沒有此要求,因此 ar -crs
無法移植。
為了可移植性,應始終使用 AR
和 RANLIB
巨集 – 有些建置需要包裝器,例如 gcc-ar
或額外的引數來指定外掛程式。
strip
工具具有平台專屬性(而 CRAN 禁止移除偵錯符號)。例如,GNU 版本的 --strip-debug 和 --strip-unneeded 選項不支援 macOS:strip
的 POSIX 標準未提及任何選項,而未指定選項時呼叫它的行為取決於平台。移除 .so 檔案甚至可能導致它無法在未測試的平台上動態載入到 R 中。
ld -S
會呼叫 GNU ld
的 strip --strip-debug
(macOS 中的行為類似),但無法移植:特別是在 Solaris 中,它的行為完全不同,而且需要引數。
pandoc
失敗,並顯示令人費解的錯誤訊息。
非 ASCII 檔案名稱也可能造成問題(特別是在非 UTF-8 地區設定中)。
在指定 Java 最低版本時,請使用官方版本名稱,這些名稱(令人困惑地)是
1.1 1.2 1.3 1.4 5.0 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
而從 2018 年起,也開始使用「18.9」這種年.月格式。幸運的是,通常只有整數值會是相關的。如果可能的話,請使用其中一個 LTS 版本 (8、11、17、21 …) 作為最低版本。建議的版本規範形式為
SystemRequirements: Java (>= 11)
對於使用 rJava 套件的 Java 至少 8 版,一個合適的測試會類似於
.jinit() jv <- .jcall("java/lang/System", "S", "getProperty", "java.runtime.version") if(substr(jv, 1L, 2L) == "1.") { jvn <- as.numeric(paste0(strsplit(jv, "[.]")[[1L]][1:2], collapse = ".")) if(jvn < 1.8) stop("Java >= 8 is needed for this package but not available") }
Java 9 變更了這個字串的格式(過去類似於「1.8.0_292-b10」);Java 11 將 jv
指定為「11+28」,而 Java 11.0.11 則指定為「11.0.11+9」。(https://openjdk.org:443/jeps/322 詳細說明了目前的格式。請注意,必須允許「11-ea+22」等預先發布版本。)
另外請注意,用於產生 jar
的編譯器可能會強制執行最低 Java 版本,通常會產生類似於
java.lang.UnsupportedClassVersionError: ... Unsupported major.minor version 52.0
(其中 https://en.wikipedia.org/wiki/Java_class_file 將類別檔案版本號碼對應到 Java 版本。)請使用類似於 javac -target 11
的指令進行編譯,以確保避免此問題。請注意,這也適用於散布(甚至是下載)他人產生的已編譯 Java 程式碼的套件,因此需要檢查其需求(通常未準確記錄)並加以考量。如有必要,應該可以透過命令列公用程式 javap
檢查類別檔案版本,在從 .jar 檔案中提取 .class 檔案後執行。例如,
jar xvf some.jar javap -verbose path/to/some.class | grep major
有些套件已聲明對特定 JDK 的需求,但套件應該只要求 JRE,除非它提供了自己的 Java 介面。
Java 8 仍廣泛使用(而且可能會持續使用,因為授權變更和對較舊作業系統的支援:OpenJDK 的安全性支援將持續到 2026 年 3 月)。另一方面,較新的平台可能只支援較新的 Java 版本:對於「arm64」macOS,第一個官方支援的版本是 17。
pandoc
,它僅適用於極少數的平台(而且從原始碼安裝的要求很繁瑣),而且功能90因建置而異,但未記載於文件中。macOS 的幾個最新版本 pandoc
無法在 R 當時鎖定的 High Sierra 上執行(這點也未記載於文件中)。另一個範例是 Rust 編譯系統(cargo
和 rustc
)。
使用外部指令時,應一律有條件地測試是否存在(或許使用 Sys.which
),並在「SystemRequirements」欄位中宣告。套件應在未存在外部指令的情況下通過檢查,且無警告或錯誤。
外部指令可能是已匯入或建議套件的(可選)需求,但套件本身的範例、測試或範例需要使用。此類用法應一律宣告且有條件。
Perl、Python 和 Ruby 等腳本語言的直譯器需要宣告為系統需求並有條件地使用:例如,macOS 10.16 已宣布不包含這些直譯器(但以 macOS 11 的形式發布,並包含這些直譯器);之後宣布 macOS 12.3 不包含 Python 2,且僅包含 Python 3 的最小安裝。Python 2 已通過使用期限並從許多主要發行版中移除。無法假設支援 Rust 或 Go。
指令 cmake
通常未安裝,即使已安裝,也可能不在路徑中。特別是,macOS 上最常見的位置是 /Applications/CMake.app/Contents/bin/cmake,如果在路徑中找不到 cmake
,應尋找此位置。
utf8
、mac
和 macroman
皆不可移植。請參閱 file
的說明,以取得更多詳細資料。
R
、Rscript
或(在 Windows 中)Rterm
呼叫 R。如本手冊前面幾個地方所指出的,請使用類似
"$(R_HOME)/bin/Rscript" "$(R_HOME)/bin$(R_ARCH_BIN)/Rterm"
加上適當的引號(例如,雖然不建議,但 R_HOME
可以包含空格)。
R_HOME
,除非傳遞它們到 shell。具體來說,不要在 include
的參數中使用 R_HOME
,因為 R_HOME
可以包含空格。加上引號到 include
的參數中沒有幫助。避免 ${R_HOME}
中空格問題的便攜式且推薦的方式是使用 make
的選項 -f
。這很容易透過遞迴呼叫 make
來執行,這也是在 include
的參數中需要 R_HOME
的唯一常見情況。
$(MAKE) -f "${R_HOME}/etc${R_ARCH}/Makeconf" -f Makefile.inner
"POSIXct"
的物件,因為它們會以 UTC 記錄時間,所代表的時間與時區無關:但列印方式可能有關。類別 "POSIXlt"
的物件應該有一個 "tzone"
屬性。日期(例如生日)通常被視為與時區無關。
請小心你的測試(和範例)實際上測試了什麼。在散布式套件中看到的壞習慣包括
套件甚至測試了系統錯誤訊息的精確格式,這些訊息取決於平台,可能也取決於地區設定。例如,在 2021 年底,libcurl
變更了其警告/錯誤訊息,包括找不到 URL 時。
View
等函數,請記住,在測試中沒有人會查看輸出。最好使用以下其中一種:
if(interactive()) View(obj) else print(head(obj)) if(interactive()) View(obj) else str(obj)
?normalizePath
以了解陷阱。
clang
目前在所有 ARM CPU 上將長雙精度與雙精度設定為相同。另一方面,有些 CPU 具有較高精度的模式,可用於 長雙精度
,特別是 64 位元組 PowerPC 和 Sparc。
如果您必須嘗試根據經驗建立容忍度,請使用 --disable-long-double 設定和建置 R,並使用適當的編譯器旗標(例如 -ffloat-store 和 -fexcess-precision=standard,適用於 gcc
,視 CPU 類型而定92),以減輕延伸精度計算的影響。最常被發現會產生不同數值結果的平台是「arm64」macOS,因此請務必將其包含在任何經驗判定中。
涉及隨機輸入或非決定性演算法的測試,通常應設定種子或針對多個種子進行測試。
options(warn = 1)
作為報告
There were 22 warnings (use warnings() to see them)
是沒有意義的,特別是對於自動化檢查系統而言。
有許多工具可供縮小 PDF 檔案大小:通常可以大幅縮小檔案大小,品質幾乎沒有損失或損失極小。大型檔案不僅佔用空間:它們會對 PDF 檢視器造成負擔,而且列印時可能需要花費好幾分鐘(如果它們可以列印的話)。
qpdf
(https://qpdf.sourceforge.io/) 可以無失真壓縮。它相當容易取得(例如它有 Windows 的二進制檔和 Debian/Ubuntu/Fedora 的套件,並作為 R 的 CRAN macOS 發行版的一部分安裝)。R CMD build
有個選項可以在 inst/doc 下對 PDF 檔案執行 qpdf
,如果節省至少 10Kb 和 10%,就取代它們。可以使用環境變數 R_QPDF
提供 qpdf
命令的完整路徑(並在 R 的 CRAN macOS 二進制檔中)。MiKTeX 似乎不使用 PDF 物件壓縮,因此 qpdf
可以大幅縮小其輸出的檔案大小:MiKTeX 的預設值可以透過 Sweave 或 LaTeX 檔案的前言中的程式碼覆寫 — 參閱 R 參考手冊在 https://svn.r-project.org/R/trunk/doc/manual/refman.top 中是如何執行的。
其他工具可以縮小含有過高解析度點陣圖影像的 PDF 檔案大小。這些影像通常最好重新產生(例如 Sweave
預設為 300 ppi,而 100–150 比較適合套件手冊)。這些工具包括 Adobe Acrobat(不是 Reader)、Apple 的 Preview93 和 Ghostscript(透過
ps2pdf options -dAutoRotatePages=/None -dPrinted=false in.pdf out.pdf
而適當的選項可能是
-dPDFSETTINGS=/ebook -dPDFSETTINGS=/screen
請參閱 https://ghostscript.readthedocs.io/en/latest/VectorDevices.html 以取得更多資訊,並考量所有影像降採樣選項)。CRAN 套件中有一些範例,而 Ghostscript 的目前版本所產生的縮減幅度遠大於早期版本(例如從 9.50
升級到 9.52
、從 9.55
升級到 9.56
,再升級到 10.00.0
)。
我們偶爾會遇到包含使用 PDF 向量圖形過於複雜圖形的超大型 PDF 檔:此類圖形通常最好重新設計,或若無法重新設計,則輸出為 PNG 檔。
選項 --compact-vignettes 至 R CMD build
的預設值為 ‘qpdf’:若要盡可能縮小檔案大小,請使用 ‘both’,前提是您有 Ghostscript 可用(請參閱 tools::compactPDF
的說明)。
有數種方法可找出檢查程序中花費時間的地方。首先將環境變數 _R_CHECK_TIMINGS_
設為 ‘0’。這將報告總 CPU 時間(非 Windows)和安裝、執行範例、測試和範例的經過時間,若適當,則會在每個子架構下報告。對於測試和範例,它會報告每個測試和範例的時間,以及總時間。
將 _R_CHECK_TIMINGS_
設定為正值會設定一個閾值(以秒為單位經過時間)以報告計時。
如果您需要更詳細地查看範例的計時,請使用選項 --timings 至 R CMD check
(這由 --as-cran 設定)。這會為 CPU 或經過時間超過 5 秒的所有範例新增摘要至檢查輸出。它會產生一個包含每個說明檔案計時的檔案 mypkg.Rcheck/mypkg-Ex.timings:這是一個可以讀入 R 以進行進一步分析的 tab 分隔檔案。
測試和範例執行摘要會提供在對應日誌檔案的底部:請注意,只有在環境變數 _R_CHECK_ALWAYS_LOG_VIGNETTE_OUTPUT_
設定為 true 值時,才會保留成功範例執行的日誌檔案。
此小節中的問題已經透過 R 4.2.0 中的變更大幅改善,在可用的地方以 UTF-8 區域設定執行 R 的 Windows 埠。然而,Windows 使用者可能會在較早版本的 Windows 上執行較早版本的 R,而不支援 UTF-8 區域設定。
如果您的套件包含非 ASCII 文字,則需要小心,特別是在打算在多個區域設定中使用時。可以在 DESCRIPTION 檔案和 .Rd 檔案中標記所使用的編碼,如本手冊的其他地方所討論。
首先,仔細考慮你是否真的需要非ASCII文字。有些 R 使用者只能正確查看其母語群組(例如西歐、東歐、簡體中文)和ASCII的文字。94。其他字元可能根本無法呈現、呈現不正確,或導致你的 R 程式碼產生錯誤。對於.Rd文件,標記編碼並包含ASCII音譯可能可以做得很好。普遍支援的字元集比 2000 年左右更廣泛,但非拉丁字母(希臘文、俄文、喬治亞文等)仍然經常有問題,而那些具有雙寬字元(中文、日文、韓文、表情符號)的字元通常需要專業字型才能正確呈現。
幾個CRAN套件在其 R 程式碼中有法文訊息(少數有德文)。要解決這個問題,更好的方法是使用本手冊其他地方討論的國際化功能。
套件tools中的函式showNonASCIIfile
可以協助在檔案中尋找非ASCII位元組。
在 R 程式碼中,有一個可攜式的方法可以在字元串(僅限)中放入任意文字,就是以 Unicode 的形式提供,例如「\uxxxx」跳脫字元(或較少需要,除了表情符號之外,「\Uxxxxxxxx」跳脫字元)。如果目前編碼中沒有任何字元,剖析器會將字元串編碼為 UTF-8,並標記為 UTF-8。這也適用於資料集中的字元串:它們可以使用「\uxxxx」跳脫字元準備,或在 UTF-8 區域設定中以 UTF-8 編碼,甚至可以透過 iconv()
轉換為 UTF-8。如果您這樣做,請確定您在 DESCRIPTION 檔案的「Depends」欄位中具有「R (>= 2.10)」(或更新版本)。
在非 UTF-8 區域設定中執行的 R 工作階段,如果可能,會重新編碼此類字串以顯示(例如,較舊版本的 Windows 中的 RGui
會執行此動作)。主控台/終端機和「X11()」與「windows()」等圖形裝置都需要選取或提供合適的字型95。使用「postscript」或「pdf」會根據 UTF-8 區域設定的語言選擇預設的 8 位元編碼,而您的使用者需要知道如何選取「encoding」引數。
請注意,前兩段僅適用於 R 程式碼中的字元字串。非 ASCII 字元特別容易出現在註解中(套件的 R 程式碼、範例、測試、範例說明,甚至在 NAMESPACE 檔案中),但應避免在這些地方出現。最常見的情況是人們使用拉丁文-1 的 Windows 延伸字元(通常是單引號和雙引號、省略號、項目符號以及連字號和破折號),這些字元在嚴格的拉丁文-1 區域設定或 Windows 上的 CJK 區域設定中不受支援。一個令人意外的常見錯誤是用法是在「don't」中使用右引號,而不是正確的撇號。
資料集可以包含標記的 UTF-8 或拉丁文-1 字元字串。由於 R 現在不太可能在拉丁文-1 或 Windows 的 CP1252 區域設定中執行,因此基於效能考量,應將這些字元字串轉換為 UTF-8。
如果您要在類 Unix 系統上對套件執行 R CMD check
,而該套件在其 DESCRIPTION 檔案中設定套件編碼 且未使用 UTF-8 區域設定,您可能需要 透過 環境變數 R_ENCODING_LOCALES
指定適當的區域設定。預設值等於值
"latin1=en_US:latin2=pl_PL:UTF-8=en_US.UTF-8:latin9=fr_FR.iso885915@euro"
(這適用於基於 glibc
的系統:macOS 需要 latin9=fr_FR.ISO8859-15
),但如果目前的區域設定是 UTF-8,套件程式碼會轉譯為 UTF-8 以進行語法檢查,因此強烈建議在 UTF-8 區域設定中檢查。
撰寫可攜式 C 和 C++ 程式碼主要是遵守標準(C99、C++14 或宣告的 C++11/17/20)並測試是否支援擴充功能(例如 POSIX 函式)。請充分利用編譯器診斷功能,這通常表示對 C 和 C++ 使用標記 -Wall 和 -pedantic,並對 C 使用 -Werror=implicit-function-declaration 和 -Wstrict-prototypes(在某些平台和編譯器版本中,這些標記是 -Wall 或 -pedantic 的一部分)。
C++ 標準:從 3.6.0 版(Windows 上為 3.6.2 版)開始,R 預設為 C++11(如果可用)96;從 R 4.1.0 版開始為 C++14,從 R 4.3.0 版開始為 C++17(如果可用)。然而,在較早的版本中,預設標準是所用編譯器的標準,通常為 C++98 或 C++14,而且預設標準可能會在未來更改。為了最大程度地提高可攜性,套件應指定標準(請參閱 使用 C++ 程式碼)或在 C++11、C++98、C++14 和 C++17 下進行測試。(指定 C++14 或更新版本會限制可攜性。)
請注意,「TR1」C++ 擴充功能不屬於任何這些標準,而且某些用於 R 的編譯器(包括 macOS 上的編譯器)未提供 <tr1/name>
標頭。(請改用 C++11 版本。)
一個常見的錯誤是假設編譯器或作業系統的版本較新。在生產環境中,「長期支援」版本的作業系統可能會使用多年,97而且其編譯器在那段時間內可能不會更新。例如,GCC 4.8 在 2022 年仍在使用,並且可以在(RHEL 7 中)使用到 2028 年:這既不支援 C++14 也不支援 C++17。
POSIX 標準僅要求宣告最近定義的函式,如果某些巨集定義為足夠大的值,而且在某些編譯器/作業系統組合中,98否則不會宣告函式。因此,您可能需要包含類似下列其中一個的內容
#define _XOPEN_SOURCE 600
或
#ifdef __GLIBC__ # define _POSIX_C_SOURCE 200809L #endif
在任何標頭之前 (strdup
、strncasecmp
和 strnlen
是此類函式 - 有幾個較舊的平台沒有 POSIX 2008 函式 strnlen
。)
不過,有些常見錯誤值得在此指出。在 https://cplusplus.com/reference/ 或 https://en.cppreference.com/w/ 查詢函式並比較各種標準中定義的內容會很有幫助。
對於未由任何這些標準指定的函式(例如 mallinfo
),需要更小心 - 希望系統上的 man
頁面會告訴您。在線上搜尋各種作業系統的此類頁面(最好至少包含 Linux 和 macOS,以及 https://man.freebsd.org/cgi/man.cgi 上的 FreeBSD 手冊頁面,讓您能選擇許多作業系統)應會揭露有用的資訊,但可能需要 configure 指令碼來檢查可用性和功能性。
編譯器和作業系統(透過系統標頭檔,即使對於名義上相同的作業系統,也會因架構而異)都會影響 C/C++ 程式碼的可編譯性。GCC、LLVM(clang
和 flang
)、Intel 和 Oracle Developer Studio 套件的編譯器已與 R 搭配使用,LLVM clang
和 Oracle 都有一個以上的 C++ 標頭和函式庫實作。各種可能性使得全面的經驗檢查不可能,而且遺憾的是,編譯器在警告非標準程式碼方面充其量只是零星。
sqrt
)在 C++11 中定義為浮點數引數:float
、double
、long double
,可能還有更多。標準指定了整數型引數會發生什麼情況,但這並不總是實作的,導致報告「重載歧義」:這在 Solaris 上很常見,但對於 pow
也出現在 macOS 和使用 clang++
的其他平台上。
一個並不少見的問題是錯誤地呼叫 floor(x/y)
或 ceil(x/y)
對於 int
參數 x
和 y
。由於 x/y
執行整數除法,結果的類型為 int
,且可能回報「重載歧義」。有些人(徒勞無功地)呼叫 floor
和 ceil
針對整數類型的參數,這可能會產生「重載歧義」。
一個令人驚訝的常見誤用是類似 pow(10, -3)
的東西:這應該是常數 1e-3
。請注意,有常數(例如 M_SQRT2
)定義 via Rmath.h99 針對 sqrt(2.0)
,經常錯誤編碼為 sqrt(2)
。
fabs
僅定義於浮點類型,但 C++11 及更新版本例外,它們在 <cmath> 中針對 std::fabs
有針對整數類型的重載。函數 abs
定義於 C99 的 <stdlib.h> 針對 int
,以及 C++ 的 <cstdlib> 針對整數類型,在 <cmath> 中針對浮點類型重載。C++11 在 <cmath> 中針對 std::abs
有針對整數類型的額外重載。呼叫 abs
搭配浮點類型的效果取決於實作:它可能會截斷為整數。為了清楚起見並避免編譯器警告,請對整數類型使用 abs
,對雙精度值使用 fabs
,且在使用 C++ 時包含 <cmath> 並使用 std::
前綴。
isnan
、isinf
和 isfinite
對於整數引數來說是個錯誤(而且不太有意義,儘管已經看過),一些編譯器會給出編譯錯誤。函式 finite
已過時,一些編譯器會警告其使用100。
INFINITY
(在 C99 和 C++11 中是一個 浮點 值),R 為其提供可攜式雙精度值 R_PosInf
(以及 R_NegInf
表示 -INFINITY
)。NAN
101 僅僅是一個 NaN 浮點 值:對於 R 的使用,NA_REAL
通常是預期的,但 R_NaN
也可用。
一些(但不是全部)擴充列於 https://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html 和 https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Extensions.html。
其他讓套件撰寫者感到困擾的 GNU 擴充是使用非可攜式字元,例如識別碼中的 ‘$’ 和在 ext 下使用 C++ 標頭。
項目在 C++ 程式碼中包含 C 式標頭並非可攜式。在 C++ 程式碼中包含舊式標頭102 math.h 可能會與 cmath 衝突,而後者可能由其他標頭包含。在 C++11 中,例如 sqrt
和 isnan
的函式定義於 math.h 中的 double
參數,以及 cmath 中包括 double
在內的範圍類型。對於 stdlib.h 和 cstdlib 也看過類似的問題。優先包含 C++ 標頭曾經是足夠的解決方法,但對於某些 2016 編譯器,只能包含其中一個。
<random>
,它是由 g++
間接包含在 <algorithm>
中。另一個問題是 C 標頭 <time.h>
,它在 Linux 和 Windows 上由其他標頭包含,但在 macOS 上則不然。)g++
11 通常需要明確包含 C++ 標頭 <limits>
(對於 numeric_limits
)或 <exception>
(對於 set_terminate
和類似函式),而較早版本則將這些包含在其他標頭中。g++
13 需要明確包含 <cstdint>
以取得類型,例如 uint32_t
,而這以前是隱含包含的。(更多此類資訊,請參閱 https://gcc.gnu.org/gcc-13/porting_to.html。)
請注意,malloc
、calloc
、realloc
和 free
由 C99 在標頭 stdlib.h 中定義,並由 C++ 標頭 cstdlib(在 std::
命名空間中)定義。一些較早的實作使用標頭 malloc.h,但這不可移植,且在 macOS 上不存在。
這也適用於 ssize_t
等類型。POSIX 標準表示在標頭 unistd.h
和 sys/types.h
中宣告,後者通常會在某些系統(但並非所有系統)中由其他標頭間接包含。
常數也類似:例如 SIZE_MAX
與 size_t
一起在 stdint.h
中定義。
glibc
延伸:有些作業系統有 machine/endian.h 或 sys/endian.h,但有些作業系統兩個都沒有。
#include "my.h"
,而非 #include <my.h>
。第二種形式是針對系統標頭,而且此類標頭的搜尋順序與平台相關(且可能不包含目前目錄)。為了更安全,請以不會與系統標頭混淆的方式命名標頭,因此不要使用 types.h 等。
std
命名空間中,但 g++
也將許多此類函式放入 C++ 主命名空間中。一種執行此動作的方式是使用宣告,例如
using std::floor;
但通常較好的是在程式碼中使用明確的命名空間前置詞。
CRAN 套件中看到的範例包括
abs acos atan bind calloc ceil div exp fabs floor fmod free log malloc memcpy memset pow printf qsort round sin sprintf sqrt strcmp strcpy strerror strlen strncmp strtol tan trunc
這個問題比以前少見,但 2019 年的 LLVM clang
在主命名空間中沒有 bind
。也曾見過僅在 std
命名空間中定義的類型 size_t
。
注意:這些函式僅保證在包含正確的 C++ 標頭時才會出現在 std
命名空間中,例如 <cmath>
而不是 <math.h>
。
如果您在 C++ 中定義受後續標準啟發的函式,請將它們放在命名空間中,並使用命名空間來參照它們。我們已經看到 C++14 中的 std::make_unique
與 C++17 中的 std::byte
、std::data
、std::sample
和 std::size
發生衝突。
using namesapace std;
並非良好的實務,且如果在標頭之前包含,特別是系統標頭(可能由其他標頭包含),則會導致與平台相關的錯誤。最佳實務是對 C++ 標準宣告在該命名空間中的所有函式使用明確的 std::
前置詞。
if(ptr > 0) { ....}
的建構,它將指標與整數 0
進行比較。這可以使用 if(ptr)
(指標位址不能為負值)來使用,但如果需要,可以針對 nullptr
(C++11)或 NULL
測試指標。
_POSIX_C_SOURCE
來避免,但最好只使用具有唯一前置詞(例如套件名稱)的全大寫名稱。
typedef
可能會與套件中的 typedef
衝突:範例包括 ulong
、index_t
、single
和 thread
。(請注意,這些可能會與其他用途作為識別碼衝突,例如定義稱為 single
的 C++ 函數。)POSIX 標準(在 §2.2.2 中)保留所有以 _t
結尾的識別碼。
-D
和要定義的巨集之間有空格。-U
也類似。
#ifdef _OPENMP # include <omp.h> #endif
任何使用 OpenMP 函數(例如 omp_set_num_threads
)也需要加上條件。為避免不斷出現以下警告,例如
warning: ignoring #pragma omp parallel [-Wunknown-pragmas]
此類指令的用途也應加上條件(或是在套件中不啟用任何平台上的 OpenMP 的程式碼中將其註解掉)。
不要硬編碼 -lgomp:這不僅限於 GCC 編譯器系列,使用正確的連結器旗標通常會設定執行時期路徑至函式庫。
strdup
。Linux 上最常見的 C 函式庫 glibc
會隱藏此類延伸的宣告,除非在包含(幾乎)任何系統標頭 之前 定義「功能測試巨集」。因此,對於 strdup
,您需要
#define _POSIX_C_SOURCE 200809L ... #include <string.h> ... strdup call(s)
適當的值可以在 Linux 上透過 man strdup
找到。(strncasecmp
的用法類似。)
然而,具有「GNU 擴充功能」的 gcc
模式(預設為 -std=gnu99 或 -std=gnu11)宣告足夠的巨集,以確保很少會看到遺漏的宣告。
這也適用於常數,例如 M_PI
和 M_LN2
,它們是 X/Open 標準的一部分:若要使用這些定義,請在包含任何標頭之前定義 _XOPEN_SOURCE
,或包含 R 標頭 Rmath.h。
alloca
很棘手:它既不是 ISO C/C++,也不是 POSIX 函式。充分可攜式的序言為
#ifdef __GNUC__ /* Includes GCC, clang and Intel compilers */ # undef alloca # define alloca(x) __builtin_alloca((x)) #elif defined(__sun) || defined(_AIX) /* this was necessary (and sufficient) for Solaris 10 and AIX 6: */ # include <alloca.h> #endif
'register' storage class specifier is deprecated and incompatible with C++17 ISO C++11 does not allow conversion from string literal to ‘char *’
(其中轉換應為 const char *
)。關鍵字 register
未在 C++98 中提及,在 C++11 中已棄用,並在 C++17 中移除。
在 C++11 中已棄用並在 C++17 中移除的 C++98 功能還有很多,而 LLVM clang
9 及更新版本會對它們發出警告(自版本 16 起已移除)。範例包括 bind1st
/bind2nd
(使用 std::bind
或 lambda104)std::auto_ptr
(已由 std::unique_ptr
取代)、std::mem_fun_ref
和 std::ptr_fun
。
bool
、false
和 true
在 C23 中已成為關鍵字,不再可用作變數名稱。如上所述,C++17 使用 byte
、data
、sample
和 size
。
因此,請避免使用其他程式語言中的常見字詞和關鍵字。
register
儲存類別指定詞(請參閱前一個項目)。
restrict
不是任何 C++ 標準的一部分105,且會遭某些 C++ 編譯器拒絕。
與具有 C API 的其他軟體介接的最可移植方式是使用 C 程式碼(通常可以在套件中與 C++ 程式碼混合使用)。
reinterpret_cast
對指標不安全:例如,類型可能有不同的比對需求。使用 memcpy
將內容複製到目標類型的全新變數。
__unix__
並未定義在所有類 Unix 系統上,特別是在 macOS 上沒有定義。為類 Unix 系統設定條件程式的合理可移植方式為
#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__)) #endif
但
#ifdef _WIN32 // Windows-specific code #else // Unix-alike code #endif
會更好。對於類 Unix 系統,最好使用 configure
來測試所需功能,而不是假設作業系統(而且人們常常忘記 R 也用於 Linux、Windows 和 macOS 以外的平台,有些人則忘記 macOS)。
g++
的平台不同)。標頭 bits/stdc++.h 不可移植,即使在包含它的平台上也不建議用於最終用戶程式碼。
malloc
或 calloc
時要小心。首先,必須始終檢查其回傳值以查看配置是否成功 - 使用 R 的 R_Calloc
通常更容易,因為它會進行檢查。其次,第一個引數的類型為 size_t
,最近的編譯器會警告傳遞有號引數(可能會提升為極大的值)。
gcc
、LLVM 和 Apple clang
支援的標記 -Wstrict-prototypes。這已找出許多函式未宣告引數的錯誤,而且可能會成為未來編譯器的預設值。(對於 C23 模式下的 LLVM clang
來說,它已經是預設值。)請注意,在 C99 和 C11 中,使用 f()
表示沒有任何參數的函式已被棄用,但預計在 C23 中不會被棄用。然而,f(void)
受所有標準支援,且可避免任何不確定性。
LLVM clang
有另一個警告 -Wdeprecated-non-prototype,它是由 -Wstrict-prototypes 啟用的。這會對 K&R 風格的使用發出警告,而這在 C23 中將不被接受。
man
頁面都會警告不要使用幾個 C 進入點,通常用非常強烈的措辭,例如「不要使用這些函數」。macOS 已開始警告106,如果這些函數用於 sprintf
、vsprintf
、gets
、mktemp
、tempmam
和 tmpnam
。強烈建議您使用更安全的替代方案(在任何平台上),但可以在包含定義它們的(C 或 C++)標頭之前,將「_POSIX_C_SOURCE」定義為例如「200809L」來避免警告。(但是,這可能會隱藏其他擴充功能。)
!DEC$ ATTRIBUTES DLLEXPORT,C,REFERENCE,ALIAS:'kdenestmlcvb' :: kdenestmlcvb
這些註解會在所有平台上由 Intel Fortran 解釋(而且不適合與 Windows 上的 R 搭配使用)。gfortran
有類似的形式,從 !GCC$
開始。
new
運算子採用引數 std::size_t size
,它是未簽署的。使用有符號的整數類型,例如 int
,可能會導致編譯器警告,例如
warning: argument 1 value ‘18446744073709551615’ exceeds maximum object size 9223372036854775807 [-Walloc-size-larger-than=]
(特別是在使用 LTO 時)。所以不要這麼做!
Martyn Plummer 在 https://journal.r-project.org/archive/2011-2/RJournal_2011-2_Plummer.pdf 提供了 C++ 的一些其他資訊。
大多數作業系統(包括所有通常用於 R 的作業系統)都有「暫定定義」的概念,其中全域 C 變數在沒有初始化項目的情況下定義。傳統上,連結器會將不同物件檔案中同一個變數的所有暫定定義解析為同一個物件,或解析為非暫定定義。然而,gcc
10108 和 LLVM clang
11109 已變更其預設值,因此暫定定義無法合併,而且如果同一個變數在多個物件檔案中定義,連結器會傳回錯誤。為避免發生這種情況,所有 C 原始碼檔案(只有一個例外)都應該宣告變數 extern
,這表示包含在標頭檔案中的任何此類變數都需要宣告為 extern
。一種常用的慣用語法(包括 R 本身)是將所有全域變數定義為標頭中的 extern
,例如 globals.h(而且只能在這裡),然後在一個(而且只有一個)原始碼檔案中使用
#define extern # include "globals.h" #undef extern
一種更乾淨的方法是根本不使用全域變數,而是將共用變數(宣告為 static
)放在單一檔案中,然後再放上所有使用這些變數的函式:這樣可能會產生更有效率的程式碼。
「現代」行為可以使用編譯器旗標 -fno-common 來查看,作為早期版本的 gcc
和 clang
中「CFLAGS」的一部分。
-fno-common 據說對 ARM cpu 特別有益。
這與不允許暫定定義的 C++ 無關。
R 4.3.0 及更新版本在編譯 C++ 時預設為 C++17,最後移除許多早在 C++11 就已棄用的 C++98 功能。編譯器/執行時期作者移除這些功能的速度很慢,但 LLVM clang
搭配其 libc++
執行時期函式庫終於在 2023 年開始這麼做,有些其他函式庫會警告,但有些則不會。
主要的罪魁禍首是 C++ 標頭和函式庫的「Boost」集合。有兩種鮮為人知的解決其過時程式碼面向的方法。一種是將
-D_HAS_AUTO_PTR_ETC=0
新增到 src/Makevars、src/Makevars.win 和 src/Makevars.ucrt 中的 PKG_CPPLAGS
。這涵蓋了移除
std::auto_ptr std::unary_function std::binary_function std::random_shuffle std::binder1st std::binder2nd
,其中大部分問題出現在包含 boost/functional.hpp(通常是間接包含)的程式碼中。
較少見的問題是對 enum
類型使用非法值,通常是負值,例如
BOOST_MPL_AUX_STATIC_CAST(AUX_WRAPPER_VALUE_TYPE, (value - 1));
在 boost/mpl/aux_/integral_wrapper.hpp 中。將
-Wno-error=enum-constexpr-conversion
新增到 PKG_CXXFLAGS
將允許這樣做,但這個標記只被近期版本的 LLVM clang
接受(未來也不會接受),因此需要 configure
測試。
預先建置的 clang
/libc++
目前版本通常可從 https://github.com/llvm/llvm-project/releases 取得,適用於各種平台(但 Windows 建置不支援 Rtools
,而 macOS 建置未簽署)。若要選取 libc++
,請將 -stdlib=libc++ 加入 CXX
,例如在 ~/.R/Makevars 中加入
CXX="/path/to/clang/clang++ -std=gnu++17 -stdlib=libc++"
。
另一個與 Rtools
相容性可能足夠的 Windows 建置可從 https://github.com/mstorsjo/llvm-mingw 取得:此建置使用 libc++
。
多年來,幾乎所有已知的 R 平台都使用 gfortran
作為 Fortran 編譯器,但現在有 LLVM 和「傳統」flang
,而 Intel 編譯器 ifort
111 和 ifx
現在免費提供。
在 CRAN 套件中仍有許多 Fortran 程式碼早於 Fortran 77。現代 Fortran 編譯器撰寫時以 Fortran 2018 最低標準為目標,而套件中的 Fortran 程式碼最好符合該標準。對於 gfortran
,可以透過將 -std=f2018 加入 FFLAGS
來檢查。最常見的問題是
DFLOAT
,它在 Fortran 77 中已被 DBLE
取代。此外,使用 DCMPLX
、DCONJG
、DIMAG
和類似的程式碼。
etime
、getpid
、isnan
112 和 sizeof
。
一個經常困擾套件撰寫者的問題是,它允許無序宣告:在符合標準的 Fortran 中,變數必須在其他宣告(例如維度)中使用之前宣告(明確或隱含)。
不幸的是,這會標記 DOUBLE COMPLEX
和 COMPLEX*16
等延伸。R 已測試過 DOUBLE COMPLEX
可行,因此優先於 COMPLEX*16
。(也可以使用類似 COMPLEX(KIND=KIND(0.0D0))
的程式碼。)
GNU Fortran 10 及更新版本會針對以前廣泛使用的做法產生編譯錯誤,例如傳遞預期陣列的 Fortran 陣列元素,或傳遞純量而非長度為一的陣列。請參閱 https://gcc.gnu.org/gcc-10/porting_to.html。Intel Fortran 編譯器也會這樣做,而且它們可能會更嚴格。
強烈建議使用 IMPLICIT NONE
– 使用 -warn 的 Intel 編譯器會針對沒有明確類型的變數發出警告。
常見的非可攜式結構包括
REAL(KIND=8)
之 Fortran 類型非常不利於移植。根據標準,這僅列舉不同的支援類型,因此 DOUBLE PRECISION
可能為 REAL(KIND=3)
(且為實際編譯器)。即使對於特定編譯器,值表示以位元組為單位的尺寸,支援的值也與平台有關,例如 gfortran
在所有當前平台上支援 4 和 8 的值,在少數平台上支援 10 和 16(但例如不支援所有「arm」CPU)。
相同情況適用於 INTEGER(KIND=4)
和 COMPLEX(KIND=16)
。
在套件中,Fortran 程式碼中許多使用整數和實數變數會與 C 互通(例如 .Fortran
以 C 編寫),且 R 已檢查 INTEGER
和 DOUBLE PRECISION
對應於 C 類型 int
和 double
。為了明確表示這一點,從 Fortran 2003 起,可以使用模組 iso_c_binding
中的名稱常數 c_int
、c_double
和 c_double_complex
。
R CMD INSTALL
會解決此問題。
.F90
來表示要預處理的原始程式碼:使用的預處理器與編譯器有關,可能是 cpp
,也可能不是。
如果您想在 Windows 或 macOS 上發行套件的二進位版本,您需要執行進一步的檢查以確認其可攜性:過於依賴您自己的機器上其他使用者可能沒有的外部軟體。
對於 Windows,請檢查套件的 DLL 依賴哪些其他 DLL(DLL 工具術語中的「匯入」)。一個方便的 GUI 工具是「Dependency Walker」(https://www.dependencywalker.com/),適用於 32 位元和 64 位元 DLL – 請注意,它會報告 R 自身 DLL 的遺失連結,例如 R.dll 和 Rblas.dll。適當工具鏈中的命令列工具 objdump
也會顯示匯入哪些 DLL。如果您使用 R 開發人員提供的工具鏈以外的工具鏈,或使用您自己的 makefile,請特別注意對工具鏈執行時期 DLL 的依賴性,例如 libgfortran、libstdc++ 和 libgcc_s。
對於 macOS,在 libs 目錄中的套件共用物件上使用 R CMD otool -L
會顯示它們依賴的項目:注意 /usr/local/lib 或 /usr/local/gfortran/lib 中的任何依賴項,特別是 libgfortran.?.dylib 和 libquadmath.0.dylib。(有關如何修正這些問題的方法,請參閱 R 安裝與管理 中的 建立二進制套件。)
許多人(包括 CRAN 套件儲存庫)不接受包含二進制檔案的原始程式碼套件,因為後者有安全風險。如果您想要散布需要 Windows 或 macOS 上外部軟體的原始程式碼套件,選項包括
Rtools
中包含 Windows 軟體,或與 Simon Urbanek 協商在他的「配方」系統中包含 macOS 軟體。
請注意,您可能需要提供額外組件的原始程式碼的授權需求(如果您的套件有類似 GPL 的授權,則需要提供)。
診斷訊息可以提供翻譯,因此以一致的樣式撰寫它們很重要。使用下一節中描述的工具來擷取所有訊息,可以提供您的一致性(或缺乏一致性)的有用概觀。以下是一些準則。
在 R 中,錯誤訊息不會使用 paste
建立訊息(此類訊息不會翻譯),而是透過 stop
或 warning
的多個引數,或透過 gettextf
建立。
'ord' must be a positive integer, at most the number of knots
在指稱 R 字元字串或類別時,則使用雙引號,例如
'format' must be "normal" or "short" - using "normal"
由於 ASCII 不包含方向性引號,因此最好使用 ‘'’ 並讓譯者(包括自動翻譯)在有需要時使用方向性引號。引號風格的範圍很廣:很不幸地,我們無法在可攜式的 texinfo
文件中複製它們。但作為範例,有些語言使用「向上」和「向下」(逗號)引號,而不是左引號或右引號,有些語言使用法式引號(有些語言使用 Adobe 所稱的「左法式引號」作為開頭,而其他語言則使用它作為結尾)。
在 R 訊息中,也可以使用 sQuote
或 dQuote
,例如
stop(gettextf("object must be of class %s or %s", dQuote("manova"), dQuote("maov")), domain = NA)
library
曾經使用過的結構,例如
if((length(nopkgs) > 0) && !missing(lib.loc)) { if(length(nopkgs) > 1) warning("libraries ", paste(sQuote(nopkgs), collapse = ", "), " contain no packages") else warning("library ", paste(sQuote(nopkgs)), " contains no package") }
並改用
if((length(nopkgs) > 0) && !missing(lib.loc)) { pkglist <- paste(sQuote(nopkgs), collapse = ", ") msg <- sprintf(ngettext(length(nopkgs), "library %s contains no packages", "libraries %s contain no packages", domain = "R-base"), pkglist) warning(msg, domain=NA) }
請注意,最好像這裡一樣使用完整的子句,因為在其他語言中可能需要說「函式庫 %s 中沒有套件」或「函式庫 %s 中沒有套件」。
有轉譯 R 和 C 層級錯誤及警告訊息的機制。只有在 R 以 NLS 支援編譯時才可用(由 configure
選項 --enable-nls 要求,預設值)。
這些程序使用 msgfmt
和 xgettext
,它們是 GNU gettext
的一部分,因此需要安裝:Windows 使用者可以在 https://www.stats.ox.ac.uk/pub/Rtools/goodies/gettext-tools.zip 找到預先編譯的二進位檔。
啟用翻譯的程序是
#include <R.h> /* to include Rconfig.h */ #ifdef ENABLE_NLS #include <libintl.h> #define _(String) dgettext ("pkg", String) /* replace pkg as appropriate */ #else #define _(String) (String) #endif
_(...)
中,例如
error(_("'ord' must be a positive integer"));
如果您想對單數和複數形式使用不同的訊息,您需要新增
#ifndef ENABLE_NLS #define dngettext(pkg, String, StringP, N) (N == 1 ? String : StringP) #endif
並以標記字串
dngettext("pkg", <singular string>, <plural string>, n)
xgettext --keyword=_ -o pkg.pot *.c
檔案 src/pkg.pot 是範本檔案,且通常會以 po/pkg.pot 的形式出貨。
機制也可用於支援 R stop
、warning
和 message
訊息的自動翻譯。它們使用訊息目錄的方式與 C 層級訊息相同,但使用網域 R-pkg
而不是 pkg
。在 stop
、warning
和 message
呼叫內部字元字串的翻譯會自動啟用,以及封裝在 gettext
或 gettextf
呼叫中的其他訊息。(若要抑制此功能,請使用參數 domain=NA
。)
準備 R-pkg.pot 檔案的工具在 tools 套件中提供:xgettext2pot
會從所有出現在 gettext
/gettextf
、stop
、warning
和 message
呼叫中的字串準備一個檔案。其中有些可能是無效的,因此檔案可能需要手動編輯。xgettext
會萃取實際呼叫,因此在整理錯誤訊息時會更有用。
R 函數 ngettext
提供一個與同名 C 函數的介面:請參閱前一節的範例。在呼叫 ngettext
時,最安全的方法是明確使用 domain="R-pkg"
,而且對於較早版本的 R 來說是必要的,除非它們是直接從套件中的函數呼叫。
一旦範本檔案建立完成,就可以進行翻譯。傳統的翻譯檔案副檔名為 .po,並放置在套件的 po 子目錄中,其名稱為 ‘ll.po’ 或 ‘R-ll.po’,分別對 C 和 R 訊息翻譯成代碼為 ‘ll’ 的語言。
請參閱 R 安裝和管理 中的 訊息在地化,以取得語言代碼的詳細資料。
套件 tools 中有一個 R 函數 update_pkg_po
,可自動執行大部分訊息翻譯的維護工作。有關其詳細功能,請參閱其說明。
如果在沒有現有翻譯的套件上呼叫此函數,它會建立目錄 pkgdir/po,在其中建立 R 訊息範本檔案 pkgdir/po/R-pkg.pot,建立「en@quot」翻譯並安裝它。(「en@quot」偽語言會在適當的 (例如 UTF-8) 區域設定中以其方向形式詮釋引號。)
如果套件在標記為翻譯的 src 目錄中有 C 原始檔,請使用
touch pkgdir/po/pkg.pot
建立一個虛擬範本檔,然後再次呼叫 update_pkg_po
(這也可以在第一次呼叫之前執行)。
當在 pkgdir/po 目錄中新增新語言的翻譯時,執行相同的指令會檢查並安裝翻譯。
如果套件來源已更新,相同的指令會更新範本檔,將變更合併到翻譯 .po 檔中,然後安裝已更新的翻譯。您經常會看到合併會將翻譯標記為「模糊」,這會在涵蓋率統計中報告。由於模糊翻譯不會使用,這表示翻譯檔需要人工處理。
合併的翻譯會透過 tools::checkPofile
執行,以檢查 C 式格式是否正確使用:如果沒有,會報告不符,且不會安裝損壞的翻譯。
此函式需要安裝並將 GNU gettext-tools
放置於路徑中:請參閱其說明頁面。
已安裝的檔案 CITATION 將由 citation()
函式使用。(它應該在套件來源的 inst 子目錄中。)
CITATION 檔案以 R 程式碼剖析(在套件宣告的編碼中,或在未宣告時以 ASCII 剖析)。它將包含對函式 bibentry
的呼叫。以下是 nlme 的範例
## R package reference generated from DESCRIPTION metadata citation(auto = meta) ## NLME book bibentry(bibtype = "Book", title = "Mixed-Effects Models in S and S-PLUS", author = c(person(c("José", "C."), "Pinheiro"), person(c("Douglas", "M."), "Bates")), year = "2000", publisher = "Springer", address = "New York", doi = "10.1007/b98882")
請注意第一個呼叫如何從物件 meta
自動產生引文資訊,這是 DESCRIPTION 檔案的剖析版本 - 雖然很誘人要硬編碼此類資訊,但它通常會過時。第一個條目作為 bibentry
呼叫的樣子,可以從任何已安裝套件的 print(citation("pkgname", auto = TRUE), style = "R")
中看到。如果沒有 CITATION 檔案,則預設會傳回自動產生的資訊。
請參閱 ?bibentry
以進一步瞭解可以提供的資訊。如果 bibentry 包含 LaTeX 標記(例如,用於重音字元或數學符號),則可能需要提供文字表示,以透過 bibentry
的 textVersion
參數用於列印 via。例如,較早版本的 nlme 另外使用了類似
textVersion = paste0("Jose Pinheiro, Douglas Bates, Saikat DebRoy, ", "Deepayan Sarkar and the R Core Team (", sub("-.*", "", meta$Date), "). nlme: Linear and Nonlinear Mixed Effects Models. ", sprintf("R package version %s", meta$Version), ".")
CITATION 檔案本身在 source
-d 時不應產生任何輸出。
建議(且對 CRAN 至關重要)CITATION 檔案不包含對函式的呼叫,例如 packageDescription
,這些函式假設套件已安裝在套件搜尋路徑上的函式庫樹中。
在 DESCRIPTION 檔案中有一個選用欄位 Type
,如果遺漏,則假設為「Package」,也就是本章節中到目前為止所討論的延伸套件類型。目前僅識別出另一種類型;過去還曾經有「Translation」類型。
這是一個相當通用的機制,用於新增新的前端,例如之前的 gnomeGUI 套件(請參閱 CRAN 上的 Archive 區域)。如果在套件的最上層目錄中找到 configure 檔案,則會執行該檔案,然後如果找到 Makefile(通常由 configure 產生),則會呼叫 make
。如果使用 R CMD INSTALL --clean
,則會呼叫 make clean
。不會執行其他動作。
R CMD build
可以封裝此類型的延伸套件,但 R CMD check
會檢查類型並略過它。
許多此類型的套件需要 R 安裝目錄的寫入權限。
R 專案的數位成員已設定服務,協助撰寫 R 套件,特別是打算公開發行的套件。
win-builder.r-project.org 提供自動準備 (32/64 位元) Windows 二進位檔,來源套件經過良好測試。
R-Forge (R-Forge.r-project.org) 和 RForge (www.rforge.net) 是名稱相似的類似服務。兩者都透過 SVN 提供原始碼管理、每日建置和檢查、郵寄清單以及可 透過 install.packages
存取的存放庫 (它們可透過 setRepositories
和使用它的 GUI 功能表選取)。套件開發人員有機會在專案網站或新聞公告中展示他們的作品。郵寄清單、論壇或 wiki 提供方便的工具,供使用者討論並在開發人員和/或有興趣的使用者之間交換資訊。
R 物件記載在以「R 文件」(Rd) 格式撰寫的檔案中,這是一種簡單的標記語言,其中許多部分與 (La)TeX 十分類似,可以處理成多種格式,包括 LaTeX、HTML 和純文字。翻譯是由 tools 套件中的函式執行,這些函式由 R_HOME/bin 中的指令碼 Rdconv
和套件的安裝指令碼呼叫。
R 發行版包含超過 1400 個此類檔案,可以在 R 原始碼樹的 src/library/pkg/man 目錄中找到,其中 pkg 代表 R 發行版中包含的其中一個標準套件。
舉例來說,我們來看一下 src/library/base/man/load.Rd 的簡化版本,其中記載了 R 函式 load
。
% File src/library/base/man/load.Rd \name{load} \alias{load} \title{Reload Saved Datasets} \description{ Reload datasets written with the function \code{save}. } \usage{ load(file, envir = parent.frame(), verbose = FALSE) } \arguments{ \item{file}{a (readable binary-mode) \link{connection} or a character string giving the name of the file to load (when \link{tilde expansion} is done).} \item{envir}{the environment where the data should be loaded.} \item{verbose}{should item names be printed during loading?} } \value{ A character vector of the names of objects created, invisibly. } \seealso{ \code{\link{save}}. } \examples{ ## save all data save(list = ls(all.names = TRUE), file = "all.RData") ## restore the saved values to the current environment load("all.RData") } \keyword{file}
一個 Rd 檔案包含三個部分。標頭提供有關檔案名稱、文件主題、標題、簡短文字說明和文件物件的 R 使用資訊的基本資訊。主體提供進一步的資訊(例如,關於函式的引數和傳回值,如上例所示)。最後,有一個包含關鍵字資訊的選用頁尾。標頭是強制性的。
資訊會提供在具備標準名稱的一系列 區段 中(也允許使用者定義區段)。除非另有說明113,否則這些區段應該只在 Rd 檔案中出現一次(任何順序),而且處理軟體只會保留檔案中標準區段的第一次出現,並會發出警告。
請參閱 “Rd 檔案指南”,以取得撰寫 Rd 格式文件的指南,這對於套件撰寫者來說應該很有用。 R 通用函式 prompt
用於建構一個準備好進行手動編輯的基礎 Rd 檔案。方法定義為文件函式(填入適當的函式和引數名稱)和資料框。還有函式 promptData
、promptPackage
、promptClass
和 promptMethods
,用於其他類型的 Rd 檔案。
Rd 檔案的一般語法總結如下。有關目前 Rd 語法的詳細技術討論,請參閱 “剖析 Rd 檔案”。
Rd 檔案包含四種文字輸入類型。最常見的類型類似 LaTeX,反斜線用作標記前綴(例如 \alias
),大括號用於表示引數(例如 {load}
)。最不常見的文字類型為「逐字」文字,其中除了註解標記 (%
) 之外,不會處理其他標記。還有一種罕見的「逐字」文字變體(用於 \eqn
、\deqn
、\figure
和 \newcommand
),其中不需要跳脫註解標記。最後一種類型類似 R,用於 R 程式碼,但允許一些嵌入巨集。R 類似文字中的引號字串會以特殊方式處理:一般字元跳脫(例如 \n
)可以原樣輸入。只有以 \l
(例如 \link
)或 \v
(例如 \var
)開頭的標記會在引號字串中被辨識。很少使用的垂直標籤 \v
必須輸入為 \\v
。
每個巨集定義其引數的輸入類型。例如,檔案最初使用類似 LaTeX 的語法,這也用於 \description
區段,但 \usage
區段使用類似 R 的語法,而 \alias
巨集使用「逐字」語法。註解從百分比符號 %
延伸到所有文字類型(罕見的「逐字」變體除外)的該行結尾(例如 load
範例的第一行)。
由於反斜線、大括號和百分比符號具有特殊含義,因此有時需要使用反斜線來將它們輸入到文字中。一般而言,平衡的大括號不需要跳脫,但百分比符號總是需要,除了在「逐字」變體中。有關跳脫的巨集和規則的完整清單,請參閱“剖析 Rd 檔案”。
用於文件化 R 物件(特別是函式)的基本標記命令在此小節中提供。
\name{名稱}
¶名稱 通常114是包含文件檔的 Rd 檔案的基本檔名。它是檔案所代表的 Rd 物件的「名稱」,而且在套件中必須是唯一的。為避免套件手冊索引的問題,它不能包含「!」、「|」或「@」,而且為避免 HTML 說明系統可能出現的問題,它不應包含「/」或空白。(允許使用 LaTeX 特殊字元,但索引中可能無法正確整理。)一個檔案中只能有一個 \name
項目,而且它不能包含任何標記。套件手冊中的項目將按照 \name
項目的字母順序115排列。
\alias{主題}
¶\alias
區段會指定檔案文件中的所有「主題」。此資訊會收集到索引資料庫中,供線上(純文字和 HTML)說明系統查詢。 主題 可以包含空白,但(基於歷史原因)開頭和結尾的空白會被移除。百分比和左大括號需要用反斜線進行跳脫。
可能會有多個 \alias
項目。通常在一個檔案中文件化多個 R 物件會比較方便。例如,檔案 Normal.Rd 會文件化常態分佈的密度、分配函數、分位數函數和隨機變異的產生,因此會從以下開始
\name{Normal} \alias{Normal} \alias{dnorm} \alias{pnorm} \alias{qnorm} \alias{rnorm}
此外,通常會用多種不同的方式來指稱一個 R 物件會比較方便,而且 \alias
不需要是物件的名稱。
請注意, \name
不一定會是文件化的主題,如果需要,則需要有明確的 \alias
項目(如同此範例)。
\title{標題}
¶Rd 檔案的標題資訊。此資訊應以大寫字母開頭,且不應以句點結尾;為了最廣泛的相容性,請盡量將其長度限制在 65 個字元以內。
文字中支援標記,但使用英文文字和標點符號以外的字元(例如「<」)可能會限制可攜性。
說明檔案中必須有一個(且僅有一個) \title
區段。
\description{…}
¶函數執行功能的簡短說明(一段落,僅數行)。(如果說明太長且無法輕易縮短,則檔案可能試圖一次文件化太多內容。)這是強制性的,套件概觀檔案除外。
\usage{函數(參數1, 參數2, …)}
¶顯示檔案中函數和變數概要的一或多行。這些內容以打字機字型設定。這是一個類似 R 的指令。
指定的用法資訊應與函數定義完全相符(如此才能自動檢查程式碼和文件之間的一致性)。
若要指出函數可以根據指定的命名參數以多種不同的方式使用,請使用區段 \details
。例如,abline.Rd 包含
\details{ Typical usages are \preformatted{abline(a, b, ...) ...... }
使用 \method{泛函}{類別}
指出繼承自類別 "類別"
物件的泛函函數 泛函 的 S3 方法名稱。在列印版本中,這將顯示為 泛函(反映出方法不應直接呼叫,而應透過方法調用),但 codoc()
和其他品質控管工具始終可以存取完整名稱。
例如,print.ts.Rd 包含
\usage{ \method{print}{ts}(x, calendar, \dots) }
將列印為
Usage: ## S3 method for class ‘ts’: print(x, calendar, ...)
替換函數的用法應以 dim(x) <- 值
的樣式提供,而不是明確指出替換函數的名稱(如上方的 "dim<-"
)。類似地,可以使用 \method{泛函}{類別}(參數清單) <- 值
指出繼承自類別 "類別"
物件的泛函替換函數 "泛函<-"
的 S3 替換方法的用法。
用於萃取或替換物件部分的 S3 方法、Ops 群組成員的 S3 方法,以及使用者定義 (二進位) 中綴運算子 (‘%xxx%’) 的 S3 方法遵循上述規則,使用適當的函式名稱。例如,Extract.factor.Rd 包含
\usage{ \method{[}{factor}(x, \dots, drop = FALSE) \method{[[}{factor}(x, \dots) \method{[}{factor}(x, \dots) <- value }
將列印為
Usage: ## S3 method for class ‘factor’: x[..., drop = FALSE] ## S3 method for class ‘factor’: x[[...]] ## S3 replacement method for class ‘factor’: x[...] <- value
\S3method
可作為 \method
的替代選項。
\arguments{…}
¶函式引數的說明,使用表單中的項目
\item{arg_i}{Description of arg_i.}
作為引數清單的每個元素。(請注意,項目三部分之間沒有空白。)引數也可以透過在 \item
標籤中以逗號 (和空白) 分隔其名稱來共同說明。在 \item
項目之外可能有選用文字,例如提供關於參數群組的一般資訊。
\details{…}
¶提供詳細且精確的說明,說明所提供的功能,並延伸 \description
欄位中的基本資訊。
\value{…}
¶函式傳回值的說明。
如果傳回清單包含多個值,您可以使用表單中的項目
\item{comp_i}{Description of comp_i.}
對於回傳清單的每個元件。在 \item
項目外可能會有選用的文字(例如 rle
和 inverse.rle
的共同說明,或 l10n_info
中的項目組)。請注意,\value
隱含地為 \describe
環境,因此不應使用該環境來列出元件,僅使用個別 \item{}{}
項目。116
\references{…}
¶包含參考文獻的部分。針對網路指標使用 \url{}
或 \href{}{}
,針對 DOI 使用 \doi{}
(這需要 R >= 3.3,請參閱 使用者定義巨集 以取得更多資訊)。
\note{...}
¶用於您想要特別指出的特殊註解。允許多個 \note
部分,但可能會讓最終使用者感到困惑。
例如,pie.Rd 包含
\note{ Pie charts are a very bad way of displaying information. The eye is good at judging linear measures and bad at judging relative areas. ...... }
\author{…}
¶關於 Rd 檔案作者的資訊。使用 \email{}
而沒有額外分隔符號(例如 ‘( )’ 或 ‘< >’)來指定電子郵件地址,或使用 \url{}
或 \href{}{}
針對網路指標。
\seealso{…}
¶使用 \code{\link{...}}
指向相關 R 物件的指標(\code
是 R 物件名稱正確的標記,\link
會在支援此功能的輸出格式中產生超連結。請參閱 標記文字 和 交叉參照)。
\examples{…}
¶函數使用範例。此區段的程式碼以打字機字型設定,不重新格式化,並由 example()
執行,除非另有標示(請見下方)。
範例不僅對文件目的有用,也提供用於診斷檢查 R 程式碼的測試程式碼。預設情況下,\examples{}
內的文字會顯示在說明頁面的輸出中,並由 example()
和 R CMD check
執行。您可以使用 \dontrun{}
來處理僅顯示但不執行的文字,以及 \dontshow{}
來處理額外的測試指令,這些指令不應顯示給使用者,但會由 example()
執行。(先前這稱為 \testonly
,此名稱仍可接受。)
\dontrun{}
內的文字為「逐字」,但 \examples
區段的其他部分為類似 R 的文字。
例如,
x <- runif(10) # Shown and run. \dontrun{plot(x)} # Only shown. \dontshow{log(x)} # Only run.
因此,未包含在 \dontrun
中的範例程式碼必須可執行!此外,不應使用任何系統特定功能或需要特殊設施(例如網際網路存取或對特定目錄的寫入權限)。\dontrun
中包含的文字會在已處理的說明檔案中以註解表示:它不必是有效的 R 程式碼,但仍必須對 %
、\
和未配對的大括弧使用跳脫字元,如同其他「逐字」文字。
範例程式碼必須能夠由 example
執行,而 example
使用 source
。這表示它不應存取 stdin,例如從範例檔案 scan()
資料。
可以透過亂數產生(例如,x <- rnorm(100)
),或使用 data()
列出的標準資料集(有關更多資訊,請參閱 ?data
)取得製作範例可執行檔案所需的資料。
最後,\donttest
(用於獨立行的開頭)用於標記應由 example()
執行,但不要由 R CMD check
執行的程式碼(預設值:可以使用 --run-donttest 選項)。這應該只有偶爾需要,但可以用於在難以測試的情況下可能會失敗的程式碼,例如在某些區域設定中。(使用例如 capabilities()
或 nzchar(Sys.which("someprogram"))
盡可能測試範例中需要的功能,您也可以使用 try()
或 tryCatch()
。使用 interactive()
來設定需要有人互動的範例。)請注意,包含在 \donttest
中的程式碼必須是正確的 R 程式碼,且應在 DESCRIPTION 檔案中宣告使用的任何套件。在 \donttest
區段中包含註解來說明為何需要它,這是一個良好的習慣。
註解之間的程式碼輸出
## IGNORE_RDIFF_BEGIN ## IGNORE_RDIFF_END
在將檢查輸出與參考輸出(-Ex.Rout.save 檔案)進行比較時,會被忽略。此標記也可以用於 tests 底下的指令碼。
\keyword{key}
¶每個檔案可以有零個或多個 \keyword
區段。每個 \keyword
區段應指定一個單一關鍵字,最好是 R 文件目錄中的 KEYWORDS 檔案中所列的標準關鍵字之一(預設為 R_HOME/doc)。例如,使用 RShowDoc("KEYWORDS")
來從 R 內部檢查標準關鍵字。如果要記錄的 R 物件屬於多個類別,則可以有多個 \keyword
項目,或沒有任何項目。
如果您打算使用少數幾個非標準關鍵字,請強烈考慮使用 \concept
(請參閱 索引)代替 \keyword
。
特殊關鍵字「internal」標示一個內部物件的頁面,該物件不是套件 API 的一部分。如果物件 foo
的說明頁面有關鍵字「internal」,則 help(foo)
會提供這個說明頁面,但 foo
會從多個主題索引中排除,包括 HTML 說明系統中的主題字母清單。
help.search()
可以依據關鍵字搜尋,包括使用者定義的值:不過,透過 help.start()
存取的「搜尋引擎與關鍵字」HTML 頁面只提供單一點擊存取至預先定義的關鍵字清單。
記錄 R 資料集的 Rd 檔案結構略有不同。不需要 \arguments
和 \value
等區段,但應說明資料的格式和來源。
舉例來說,我們來看一下 src/library/datasets/man/rivers.Rd,它記錄了標準 R 資料集 rivers
。
\name{rivers} \docType{data} \alias{rivers} \title{Lengths of Major North American Rivers} \description{ This data set gives the lengths (in miles) of 141 \dQuote{major} rivers in North America, as compiled by the US Geological Survey. } \usage{rivers} \format{A vector containing 141 observations.} \source{World Almanac and Book of Facts, 1975, page 406.} \references{ McNeil, D. R. (1977) \emph{Interactive Data Analysis}. New York: Wiley. } \keyword{datasets}
它使用了以下額外標記命令。
\docType{…}
指出文件物件的「類型」。對於資料集,永遠是「data」;對於 pkg-package.Rd 概觀檔案,永遠是「package」。S4 方法和類別的文件使用「methods」(來自 promptMethods()
)和「class」(來自 promptClass()
)。
\format{…}
¶資料集格式的說明(作為向量、矩陣、資料框、時間序列等)。對於矩陣和資料框,這應該提供每一個欄位的說明,最好是作為清單或表格。請參閱 清單和表格,以取得更多資訊。
\source{…}
¶原始來源的詳細資料(參考或 URL,請參閱 指定 URL)。此外,區段 \references
可以提供次要來源和用法。
另請注意,在記錄資料集 bar 時,
\usage
項目永遠是 bar
或(對於不使用資料延遲載入的套件) data(bar)
。(特別是,每個 Rd 檔案只記錄 單一 資料物件。)
\keyword
項目永遠應該是「datasets」。
如果 bar
是資料框,可以 透過 prompt(bar)
將它記錄為資料集。否則,可以使用 promptData
函式。
有特殊的方法可以使用「?」運算子,即「class?主題」和「methods?主題」,分別存取 S4 類別和方法的說明文件。此機制依賴於 \alias
項目中所使用的主題名稱慣例。S4 類別和方法的主題名稱分別為下列形式
class-class generic,signature_list-method
其中 簽章_清單 包含方法簽章中類別的名稱(不含引號),以「,」分隔(不含空白),對於沒有明確規範的引數,則使用「ANY」。例如,「genericFunction-class」是 S4 類別 "genericFunction"
說明文件的主題名稱,而「coerce,ANY,NULL-method」則是 S4 方法 coerce
簽章 c("ANY", "NULL")
說明文件的主題名稱。
可以使用套件 methods 中的函數 promptClass()
和 promptMethods()
產生 S4 類別和方法的說明文件範本。如果需要或想要為 S4 方法提供明確的函數宣告(在 \usage
區段中)(例如,如果它有「令人驚訝的引數」需要明確提到),可以使用特殊標記
\S4method{generic}{signature_list}(argument_list)
(例如,「\S4method{coerce}{ANY,NULL}(from, to)」)。
為了充分利用線上說明文件系統的潛力,套件中所有使用者可見的 S4 類別和方法至少應在套件的其中一個 Rd 檔案中有一個合適的 \alias
項目。如果套件有在其他地方最初定義的函數方法,且不會變更函數的底層預設方法,則套件負責記錄它所建立的方法,但不負責函數本身或預設方法。
S4 取代方法的記錄方式與 S3 相同:請參閱 記錄函數 中對 \method
的說明。
請參閱 help("Documentation", package = "methods")
以取得更多有關使用和建立 S4 類別和方法的線上文件資訊。
套件可以有一個概觀說明頁,其中包含 \alias
pkgname-package
,例如 utils 套件的「utils-package」,當 package?pkgname
會開啟該說明頁。如果在其他 Rd 檔案中不存在名為 pkgname
的主題,則建議將其用作額外的 \alias
。
可以使用函數 promptPackage()
為套件產生文件架構。如果使用 final = TRUE
參數,則 Rd 檔案將以最終形式產生,其中包含 library(help = pkgname)
會產生的資訊。否則(預設值),將插入提供內容建議的註解。
除了強制性的 \name
和 \title
以及 pkgname-package
別名之外,套件概觀頁面的唯一需求是包含 \docType{package}
陳述式。所有其他內容都是選用的。我們建議它應該是一個簡短的概觀,以提供不熟悉該套件的讀者足夠的資訊以開始使用。更廣泛的文件最好放入套件範例(請參閱 撰寫套件範例)並從此頁面參照,或放入函數、資料集或類別的個別手冊頁面中。
要開始新的段落或在範例中留空白行,只要插入空行(如 (La)TeX)。要換行,請使用 \cr
。
除了預定義的區段(例如 \description{}
、\value{}
等),您可以透過 \section{section_title}{…}
來「定義」任意區段。 例如
\section{Warning}{ You must not call this function unless ... }
為了與預先指派的區段保持一致,區段名稱(\section
的第一個引數)應大寫(但不要全部大寫),且不要以句點結尾。第一個和大括弧表達式之間不允許有空白。區段標題中的標記(例如 \code
)可能會導致 latex 轉換出現問題(取決於巨集套件的版本,例如「hyperref」),因此應避免使用。
\subsection
巨集採用與 \section
相同格式的引數,但用於區段內,因此可用於在區段或其他小節中巢狀小節。巢狀層級沒有預定義限制,但格式設計不適用於超過 3 個層級(即區段中的小節中的小節)。
請注意,額外命名的區段始終會插入在輸出中的固定位置(\note
、\seealso
和範例之前),無論它們出現在輸入中的何處(但在輸入中它們之間的順序相同)。
下列邏輯標記命令可供強調或引用文字使用。
\emph{文字}
¶\strong{文字}
如果可能,使用斜體和粗體字型來強調文字;\strong
被視為更強(更強調)。
\bold{文字}
¶如果可能,將文字設定為粗體字型。
\sQuote{文字}
¶\dQuote{文字}
可攜式地單引號或雙引號文字(不用硬連線用於引號的字元)。
以上每個指令都接受類似 LaTeX 的輸入,因此其他巨集可以在文字中使用。
以下邏輯標記指令可供指示特定類型的文字。除非另有說明,否則這些指令會接受「逐字」文字輸入,因此其他巨集不能在其中使用。某些字元需要跳脫(請參閱插入)。
\code{文字}
¶指出文字是 R 程式片段的實際範例,例如 R 程式碼片段或 R 物件的名稱。文字以類似 R 的語法輸入,並在可能的情況下使用打字機
字型顯示。巨集\var
和\link
會在文字中解釋。
\preformatted{文字}
¶指出文字是程式片段的實際範例。文字在可能的情況下使用打字機
字型顯示。格式化(例如換行)會保留。(請注意,這包括在初始{之後換行,因此文字通常應從與指令相同的行開始。)
由於 LaTeX 在撰寫本文時有其限制,此巨集可能無法嵌套在其他標記巨集中,例如 \dQuote
和 \sQuote
,否則可能會導致錯誤或格式不佳。
\kbd{鍵盤字元}
¶指示鍵盤輸入,如果可能,請使用 斜體打字機 字型,以便使用者可以區分他們應該輸入的字元和電腦輸出。文字以「逐字」輸入。
\samp{文字}
¶指示文字為字元序列的文字範例,以「逐字」輸入,包含在換行文字中。顯示在單引號中,並在可能的情況下使用 打字機
字型。
\verb{文字}
¶指示文字為字元序列的文字範例,以「逐字」輸入。不會換行或重新格式化。在可能的情況下使用 打字機
字型顯示。
\pkg{套件名稱}
¶指示 R 套件的名稱。類似 LaTeX。
\file{檔案名稱}
¶指示檔案的名稱。文字類似 LaTeX,因此反斜線需要跳脫。在可能的情況下使用不同的字型顯示。
\email{電子郵件地址}
¶指示電子郵件地址。類似 LaTeX,會在 HTML 和 PDF 轉換中呈現為超連結。在可能的情況下使用 打字機
字型顯示。
\url{統一資源定位器}
¶指出萬維網的統一資源定位器 (URL)。參數會以「逐字」文字處理(百分號和括弧會以反斜線跳脫),並在 HTML 和 PDF 轉換中呈現為超連結。換行符號會移除,而前導和尾隨空白字元117也會移除。請參閱 指定 URL。
在可能的情況下,使用 typewriter
字型顯示。
\href{uniform_resource_locator}{text}
¶指出萬維網的超連結。第一個參數會以「逐字」文字處理(百分號和括弧會以反斜線跳脫),並用於超連結中的 URL,而第二個參數的 LaTeX 類似文字會顯示給使用者。換行符號會從第一個參數中移除,而前導和尾隨空白字元也會移除。
請注意,在 R 3.1.3 之前的版本中,RFC3986 編碼的 URL(例如,使用「%28VS.85%29」取代「(VS.85)」)可能無法正確運作,最好避免使用,請使用 URLdecode()
解碼。
\var{metasyntactic_variable}
¶指出元語法變數。在某些情況下,這會以不同的方式呈現,例如以斜體,但並非所有情況118都如此。類似 LaTeX。
\env{environment_variable}
¶指出環境變數。「逐字」。在可能的情況下,使用 typewriter
字型顯示
\option{選項}
¶指出命令列選項。‘逐字’。在可能的情況下,使用 打字機
字體顯示。
\command{命令名稱}
¶指出命令的名稱。類似 LaTeX,因此會解釋 \var
。在可能的情況下,使用 打字機
字體顯示。
\dfn{術語}
¶指出術語的介紹或定義用途。類似 LaTeX。
\cite{參考}
¶指出沒有直接交叉參照的參考,透過 \link
(請參閱 交叉參照),例如書籍名稱。類似 LaTeX。
\acronym{縮寫}
¶指出縮寫(以全大寫字母寫成的縮寫),例如 GNU。類似 LaTeX。
\itemize
和 \enumerate
命令只接受一個引數,其中可以包含一個或多個 \item
命令。每個 \item
後面的文字會格式化為一個或多個段落,適當地縮排,且第一個段落會標記為項目符號 (\itemize
) 或數字 (\enumerate
)。
請注意,與引數清單不同,這些格式中的 \item
後面接著一個空白和文字(未包含在括號中)。例如
\enumerate{ \item A database consists of one or more records, each with one or more named fields. \item Regular lines start with a non-whitespace character. \item Records are separated by one or more empty lines. }
\itemize
和 \enumerate
命令可以巢狀。
\describe
命令類似於 \itemize
,但允許指定初始標籤。每個 \item
採用兩個引數,標籤和項目主體,方式與引數或值 \item
完全相同。 \describe
命令會對應到 HTML 中的 <DL>
清單和 LaTeX 中的 \description
清單。
在沒有任何 \item
的情況下使用這些命令可能會導致某些轉換出現問題,而且意義不大。
\tabular
命令採用兩個引數。第一個引數為每一欄指定所需的對齊方式(左對齊為「l」,右對齊為「r」,置中為「c」)。第二個引數包含任意數量的行,各行以 \cr
分隔,各欄位以 \tab
分隔。例如
\tabular{rlll}{ [,1] \tab Ozone \tab numeric \tab Ozone (ppb)\cr [,2] \tab Solar.R \tab numeric \tab Solar R (lang)\cr [,3] \tab Wind \tab numeric \tab Wind (mph)\cr [,4] \tab Temp \tab numeric \tab Temperature (degrees F)\cr [,5] \tab Month \tab numeric \tab Month (1--12)\cr [,6] \tab Day \tab numeric \tab Day of month (1--31) }
每一行中的欄位數必須與第一個引數中的對齊方式數相同,而且欄位不能為空(但可以只包含空白)。(\tabular
與第一個引數之間沒有空白,兩個引數之間也沒有空白。)
標記 \link{foo}
(通常與組合 \code{\link{foo}}
一起使用)會產生一個超連結,連結到 foo 的說明。這裡的 foo 是主題,也就是另一個 Rd 檔案(可能在另一個套件中)中 \alias
標記的參數。某些 Rd 檔案轉換成的格式支援超連結,例如 HTML 和 PDF,但其他格式(例如文字格式)則會忽略超連結。
在說明頁面的 \seealso
區段中,\link
的主要用法之一,請參閱 Rd 格式。
請注意,從 \alias
中擷取主題時,會移除前導和尾隨空格,但在查詢 \link
的主題時,不會移除這些空格。
您可以透過 \link[=目的地]{名稱}
指定連結至不同主題,其連結至主題 目的地,名稱為 名稱。這可以用於參照 S3/4 類別的文件,例如 \code{"\link[=abc-class]{abc}"}
會是參照封裝中定義的 S4 類別 "abc"
文件的方法,而 \code{"\link[=terms.object]{terms}"}
則會參照 S3 "terms"
類別(在封裝 stats 中)。為了讓這些在原始檔中易於閱讀,\code{"\linkS4class{abc}"}
會擴充為上面給定的形式。
還有另外兩種形式,帶有選用引數,指定為 \link[封裝]{foo}
和 \link[封裝:bar]{foo}
,分別連結至封裝 封裝 中的主題 foo
和 bar
。它們目前僅用於 HTML 說明(並忽略說明頁面 LaTeX 轉換中的超連結)。對於包含特殊字元(例如算術運算子)的主題,應小心,因為它們可能會導致無法解析的連結,最好在同一個說明頁面中使用較安全的別名。
在歷史上(R 版本 4.1.0 之前),形式為 \link[pkg]{foo}
和 \link[pkg:bar]{foo}
的連結用於解釋為連結至套件 pkg 中的 檔案 foo.html 和 bar.html。因此,如果 HTML 說明系統找不到主題 foo
,它會在套件 pkg 中尋找檔案 foo.html,然後在其他已安裝的套件中搜尋該主題。若要測試連結在舊系統和新系統中都能運作,可以透過設定環境變數 _R_HELP_LINKS_TO_TOPICS_=false
來還原 4.1.0 之前的行為。
這些「其他形式」所引用的套件應在 DESCRIPTION 檔案中的「Depends」、「Imports」、「Suggests」或「Enhances」欄位中宣告。
數學公式應設定為列印文件和 KaTeX/MathJax 增強的 HTML 說明(從 R 4.2.0 開始)的美麗格式,但我們仍想要一些對純文字(和舊版 HTML)說明有用的東西。為此,使用了兩個指令 \eqn{latex}{ascii}
和 \deqn{latex}{ascii}
。其中 \eqn
用於「內嵌」公式(對應 TeX 的 $…$
),\deqn
提供「顯示方程式」(如 LaTeX 的 displaymath
環境,或 TeX 的 $$…$$
)。兩個參數都視為「逐字」文字。
兩個指令也可以用作 \eqn{latexascii}
(僅 一個 參數),然後用於 latex 和 ascii。指令和第一個參數之間,以及第一個和第二個參數之間不允許有空白。
下列範例來自 Poisson.Rd
\deqn{p(x) = \frac{\lambda^x e^{-\lambda}}{x!}}{% p(x) = \lambda^x exp(-\lambda)/x!} for \eqn{x = 0, 1, 2, \ldots}.
在純文字說明中,我們得到
p(x) = lambda^x exp(-lambda)/x! for x = 0, 1, 2, ....
在舊版 HTML 說明中,如果希臘字母(大小寫)前面有反斜線,將會呈現,\dots
和 \ldots
將會呈現為省略號,\sqrt
、\ge
和 \le
將會呈現為數學符號。
請注意,只能使用基本的 LaTeX,沒有提供指定 LaTeX 樣式檔案的規定,但從 R 4.2.2 開始支援 AMS 擴充套件。
若要在說明頁面中加入圖形,請使用 \figure
標記。共有三種形式。
兩種常用的簡式為 \figure{filename}
和 \figure{filename}{alternate text}
。這會在 HTML 或 LaTeX 輸出中包含圖形的副本。在文字輸出中,將顯示備用文字。(當省略第二個參數時,將使用檔名。)檔名和備用文字都將逐字解析,不應包含在 HTML 或 LaTeX 中重要的特殊字元。
專家形式為 \figure{filename}{options: string}
。(字詞「options:」必須如所示精確輸入,並至少接一個空格。)在此形式中,string 會複製到 HTML img
標籤中,作為 src
屬性後面的屬性,或複製到 LaTeX 中 \Figure
巨集的第二個參數,預設用作 \includegraphics
呼叫的選項。由於不太可能有任何單一字串足以應付兩種顯示模式,因此專家形式通常會包在條件式中。作者有責任確保使用合法的 HTML/LaTeX。例如,要在 HTML(使用簡式)和 LaTeX(使用專家形式)中包含標誌,可以使用下列內容
\if{html}{\figure{Rlogo.svg}{options: width=100 alt="R logo"}} \if{latex}{\figure{Rlogo.pdf}{options: width=0.5in}}
包含圖形的檔案應儲存在目錄 man/figures 中。安裝時,具有副檔名 .jpg、.jpeg、.pdf、.png 和 .svg 的檔案將從該目錄複製到 help/figures 目錄。(PDF 格式的圖形不會顯示在大部分 HTML 瀏覽器中,但可能是參考手冊中的最佳選擇。)在 \figure
指令中,相對於 man/figures 指定檔名。
使用 \R
表示 R 系統本身。\dots
巨集是使用文字「…」作為函數引數清單中省略符號的傳統替代方式;使用 \ldots
表示一般文字中的省略符號。119 這些巨集後方可以接續 {}
,除非後方為空白,否則應接續。
在未跳脫的「%」之後,您可以放置您自己關於說明文字的註解。該行的其餘部分(但不包含結尾的新行)將會完全忽略。因此,您也可以使用它讓「說明」的一部分隱藏起來。
您可以透過另一個反斜線跳脫反斜線(「\」)來產生反斜線。(請注意 \cr
用於產生換行符號。)
「註解」字元「%」和未配對的大括弧120 幾乎總是需要透過「\」跳脫,而「\\」可以用於反斜線,且當有兩個或兩個以上相鄰的反斜線時需要使用它。在類似 R 的程式碼中,引號字串的處理方式略有不同;有關詳細資訊,請參閱 “Parsing Rd files” – 特別是,引號字串中的大括弧不應跳脫。
所有「% { } \」都應在類似 LaTeX 的文字中跳脫。
可能需要在不同編碼中以不同方式表示的文字應標示為 \enc
,例如 \enc{Jöreskog}{Joreskog}
(大括號之間沒有空白),其中第一個參數將用於允許編碼的地方,而第二個參數應為 ASCII(並用於例如無法表示編碼形式的語言環境中的文字轉換)。(這旨在用於個別單字,而非整句或段落。)
\alias
指令(請參閱 文件函數)用於指定文件中的「主題」,其中應包括封裝中所有 R 物件,例如函數和變數、資料集,以及 S4 類別和方法(請參閱 文件 S4 類別和方法)。線上說明系統會搜尋由所有別名主題組成的索引資料庫。
此外,可以使用 \concept
提供「概念索引條目」,可用於 help.search()
查詢。例如,標準封裝 stats 中的檔案 cor.test.Rd 包含
\concept{Kendall correlation coefficient} \concept{Pearson correlation coefficient} \concept{Spearman correlation coefficient}
因此例如 ??Spearman 會成功找到使用 Spearman 的 rho 對配對樣本之間關聯進行測試的說明頁面。
(請注意,help.search()
僅使用沒有額外標記的文件物件的「區段」。)
每個 \concept
條目都應提供單一索引詞彙(字詞或片語),且不使用任何 Rd 標記。
如果您想透過 \link
via 從其他說明檔案交叉參照此類項目,您需要使用 \alias
,而不是 \concept
。
有時說明文件需要依據平台而有所不同。目前有兩個作業系統特定的選項可用,分別為「unix」和「windows」,而說明來源檔案中的行可以包含在
#ifdef OS ... #endif
或
#ifndef OS ... #endif
中,以進行作業系統特定的包含或排除。此類區塊不應巢狀,且應完全包含在一個區塊中(亦即在章節或項目的開頭和結尾大括號之間),或在頂層包含一個或多個完整章節。
如果平台之間的差異很大,或說明的 R 物件僅與一個平台相關,則可以將平台特定的 Rd 檔案放在 unix 或 windows 子目錄中。
偶爾,最適合某一輸出格式的內容與最適合另一種格式的內容不同。對於這種情況,請使用 \if{format}{text}
或 \ifelse{format}{text}{alternate}
標記。其中 format 是以逗號分隔的格式清單,text 應以這些格式呈現。如果格式不符,則會呈現 alternate。 text 和 alternate 都可以是任何文字和標記的順序。
目前識別的格式如下:example
、html
、latex
和 text
。這些會針對對應的目標選取輸出。(請注意,example
指的是萃取的範例程式碼,而不是以其他格式顯示的範例。)另外也接受 TRUE
(符合所有格式)和 FALSE
(不符合任何格式)。這些可能是 \Sexpr
巨集的輸出(請參閱 動態頁面)。
\out{literal}
巨集通常會用於 \if{format}{text}
的 text 部分中。它會讓渲染器精確輸出文字,不會嘗試跳脫特殊字元。例如,使用下列內容在 LaTeX 或 HTML 中輸出顯示希臘字母所需的標記,以及在其他格式中輸出文字字串 alpha
\ifelse{latex}{\out{$\alpha$}}{\ifelse{html}{\out{α}}{alpha}}
支援動態產生說明頁面的兩個巨集為 \Sexpr
和 \RdOpts
。這些巨集是仿照 Sweave 建模,目的是在 Rd 檔案中包含可執行的 R 運算式。
傳遞給 \Sexpr
的主要參數必須是可以執行的有效 R 程式碼。它也可以在主要參數前加上方括弧中的選項。根據選項,程式碼可以在封裝建置時間、封裝安裝時間或手冊頁面呈現時間執行。
選項遵循與 Sweave 相同的格式,但支援不同的選項。目前允許的選項及其預設值為
eval=TRUE
是否應評估 R 程式碼。
echo=FALSE
是否應顯示 R 程式碼。如果為 TRUE
,將在預格式化區塊中顯示。例如,\Sexpr[echo=TRUE]{ x <- 1 }
將顯示為
> x <- 1
keep.source=TRUE
在顯示程式碼時是否保留作者的格式設定,或將其丟棄並使用已剖析的版本。
results=text
結果應如何顯示?可能性包括
results=text
對程式碼的結果套用 as.character()
,並將其插入為文字元素。
results=verbatim
列印程式碼的結果,就像在主控台中執行一樣,並逐字包含列印的結果。(隱藏的結果不會列印。)
results=rd
假設結果是一個字元向量,其中包含要傳遞給 parse_Rd()
的標記,結果會插入適當的位置。例如,這可以用於插入計算別名。首先使用 fragment = FALSE
呼叫 parse_Rd()
,以允許插入單一 Rd 區段巨集。如果失敗,則再次使用 fragment = TRUE
呼叫它,這是較舊的行為。
results=hide
不插入任何輸出。
strip.white=true
如果 strip.white=true
(或 TRUE
),則移除逐字輸出中的前導和尾隨空白行。使用 strip.white=all
,移除所有空白行。
stage=install
控制這個巨集何時執行。可能的值包括
stage=build
巨集在建置原始碼 tarball 時執行。
stage=install
巨集在從原始碼安裝時執行。
stage=render
巨集在顯示說明頁面時執行。
條件式(例如 #ifdef
,請參閱 特定於平台的文件)會在 build
巨集之後,但 install
巨集之前套用。在某些情況下(例如直接從原始碼目錄安裝,而無 tarball,或建置二進位套件),上述說明並不完全正確,但作者可以依賴 build
、#ifdef
、install
、render
的順序,並執行所有階段。
每個階段只執行一次程式碼,因此 \Sexpr[results=rd]
巨集可以輸出一個 \Sexpr
巨集,專門設計用於後續階段,但不用於目前階段或任何較早階段。
width, height, fig
目前允許這些選項,但會忽略。
\RdOpts
巨集用於設定新的預設值,以套用至 \Sexpr
的後續使用。
如需更多詳細資訊,請參閱線上文件 “剖析 Rd 檔案”。
\newcommand
和 \renewcommand
巨集允許在 Rd 檔案中定義新的巨集。這些巨集類似於同名的 LaTeX 巨集,但並不完全相同。
它們各取兩個參數,並逐字分析。第一個是新巨集的名稱,包含初始反斜線,第二個是巨集定義。如同 LaTeX 中,\newcommand
要求新巨集之前未曾定義,而 \renewcommand
則允許取代現有巨集(包括所有內建巨集)。(此測試預設為停用,但可透過將環境變數 _R_WARN_DUPLICATE_RD_MACROS_
設為 true 值來啟用。)
同樣地,如同 LaTeX 中,新巨集可以定義為接受參數,且數字佔位符(例如 #1
)用於巨集定義中。然而,與 LaTeX 不同的是,參數數量會從巨集定義中看到的最高佔位符數字自動決定。例如,包含 #1
和 #3
(但沒有其他佔位符)的巨集定義,將定義一個三個參數的巨集(其第二個參數將被忽略)。如同 LaTeX 中,最多可以定義 9 個參數。如果 #
字元後接非數字,它將沒有特殊意義。使用者定義巨集的所有參數都將逐字分析,且將使用簡單的文字替換來取代佔位符,之後將分析替換文字。
許多巨集定義在 R 原始碼或主目錄的檔案 share/Rd/macros/system.Rd 中,且這些巨集通常會在所有 .Rd 檔案中可用。例如,該檔案包含定義
\newcommand{\PR}{\Sexpr[results=rd]{tools:::Rd_expr_PR(#1)}}
其中定義 \PR
為單一參數巨集;然後程式碼(通常用於 NEWS.Rd 檔案)如下
\PR{1234}
將擴充為
\Sexpr[results=rd]{tools:::Rd_expr_PR(1234)}
在解析時。
一些可能會有用的一般巨集是
請參閱 share/Rd/macros 中的 system.Rd 檔案,以取得更多詳細資料和巨集定義,包括巨集 \packageTitle
、\packageDescription
、\packageAuthor
、\packageMaintainer
、\packageDESCRIPTION
和 \packageIndices
。
套件也可以定義自己的共用巨集;這些巨集會儲存在套件來源中 man/macros 的 .Rd 檔案中,並會在安裝套件時安裝到 help/macros。套件也可以透過在 DESCRIPTION 檔案的「RdMacros」欄位中列出其他套件,來使用其他套件的巨集。
Rd 檔案為文字檔案,因此無法推論其編碼方式,除非為 ASCII:具有 8 位元字元的檔案可能是 UTF-8、Latin-1、Latin-9、KOI8-R、EUC-JP、等。因此,如果編碼方式並非 ASCII,則必須使用 \encoding{}
區段來指定編碼方式。(\encoding{}
區段必須獨立成一行,且特別是不能包含任何非 ASCII 字元。如果檔案中未宣告編碼方式,則會使用 DESCRIPTION 檔案中宣告的編碼方式。)Rd 檔案會在剖析前轉換為 UTF-8,因此檔案本身偏好的編碼方式現在為 UTF-8。
在 Rd 檔案中,應盡可能避免使用非 ASCII 字元,甚至在「verbatim」環境之外,也不應使用「<」、「>」、「$」、「^」、「&」、「|」、「@」、「~」和「*」等符號(因為這些符號可能會在設計用於呈現文字的字型中消失)。(套件 tools 中的函數 showNonASCIIfile
可協助在檔案中找出非 ASCII 位元組。)
為方便起見,編碼名稱「latin1」和「latin2」會一律辨識:這些名稱和「UTF-8」很可能會廣泛使用。不過,這並不表示 UTF-8 中的所有字元都會被辨識,而且非拉丁字元的涵蓋範圍121相當低。使用 LaTeX inputenx
(請參閱 R 中的 ?Rd2pdf
)將提供 UTF-8 更廣泛的涵蓋範圍。
\enc
指令(請參閱 插入)可用於提供轉寫,這些轉寫將用於不支援已宣告編碼的轉換。
LaTeX 轉換會將檔案從已宣告的編碼轉換成 UTF-8,並包含一個
\inputencoding{utf8}
指令,而這需要搭配適當呼叫 \usepackage{inputenc}
指令。R 工具程式 R CMD Rd2pdf
會檢視轉換後的程式碼並包含所使用的編碼:例如,它可能會使用
\usepackage[utf8]{inputenc}
(使用 utf8
作為編碼需要 LaTeX 日期為 2003/12/01 或更新版本。此外,在「UTF-8」中使用西里爾字母似乎也需要「\usepackage[T2A]{fontenc}」,而 R CMD Rd2pdf
會根據檔案 t2aenc.def 的存在和環境變數 _R_CYRILLIC_TEX_
的設定,有條件地包含這項設定。)
請注意,此機制最適合搭配拉丁字母:LaTeX 中 UTF-8 的涵蓋範圍相當低。
系統命令列中有多個指令可以處理 Rd 檔案。
使用 R CMD Rdconv
可以將 R 文件格式轉換成其他格式,或擷取可執行範例以進行執行時間測試。目前支援的轉換包括純文字、HTML 和 LaTeX,以及範例擷取。
R CMD Rd2pdf
會從 Rd 檔案中的文件產生 PDF 輸出,可以明確指定檔案,或透過路徑指定套件來源的目錄。在後一種情況下,會為套件中所有有文件說明的物件建立參考手冊,其中包含 DESCRIPTION 檔案中的資訊。
R CMD Sweave
和 R CMD Stangle
會處理類似摘要的文件檔案(例如附檔名為「.Snw」或「.Rnw」的 Sweave 摘要,或其他非 Sweave 摘要)。R CMD Stangle
用於擷取 R 程式碼片段。
所有這些指令的詳細用法和可用選項清單,可以透過執行 R CMD command --help
取得,例如 R CMD Rdconv --help。所有可用指令可以使用 R --help(或 Windows 中的 Rcmd --help)列出。
這些指令都在 Windows 中運作。您可能需要安裝工具來從來源建立套件,如「R 安裝和管理」手冊中所述,不過通常只需要安裝 LaTeX 即可。
使用了解其語法並會突顯指令、縮排以顯示結構和偵測不匹配的大括號等的編輯器來準備 .Rd 檔案會非常有幫助。
最常使用於此的系統是 Emacs
(包含 XEmacs
)的某個版本,並搭配 ESS 套件(https://ESS.R-project.org/:它通常會與 Emacs
一起安裝,但可能需要另外載入,甚至安裝)。
另一個是搭配 Stat-ET 外掛程式的 Eclipse IDE(https://projects.eclipse.org/projects/science.statet),以及(僅限 Windows)Tinn-R(https://sourceforge.net/projects/tinn-r/)。
人們也使用編輯器的 LaTeX 模式,因為 .Rd 檔案與 LaTeX 檔案相當類似。
有些 R 前端提供 .Rd 檔案的編輯支援,例如 RStudio(https://posit.co/)。
值得封裝在套件中並讓其他人使用的 R 程式碼,值得文件化、整理,甚至最佳化。後兩項活動是本章的主題。
R 以不同的方式處理從套件載入的函式程式碼和使用者輸入的程式碼。預設情況下,使用者輸入的程式碼會將原始碼儲存在內部,列出函式時,會複製原始原始碼。從套件載入程式碼(預設情況下)會捨棄原始碼,函式清單會從函式的剖析樹重新建立。
通常保留原始碼是個好主意,特別是避免從原始碼中移除註解。然而,我們可以利用從剖析樹重新建立函式清單的能力,產生函式的整理版本,例如,運算子周圍具有相符的縮排和空格。如果原始原始碼不遵循標準格式,這個整理過的版本會更容易閱讀。
我們可以用兩種方式顛覆原始碼的保留。
keep.source
可以設定為 FALSE
,然後再將程式碼載入 R。
removeSource()
函數移除,例如透過
myfun <- removeSource(myfun)
在每種情況下,如果我們列出函數,我們將取得標準配置。
假設我們有一個函數檔案 myfuns.R,我們想要整理它。建立一個包含下列內容的檔案 tidy.R
source("myfuns.R", keep.source = FALSE) dump(ls(all.names = TRUE), file = "new.myfuns.R")
並執行 R,並將其作為原始檔,例如透過 R --vanilla < tidy.R 或貼到 R 執行階段。然後檔案 new.myfuns.R 將包含標準配置中以字母順序排列的函數。警告:函數中的註解將會遺失。
標準格式提供了一個良好的起點,以便進一步整理。儘管 deparsing 無法執行此操作,但我們建議一致使用偏好的賦值運算子「<-」(而非「=」)進行賦值。許多套件作者使用 Emacs(類似 Unix 或 Windows)的一個版本來編輯 R 程式碼,使用 ESS Emacs 套件的 ESS[S] 模式。請參閱 R Internals 中的 R 編碼標準,以取得建議用於 R 本身原始程式碼的 ESS[S] 模式中的樣式選項。
可以在 Windows 和大多數122 類 Unix 版本的 R 上對 R 程式碼進行剖析。
命令 Rprof
用於控制剖析,可以參閱其說明頁面以取得完整詳細資料。剖析透過在固定間隔123(預設每 20 毫秒)記錄正在使用哪個 R 函式的哪一行,並將結果記錄在檔案中(工作目錄中的預設 Rprof.out)。然後可以使用函式 summaryRprof
或命令列公用程式 R CMD Rprof Rprof.out
來摘要活動。
舉例來說,考量以下程式碼(來自 Venables & Ripley,2002,第 225-6 頁)。
library(MASS); library(boot) storm.fm <- nls(Time ~ b*Viscosity/(Wt - c), stormer, start = c(b=30.401, c=2.2183)) st <- cbind(stormer, fit=fitted(storm.fm)) storm.bf <- function(rs, i) { st$Time <- st$fit + rs[i] tmp <- nls(Time ~ (b * Viscosity)/(Wt - c), st, start = coef(storm.fm)) tmp$m$getAllPars() } rs <- scale(resid(storm.fm), scale = FALSE) # remove the mean Rprof("boot.out") storm.boot <- boot(rs, storm.bf, R = 4999) # slow enough to profile Rprof(NULL)
執行後,我們可以透過以下方式摘要結果
R CMD Rprof boot.out Each sample represents 0.02 seconds. Total run time: 22.52 seconds. Total seconds: time spent in function and callees. Self seconds: time spent in function alone.
% total % self total seconds self seconds name 100.0 25.22 0.2 0.04 "boot" 99.8 25.18 0.6 0.16 "statistic" 96.3 24.30 4.0 1.02 "nls" 33.9 8.56 2.2 0.56 "<Anonymous>" 32.4 8.18 1.4 0.36 "eval" 31.8 8.02 1.4 0.34 ".Call" 28.6 7.22 0.0 0.00 "eval.parent" 28.5 7.18 0.3 0.08 "model.frame" 28.1 7.10 3.5 0.88 "model.frame.default" 17.4 4.38 0.7 0.18 "sapply" 15.0 3.78 3.2 0.80 "nlsModel" 12.5 3.16 1.8 0.46 "lapply" 12.3 3.10 2.7 0.68 "assign" ...
% self % total self seconds total seconds name 5.7 1.44 7.5 1.88 "inherits" 4.0 1.02 96.3 24.30 "nls" 3.6 0.92 3.6 0.92 "$" 3.5 0.88 28.1 7.10 "model.frame.default" 3.2 0.80 15.0 3.78 "nlsModel" 2.8 0.70 9.8 2.46 "qr.coef" 2.7 0.68 12.3 3.10 "assign" 2.5 0.64 2.5 0.64 ".Fortran" 2.5 0.62 7.1 1.80 "qr.default" 2.2 0.56 33.9 8.56 "<Anonymous>" 2.1 0.54 5.9 1.48 "unlist" 2.1 0.52 7.9 2.00 "FUN" ...
這通常會產生令人驚訝的結果,可用於識別瓶頸或可以透過編譯程式碼取代的 R 程式碼片段。
兩個警告:剖析會造成輕微的效能損失,而且如果在預設抽樣間隔剖析長時間執行,輸出檔案可能會非常龐大。
剖析短時間執行有時可能會產生誤導性結果。R 會不時執行垃圾回收以回收未使用的記憶體,這需要大量時間,剖析會將這段時間算入觸發它的任何函式。在呼叫 gc()
之後立即剖析程式碼,並與未先呼叫 gc
的剖析執行結果進行比較,這可能會很有用。
可以透過 CRAN 套件 proftools 和 profr 中的工具對輸出進行更詳細的分析:特別是這些工具允許研究呼叫圖。
在 R 程式碼中衡量記憶體使用量非常有用,無論程式碼佔用的記憶體是否超過可用的記憶體,或是物件的記憶體配置和複製導致程式碼執行速度變慢。有 3 種方法可以在 R 程式碼中剖析記憶體使用量。第二和第三種方法要求 R 已使用 --enable-memory-profiling 編譯,這並非預設值,但目前 macOS 和 Windows 二進位發行版都使用此值。所有方法都可能產生誤導,原因各不相同。
在了解記憶體剖析時,多了解一些 R 的記憶體配置會很有幫助。查看 gc()
的結果會顯示記憶體分為用於儲存向量內容的 Vcells
,以及用於儲存所有其他內容的 Ncells
,包括向量的所有管理開銷,例如類型和長度資訊。事實上,向量內容分為兩個區塊。小向量(預設為 128 位元組或更少)的記憶體會以大區塊取得,然後由 R 分配;較大向量的記憶體會直接從作業系統取得。
在已詮釋程式碼中,有些記憶體配置很明顯,例如:
y <- x + 1
配置新向量 y
的記憶體。其他記憶體配置較不明顯,而且會發生是因為 R
被迫兌現其「按值呼叫」引數傳遞的承諾。當引數傳遞給函數時,並不會立即複製。只有在修改引數時才會進行複製(如果需要)。這可能會導致記憶體使用量令人驚訝。例如,在「調查」套件中,我們有
print.svycoxph <- function (x, ...) { print(x$survey.design, varnames = FALSE, design.summaries = FALSE, ...) x$call <- x$printcall NextMethod() }
可能不會很明顯,指定給 x$call
會導致整個物件 x
被複製。這種複製是為了維持呼叫傳值幻覺,通常是由內部 C 函數 duplicate
執行。
記憶體使用狀況剖析困難的主要原因是垃圾回收。在 R 程式中,記憶體會在明確的時間點配置,但會在垃圾回收執行時釋放。
Rprof
的記憶體統計資料 ¶前一節所述的抽樣分析器 Rprof
可以給予選項 memory.profiling=TRUE
。然後它會在每個抽樣區間寫出 R 記憶體配置在小向量、大向量和 cons 單元格或節點中的總量。它也會寫出內部函數 duplicate
的呼叫次數,此函數用於複製 R 物件。 summaryRprof
提供此資訊的摘要。這可能會產生誤導的主要原因是,記憶體使用量會歸因於在抽樣區間結束時執行的函數。第二個原因是,垃圾回收可能會減少使用的記憶體量,因此函數似乎只使用少量記憶體。在 gctorture
下執行有助於解決這兩個問題:它會減慢程式碼速度以有效增加抽樣頻率,而且會讓每次垃圾回收釋放較少的記憶體量。
第二種記憶體剖析方法使用記憶體配置剖析器 Rprofmem()
,它會在每次配置大向量(針對「大」設定使用者指定的閾值)或為 R 堆配置新記憶體頁面時,寫出堆疊追蹤至輸出檔案。此輸出的摘要函數仍在設計中。
執行前一節的範例,
> Rprofmem("boot.memprof",threshold=1000) > storm.boot <- boot(rs, storm.bf, R = 4999) > Rprofmem(NULL)
顯示除了 boot
中的一些初始和最終工作外,沒有超過 1000 位元的向量配置。
記憶體剖析的第三種方法涉及追蹤特定(假設為大型)R 物件所做的拷貝。對物件呼叫 tracemem
會標記該物件,以便在物件透過 duplicate
或強制轉換為另一種類型拷貝時,或在算術運算中建立相同大小的新物件時,會將訊息列印到標準輸出。這可能會產生誤導的主要原因是,系統不會追蹤物件的子集或組件拷貝。對這些組件使用 tracemem
可能會有幫助。
在上面的範例中,我們可以在資料框 st
上執行 tracemem
> tracemem(st) [1] "<0x9abd5e0>" > storm.boot <- boot(rs, storm.bf, R = 4) memtrace[0x9abd5e0->0x92a6d08]: statistic boot memtrace[0x92a6d08->0x92a6d80]: $<-.data.frame $<- statistic boot memtrace[0x92a6d80->0x92a6df8]: $<-.data.frame $<- statistic boot memtrace[0x9abd5e0->0x9271318]: statistic boot memtrace[0x9271318->0x9271390]: $<-.data.frame $<- statistic boot memtrace[0x9271390->0x9271408]: $<-.data.frame $<- statistic boot memtrace[0x9abd5e0->0x914f558]: statistic boot memtrace[0x914f558->0x914f5f8]: $<-.data.frame $<- statistic boot memtrace[0x914f5f8->0x914f670]: $<-.data.frame $<- statistic boot memtrace[0x9abd5e0->0x972cbf0]: statistic boot memtrace[0x972cbf0->0x972cc68]: $<-.data.frame $<- statistic boot memtrace[0x972cc68->0x972cd08]: $<-.data.frame $<- statistic boot memtrace[0x9abd5e0->0x98ead98]: statistic boot memtrace[0x98ead98->0x98eae10]: $<-.data.frame $<- statistic boot memtrace[0x98eae10->0x98eae88]: $<-.data.frame $<- statistic boot
物件拷貝了十五次,對 storm.bf
的每個 R+1
呼叫拷貝三次。這令人驚訝,因為沒有任何拷貝發生在 nls
內部。在偵錯器中逐步執行 storm.bf
會顯示所有三次都發生在這一行
st$Time <- st$fit + rs[i]
資料框比矩陣慢,這就是原因之一。使用 tracemem(st$Viscosity)
不會顯示任何額外的拷貝。
編譯程式碼的剖析高度系統特定,但本節包含一些從各種 R 使用者收集的提示。有些方法對於編譯的可執行檔和 R 套件使用的動態/共用函式庫/物件必須不同。
本章根據使用者的報告,資訊可能並非最新。
選項包括使用 sprof
來處理共用物件,以及 oprofile
(請參閱 https://oprofile.sourceforge.io/news/)和 perf
(請參閱 https://perf.wiki.kernel.org/index.php/Tutorial)來處理任何可執行檔或共用物件。這些工具似乎比過去提供的範圍更小。還有「Google Performance Tools」,也稱為 gperftools 或 google-perftools。
當 R 和任何套件都已使用偵錯符號建置時,所有這些工具都能發揮最佳效用。
perf
¶這似乎是最廣泛使用的工具。以下是使用 R 4.3.1 在使用 LTO 建置的 x86_64
Linux 上的範例。
最簡單的
perf record R -f tests/Examples/stats-Ex.R perf report --sort=dso perf report --sort=srcfile rm perf.data*
第一個報告是
75.67% R 9.25% libc.so.6 4.87% [unknown] 3.75% libz.so.1.2.11 3.37% stats.so 1.17% libm.so.6 0.63% libtirpc.so.3.0.0 0.41% graphics.so 0.30% grDevices.so 0.20% libRblas.so 0.09% libpcre2-8.so.0.11.0 0.07% methods.so ...
顯示時間花在哪些共用函式庫 (DSO) 中。
perf annotate
可用於使用 GCC 和 -ggdb 建置的應用程式:它會交錯反組譯和原始碼。
oprofile
和 operf
¶oprofile
專案有兩種操作模式。自 0.9.8 版(2012 年 8 月)以來,建議的模式是使用 operf
,因此我們只討論此模式。
讓我們看看 §3.2 中的 boot 範例,在使用 R 4.3.1 的 x86_64
Linux 上。
這可以在 operf
下執行,並透過類似以下的指令進行分析
operf R -f boot.R opreport opreport -l /path/to/R_HOME/bin/exec/R opreport -l /path/to/R_HOME/library/stats/src/stats.so opannotate --source /path/to/R_HOME/bin/exec/R
第一行必須以 root 身分執行。
第一個報告顯示時間花在哪些函式庫 (等) 中
CPU_CLK_UNHALT...| samples| %| ------------------ 278341 91.9947 R 18290 6.0450 libc.so.6 2277 0.7526 kallsyms 1426 0.4713 stats.so 739 0.2442 libRblas.so 554 0.1831 libz.so.1.2.11 373 0.1233 libm.so.6 352 0.1163 libtirpc.so.3.0.0 153 0.0506 ld-linux-x86-64.so.2 12 0.0040 methods.so
(kallsyms
是核心。)
其餘的輸出很龐大,只顯示摘錄。
在 R 中花費的大部分時間是在
samples % image name symbol name 52955 19.0574 R bcEval.lto_priv.0 16484 5.9322 R Rf_allocVector3 14224 5.1189 R Rf_findVarInFrame3 12581 4.5276 R CONS_NR 8289 2.9830 R Rf_matchArgs_NR 8034 2.8913 R Rf_cons 7114 2.5602 R R_gc_internal.lto_priv.0 6552 2.3579 R Rf_eval 5969 2.1481 R VECTOR_ELT 5684 2.0456 R Rf_applyClosure 5497 1.9783 R findVarLocInFrame.part.0.lto_priv.0 4827 1.7371 R Rf_mkPROMISE 4609 1.6587 R Rf_install 4317 1.5536 R Rf_findFun3 4035 1.4521 R getvar.lto_priv.0 3491 1.2563 R SETCAR 3179 1.1441 R Rf_defineVar 2892 1.0408 R duplicate1.lto_priv.0
和 stats.so
中
samples % image name symbol name 285 24.4845 stats.so termsform 284 24.3986 stats.so numeric_deriv 213 18.2990 stats.so modelframe 114 9.7938 stats.so nls_iter 55 4.7251 stats.so ExtractVars 47 4.0378 stats.so EncodeVars 37 3.1787 stats.so getListElement 32 2.7491 stats.so TrimRepeats 25 2.1478 stats.so InstallVar 20 1.7182 stats.so MatchVar 20 1.7182 stats.so isZeroOne 15 1.2887 stats.so ConvInfoMsg.isra.0
預設會將剖析資料儲存在目前目錄的 oprofile_data 子目錄中,可以在工作階段結束時移除。
sprof
¶您可以透過設定環境變數 LD_PROFILE
來使用 sprof
選擇要剖析的共用物件。例如
% setenv LD_PROFILE /path/to/R_HOME/library/stats/libs/stats.so % R -f boot.R % sprof /path/to/R_HOME/library/stats/libs/stats.so \ /var/tmp/path/to/R_HOME/library/stats/libs/stats.so.profile Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls us/call us/call name 76.19 0.32 0.32 0 0.00 numeric_deriv 16.67 0.39 0.07 0 0.00 nls_iter 7.14 0.42 0.03 0 0.00 getListElement ... to clean up ... rm /var/tmp/path/to/R_HOME/library/stats/libs/stats.so.profile
可能需要 root 存取權才能建立用於剖析資料的目錄。
開發人員建議使用 Instruments
(Xcode
的一部分,請參閱 https://help.apple.com/instruments/mac/current/),在 macOS 12 之前,它有一個命令列版本。
Very Sleepy
(https://github.com/VerySleepy/verysleepy) 已被使用。存取偵錯資訊時會出現問題,但透過 GUI 或使用 /a:(pid 透過 Sys.getpid()
取得)將剖析器附加到現有的 Rterm
程序,可以取得包含函數名稱的最佳結果。
本章節涵蓋 R 擴充套件的偵錯,從取得有用的錯誤資訊的方法開始,再到處理導致 R 崩潰的錯誤。
R 層級的大部分偵錯功能都是基於內建瀏覽器。這可透過在函數程式碼中插入呼叫 browser()
直接使用(例如,使用 fix(my_function)
)。當程式碼執行到達函數中的該點時,控制權會傳回 R 主控台,並顯示特殊提示。例如
> fix(summary.data.frame) ## insert browser() call after for() loop > summary(women) Called from: summary.data.frame(women) Browse[1]> ls() [1] "digits" "i" "lbs" "lw" "maxsum" "nm" "nr" "nv" [9] "object" "sms" "z" Browse[1]> maxsum [1] 7 Browse[1]> height weight Min. :58.0 Min. :115.0 1st Qu.:61.5 1st Qu.:124.5 Median :65.0 Median :135.0 Mean :65.0 Mean :136.7 3rd Qu.:68.5 3rd Qu.:148.0 Max. :72.0 Max. :164.0 > rm(summary.data.frame)
在瀏覽器提示中,使用者可以輸入任何 R 表達式,因此例如 ls()
會列出目前框架中的物件,而輸入物件名稱會124列印它。系統也會接受下列指令
n
進入「逐步執行」模式。在此模式中,按 Enter 會執行下一行程式碼(更精確地說,是一行及其任何延續行)。輸入 c
會繼續執行到目前內容的結尾,例如到目前迴圈或函數的結尾。
c
在一般模式中,這會退出瀏覽器並繼續執行,而 just return 的作用方式相同。 cont
是同義字。
where
這會列印呼叫堆疊。例如
> summary(women) Called from: summary.data.frame(women) Browse[1]> where where 1: summary.data.frame(women) where 2: summary(women) Browse[1]>
Q
退出瀏覽器和目前表達式,並傳回頂層提示。
在瀏覽器提示中執行的程式碼中的錯誤通常會將控制權傳回瀏覽器提示。可以透過指定來變更物件,而當瀏覽器結束時,物件會保留其變更的值。如果真的有必要,可以從瀏覽器提示將物件指定給工作區(如果名稱尚未在範圍內,則使用 <<-
)。
假設您的 R 程式提供錯誤訊息。找出錯誤時 R 正在做什麼是第一要務,而最實用的工具是 traceback()
。我們建議在無法立即找出錯誤原因時執行此操作。錯誤通常會回報給 R 郵件清單,表示錯誤出在某個套件中,但 traceback()
會顯示錯誤是由其他套件或 R 基底回報的。以下是回歸套件的範例。
> success <- c(13,12,11,14,14,11,13,11,12) > failure <- c(0,0,0,0,0,0,0,2,2) > resp <- cbind(success, failure) > predictor <- c(0, 5^(0:7)) > glm(resp ~ 0+predictor, family = binomial(link="log")) Error: no valid set of coefficients has been found: please supply starting values > traceback() 3: stop("no valid set of coefficients has been found: please supply starting values", call. = FALSE) 2: glm.fit(x = X, y = Y, weights = weights, start = start, etastart = etastart, mustart = mustart, offset = offset, family = family, control = control, intercept = attr(mt, "intercept") > 0) 1: glm(resp ~ 0 + predictor, family = binomial(link ="log"))
對活動框架的呼叫會以反向順序提供(從最內層開始)。因此我們會看到錯誤訊息來自 glm.fit
中的明確檢查。(traceback()
會顯示函式呼叫的所有行,可透過設定 option
"deparse.max.lines" 來限制行數。)
有時追蹤會指出錯誤是在編譯程式碼中偵測到的,例如(來自 ?nls
)
Error in nls(y ~ a + b * x, start = list(a = 0.12345, b = 0.54321), trace = TRUE) : step factor 0.000488281 reduced below ‘minFactor’ of 0.000976563 > traceback() 2: .Call(R_nls_iter, m, ctrl, trace) 1: nls(y ~ a + b * x, start = list(a = 0.12345, b = 0.54321), trace = TRUE)
如果最內層的呼叫是 .C
、.Fortran
、.Call
、.External
或 .Internal
,就會發生這種情況,但由於此類程式碼也有可能評估 R 表達式,因此這不必是最內層的呼叫,例如
> traceback() 9: gm(a, b, x) 8: .Call(R_numeric_deriv, expr, theta, rho, dir) 7: numericDeriv(form[[3]], names(ind), env) 6: getRHS() 5: assign("rhs", getRHS(), envir = thisEnv) 4: assign("resid", .swts * (lhs - assign("rhs", getRHS(), envir = thisEnv)), envir = thisEnv) 3: function (newPars) { setPars(newPars) assign("resid", .swts * (lhs - assign("rhs", getRHS(), envir = thisEnv)), envir = thisEnv) assign("dev", sum(resid^2), envir = thisEnv) assign("QR", qr(.swts * attr(rhs, "gradient")), envir = thisEnv) return(QR$rank < min(dim(QR$qr))) }(c(-0.00760232418963883, 1.00119632515036)) 2: .Call(R_nls_iter, m, ctrl, trace) 1: nls(yeps ~ gm(a, b, x), start = list(a = 0.12345, b = 0.54321))
偶爾 traceback()
無法提供協助,如果涉及 S4 方法調度,就會發生這種情況。請考慮以下範例
> xyd <- new("xyloc", x=runif(20), y=runif(20)) Error in as.environment(pkg) : no item called "package:S4nswv" on the search list Error in initialize(value, ...) : S language method selection got an error when called from internal dispatch for function ‘initialize’ > traceback() 2: initialize(value, ...) 1: new("xyloc", x = runif(20), y = runif(20))
這沒有太大幫助,因為 initialize
中沒有呼叫 as.environment
(而且備註「由內部調度呼叫」告訴我們這一點)。在這種情況下,我們在 R 來源中搜尋引號呼叫,只在一個地方出現,methods:::.asEnvironmentPackage
。因此我們現在知道錯誤發生在哪裡。(這是一個異常不透明的範例。)
錯誤訊息
evaluation nested too deeply: infinite recursion / options(expressions=)?
使用預設值(5000)時可能難以處理。除非您知道實際上正在進行深度遞迴,否則設定類似
options(expressions=500)
並重新執行顯示錯誤的範例會有幫助。
有時會出現警告,顯然是稍後某個錯誤的前兆,但並不明顯它來自何處。設定 options(warn = 2)
(將警告轉換為錯誤)在此情況下可能有所幫助。
一旦我們找到錯誤,我們有一些選擇。繼續進行的一種方式是透過查看事後驗屍傾印檔,找出在崩潰時發生了什麼事。為此,設定 options(error=dump.frames)
,然後再次執行程式碼。然後呼叫 debugger()
,並探索傾印檔。繼續我們的範例
> options(error = dump.frames) > glm(resp ~ 0 + predictor, family = binomial(link ="log")) Error: no valid set of coefficients has been found: please supply starting values
與之前相同,但工作區中已出現稱為 last.dump
的物件。(此類物件可能很大,因此在不再需要時將其移除。)我們可以在稍後透過呼叫函式 debugger
來檢查它。
> debugger() Message: Error: no valid set of coefficients has been found: please supply starting values Available environments had calls: 1: glm(resp ~ 0 + predictor, family = binomial(link = "log")) 2: glm.fit(x = X, y = Y, weights = weights, start = start, etastart = etastart, mus 3: stop("no valid set of coefficients has been found: please supply starting values Enter an environment number, or 0 to exit Selection:
它提供與 traceback
相同的呼叫順序,但以從外到內的順序,且僅顯示呼叫的第一行,並截斷為目前的寬度。但是,我們現在可以更詳細地檢查錯誤發生時的狀況。選擇環境會在該框架中開啟瀏覽器。因此,我們選擇產生錯誤訊息的函式呼叫,並探索一些變數(並執行兩個函式呼叫)。
Enter an environment number, or 0 to exit Selection: 2 Browsing in the environment with call: glm.fit(x = X, y = Y, weights = weights, start = start, etas Called from: debugger.look(ind) Browse[1]> ls() [1] "aic" "boundary" "coefold" "control" "conv" [6] "dev" "dev.resids" "devold" "EMPTY" "eta" [11] "etastart" "family" "fit" "good" "intercept" [16] "iter" "linkinv" "mu" "mu.eta" "mu.eta.val" [21] "mustart" "n" "ngoodobs" "nobs" "nvars" [26] "offset" "start" "valideta" "validmu" "variance" [31] "varmu" "w" "weights" "x" "xnames" [36] "y" "ynames" "z" Browse[1]> eta 1 2 3 4 5 0.000000e+00 -2.235357e-06 -1.117679e-05 -5.588393e-05 -2.794197e-04 6 7 8 9 -1.397098e-03 -6.985492e-03 -3.492746e-02 -1.746373e-01 Browse[1]> valideta(eta) [1] TRUE Browse[1]> mu 1 2 3 4 5 6 7 8 1.0000000 0.9999978 0.9999888 0.9999441 0.9997206 0.9986039 0.9930389 0.9656755 9 0.8397616 Browse[1]> validmu(mu) [1] FALSE Browse[1]> c Available environments had calls: 1: glm(resp ~ 0 + predictor, family = binomial(link = "log")) 2: glm.fit(x = X, y = Y, weights = weights, start = start, etastart = etastart 3: stop("no valid set of coefficients has been found: please supply starting v Enter an environment number, or 0 to exit Selection: 0 > rm(last.dump)
由於 last.dump
可以稍後或甚至在其他 R 工作階段中查看,因此即使對於 R 的批次使用,事後驗屍除錯也是可行的。我們確實需要安排儲存傾印檔:可以使用命令列旗標 --save 在執行結束時儲存工作區,或透過設定,例如
> options(error = quote({dump.frames(to.file=TRUE); q()}))
請參閱 dump.frames
的說明,以取得更多選項和實際範例。
另一種錯誤動作是使用函數 recover()
> options(error = recover) > glm(resp ~ 0 + predictor, family = binomial(link = "log")) Error: no valid set of coefficients has been found: please supply starting values Enter a frame number, or 0 to exit 1: glm(resp ~ 0 + predictor, family = binomial(link = "log")) 2: glm.fit(x = X, y = Y, weights = weights, start = start, etastart = etastart Selection:
它與 dump.frames
非常類似。但是,我們可以直接檢查程式狀態,而無需傾印和重新載入傾印。如其說明頁面所述,recover
可以例行用作錯誤動作,取代 dump.calls
和 dump.frames
,因為它在非互動使用中表現得像 dump.frames
。
事後除錯對於找出問題所在非常有幫助,但未必能找出原因。另一種方法是仔細查看錯誤發生前的狀況,而使用 debug
是個不錯的方法。這會在函數開頭插入一個對瀏覽器的呼叫,從逐步執行模式開始。因此,在我們的範例中,我們可以使用
> debug(glm.fit) > glm(resp ~ 0 + predictor, family = binomial(link ="log")) debugging in: glm.fit(x = X, y = Y, weights = weights, start = start, etastart = etastart, mustart = mustart, offset = offset, family = family, control = control, intercept = attr(mt, "intercept") > 0) debug: { ## lists the whole function Browse[1]> debug: x <- as.matrix(x) ... Browse[1]> start [1] -2.235357e-06 debug: eta <- drop(x %*% start) Browse[1]> eta 1 2 3 4 5 0.000000e+00 -2.235357e-06 -1.117679e-05 -5.588393e-05 -2.794197e-04 6 7 8 9 -1.397098e-03 -6.985492e-03 -3.492746e-02 -1.746373e-01 Browse[1]> debug: mu <- linkinv(eta <- eta + offset) Browse[1]> mu 1 2 3 4 5 6 7 8 1.0000000 0.9999978 0.9999888 0.9999441 0.9997206 0.9986039 0.9930389 0.9656755 9 0.8397616
(提示 Browse[1]>
表示這是瀏覽的第一個層級:可以逐步進入另一個正在除錯的函數,或包含對 browser()
的呼叫。)
debug
可用於隱藏函數和 S3 方法,例如 debug(stats:::predict.Arima)
。(它不能用於 S4 方法,但 debug
的說明頁面提供了替代方法。)有時您想要除錯在另一個函數內定義的函數,例如在 arima
內定義的函數 arimafn
。為此,請在外部函數(這裡是 arima
)上設定 debug
,並逐步執行它,直到定義了內部函數。然後在內部函數上呼叫 debug
(並使用 c
退出外部函數的逐步執行模式)。
若要移除函式的偵錯,請使用 undebug
呼叫先前提供給 debug
的引數;否則,偵錯會持續到 R 會話結束(或直到函式被編輯或以其他方式取代)。
trace
可用於暫時將偵錯程式碼插入函式中,例如在錯誤點之前插入呼叫 browser()
。回到我們的執行範例
## first get a numbered listing of the expressions of the function > page(as.list(body(glm.fit)), method="print") > trace(glm.fit, browser, at=22) Tracing function "glm.fit" in package "stats" [1] "glm.fit" > glm(resp ~ 0 + predictor, family = binomial(link ="log")) Tracing glm.fit(x = X, y = Y, weights = weights, start = start, etastart = etastart, .... step 22 Called from: eval(expr, envir, enclos) Browse[1]> n ## and single-step from here. > untrace(glm.fit)
對於您自己的函式,使用 fix
插入暫時程式碼可能會很簡單,但 trace
可以協助命名空間中的函式(fixInNamespace
也可以)。或者,使用 trace(,edit=TRUE)
以視覺方式插入程式碼。
在某些機器上,記憶體配置錯誤以及在陣列外讀取/寫入是導致崩潰(例如,區段錯誤)的常見原因。崩潰通常會在無效記憶體存取之後很長一段時間才出現:特別是,R 本身配置的結構損壞可能只會在下次垃圾回收時(或甚至在刪除物件之後的後續垃圾回收時)才會顯現。
請注意,使用 LAPACK、BLAS、OpenMP 和 Java 的套件可能會看到記憶體存取錯誤:其中一些似乎是故意的,而有些則與將字元傳遞給 Fortran 有關。
其中一些工具可以偵測到不匹配的配置和解除配置。C++ 程式設計師應注意,由 new []
配置的記憶體必須由 delete []
釋放,其他由 new
使用的 delete
,以及由 malloc
、calloc
和 realloc
配置的記憶體由 free
釋放。有些平台會容忍不匹配(可能會有記憶體外洩),但其他平台會區段錯誤。
我們可以透過盡可能頻繁執行垃圾回收來協助提早偵測 R 物件中的記憶體問題。這可透過 gctorture(TRUE)
來達成,其說明頁中說明
在(幾乎)每次記憶體配置時觸發垃圾回收。用於找出記憶體保護錯誤。但不幸的是,也會讓 R 執行得非常慢。
「記憶體保護」是指遺漏對 PROTECT
/UNPROTECT
的 C 層級呼叫(請參閱 處理垃圾回收的影響),如果遺漏,會讓 R 物件在仍被使用時遭到垃圾回收。但它也可以協助處理其他與記憶體相關的錯誤。
通常在 gctorture(TRUE)
下執行只會讓 R 程式在較早階段崩潰,希望接近實際原因。請參閱下一章節,了解如何解讀此類崩潰。
透過使用選項 --use-gct,可以在 gctorture(TRUE)
下執行 R CMD check
所涵蓋的所有範例、測試和範例。
函數 gctorture2
提供對 GC 測試程序更精細的控制。其參數 step
、wait
和 inhibit_release
已記錄在說明頁中。環境變數也可以在 R 會話開始時用於開啟 GC 測試:R_GCTORTURE
對應於 gctorture2
的 step
參數,R_GCTORTURE_WAIT
對應於 wait
,R_GCTORTURE_INHIBIT_RELEASE
對應於 inhibit_release
。
如果 R 使用 --enable-strict-barrier 進行組態,則會啟用各種寫入屏障完整性測試。此外,還會啟用有助於偵測保護問題的測試
NEWSXP
。
NEWSXP
類型的空閒節點都會標記為 FREESXP
類型,並記錄其先前的類型。
SEXP
輸入和 SEXP
輸出,如果發現 FREESXP
,則會發出錯誤訊號。節點的位址和舊類型會包含在錯誤訊息中。
可以設定 R CMD check --use-gct
以使用 gctorture2(n)
,而不是 gctorture(TRUE)
,方法是將環境變數 _R_CHECK_GCT_N_
設定為正整數值,以用作 n
。
與偵錯器和 gctorture
或 gctorture2
搭配使用時,此機制有助於找出記憶體保護問題。
如果您可以在常見的 CPU 類型或受支援版本的 FreeBSD 或 Solaris125上存取 Linux,您可以使用 valgrind
(https://valgrind.org/,發音與「tinned」押韻)來檢查可能的錯誤。要在 valgrind
下執行一些範例,請使用類似下列指令
R -d valgrind --vanilla < mypkg-Ex.R R -d "valgrind --tool=memcheck --leak-check=full" --vanilla < mypkg-Ex.R
其中 mypkg-Ex.R 是一組範例,例如 R CMD check
在 mypkg.Rcheck 中建立的檔案。偶爾會回報「未初始化值」的記憶體讀取,這是編譯器最佳化的結果,因此值得在未最佳化的編譯下檢查:要取得最大的資訊,請使用具有偵錯符號的建置。我們知道 readline
和 R 本身會有一些小的記憶體外洩——這些記憶體區域在 R 工作階段結束前都會使用。預期執行速度會比沒有 valgrind
的情況慢 20 倍左右,在某些情況下會更慢。數個版本的 valgrind
對使用 CPU 特定指令的一些最佳化 BLAS 函式庫不滿意,因此您可能需要特別建置 R 版本才能與 valgrind
搭配使用。
在已安裝 valgrind
及其標頭126 的平台上,您可以建立一個 R 版本,並提供額外的工具,以協助 valgrind
偵測從 R 堆疊中配置的記憶體使用錯誤。configure
選項為 --with-valgrind-instrumentation=level,其中 level 為 0、1 或 2。預設為等級 0,且不會新增任何內容。等級 1 會偵測未初始化記憶體的某些使用情況127,且對速度的影響不大(與等級 0 相較)。等級 2 會偵測許多其他記憶體使用錯誤128,但在 valgrind
下執行時,會讓 R 變得更慢。將其與 gctorture
搭配使用,可能會更有效(甚至更慢)。
強烈建議安裝 valgrind
標頭(有時會個別封裝,例如 valgrind-devel),以建立一個經過工具化的 R:這在未來會是必要的。
valgrind
輸出的範例為
==12539== Invalid read of size 4 ==12539== at 0x1CDF6CBE: csc_compTr (Mutils.c:273) ==12539== by 0x1CE07E1E: tsc_transpose (dtCMatrix.c:25) ==12539== by 0x80A67A7: do_dotcall (dotcode.c:858) ==12539== by 0x80CACE2: Rf_eval (eval.c:400) ==12539== by 0x80CB5AF: R_execClosure (eval.c:658) ==12539== by 0x80CB98E: R_execMethod (eval.c:760) ==12539== by 0x1B93DEFA: R_standardGeneric (methods_list_dispatch.c:624) ==12539== by 0x810262E: do_standardGeneric (objects.c:1012) ==12539== by 0x80CAD23: Rf_eval (eval.c:403) ==12539== by 0x80CB2F0: Rf_applyClosure (eval.c:573) ==12539== by 0x80CADCC: Rf_eval (eval.c:414) ==12539== by 0x80CAA03: Rf_eval (eval.c:362) ==12539== Address 0x1C0D2EA8 is 280 bytes inside a block of size 1996 alloc'd ==12539== at 0x1B9008D1: malloc (vg_replace_malloc.c:149) ==12539== by 0x80F1B34: GetNewPage (memory.c:610) ==12539== by 0x80F7515: Rf_allocVector (memory.c:1915) ...
這個範例來自經過工具化的 R 版本,同時在 2006 年追蹤 Matrix 套件中的錯誤。第一行指出 R 已嘗試從它無權存取的記憶體位址讀取 4 個位元組。接著是一個 C 堆疊追蹤,顯示錯誤發生在哪裡。接下來是對已存取記憶體的說明。它位於由 malloc
配置的區塊內,從 GetNewPage
呼叫,亦即在 R 內部堆疊中。由於這個記憶體全部屬於 R,因此 valgrind
偵測不到(而且沒有偵測到)未經過工具化的 R 建置中的問題。在此範例中,堆疊追蹤足以隔離並修正錯誤,該錯誤位於 tsc_transpose
中,且在此範例中,在 gctorture()
下執行並未提供任何其他資訊。
valgrind
擅長找出未初始化值的用法:使用選項 --track-origins=yes 來顯示這些值從何而來。它無法偵測的是堆疊上配置的陣列的錯誤使用:這包括 C 自動變數和一些129 Fortran 陣列。
可以使用選項 --use-valgrind 在 valgrind
下執行 R CMD check
涵蓋的所有範例、測試和範例。如果您這麼做,您將需要以其他方式選取 valgrind
選項,例如有一個包含下列內容的 ~/.valgrindrc 檔案
--leak-check=full --track-origins=yes
或設定環境變數 VALGRIND_OPTS
。從 R 4.2.0 開始,--use-valgrind 在重新建構範例時也會使用 valgrind
。
本節已說明 memtest
的用法,這是 valgrind
工具中預設(且最有用的)。其文件說明中描述了其他工具:helgrind
可用於執行緒程式。
AddressSanitizer
(「ASan」)是一個工具,其目標與 valgrind
中的記憶體檢查器類似。它可在 gcc
和 clang
的合適建置中使用,適用於常見的 Linux 和 macOS 平台。請參閱 https://clang.llvm.org/docs/UsersManual.html#controlling-code-generation、https://clang.llvm.org/docs/AddressSanitizer.html 和 https://github.com/google/sanitizers。
如果 C++ 函式庫已「加註解」,則會對 C++ 程式碼進行更徹底的檢查:在撰寫本文時,這適用於 libc++
中的 std::vector
,供與 clang
搭配使用,並產生「container-overflow」131 報告。
它需要使用 -fsanitize=address 編譯程式碼並連結,而使用 -fno-omit-frame-pointer
編譯會產生更易讀的報告。它的執行時間懲罰為 2-3 倍,編譯時間延長,而且在執行時使用更多記憶體,通常為 1-2GB。在 64 位元平台上,它保留(但不配置)16-20TB 的虛擬記憶體:限制性的 shell 設定可能會造成問題。增加堆疊大小會有幫助,例如增加到 40MB。
與 valgrind
相比,ASan 可以偵測堆疊和全域變數的錯誤使用,但無法偵測未初始化記憶體的使用。
最近的版本會傳回錯誤位置的符號位址,前提是 llvm-symbolizer
132 位於路徑中:如果它可用,但不在路徑中或已重新命名133,則可以使用環境變數,例如
ASAN_SYMBOLIZER_PATH=/path/to/llvm-symbolizer
另一種方法是透過 asan_symbolize.py
134 管道輸出,然後(對於編譯的 C++ 程式碼)使用 c++filt
。(在 macOS 上,您可能需要執行 dsymutil
以取得行號報告。)
使用此功能最簡單的方法是使用類似下列內容建置 R 版本
CC="gcc -std=gnu99 -fsanitize=address" CFLAGS="-fno-omit-frame-pointer -g -O2 -Wall -pedantic -mtune=native"
這將確保 libasan
執行時間函式庫編譯到 R 可執行檔中。不過,可以使用類似下列內容的 ~/.R/Makevars 檔案,針對每個套件啟用此檢查
CC = gcc -std=gnu99 -fsanitize=address -fno-omit-frame-pointer CXX = g++ -fsanitize=address -fno-omit-frame-pointer FC = gfortran -fsanitize=address
(請注意,-fsanitize=address
必須是編譯器規格的一部分,以確保它用於連結。這些設定不會受到忽略 ~/.R/Makevars 的套件採用。)如果在建置 R 時未將它指定為「CC」的一部分,則必須使用
MAIN_LDFLAGS = -fsanitize=address
將執行時期函式庫連結到 R 可執行檔中。(對於某些沒有 OpenMP 的建置,也需要 -pthread。)
對於可透過環境變數 ASAN_OPTIONS
使用的選項,請參閱 https://github.com/google/sanitizers/wiki/AddressSanitizerFlags。使用 gcc
時,可透過 --param 旗標使用其他控制項:請參閱其 man
頁面。
若要取得錯誤的更詳細資訊,可以在產生位址消毒程式報告之前,在偵錯程式下執行 R 並設定中斷點:對於 gdb
或 lldb
,您可以使用
break __asan_report_error
(請參閱 https://github.com/google/sanitizers/wiki/AddressSanitizerAndDebugger。)
較新的版本135 新增了旗標 -fsanitize-address-use-after-scope:請參閱 https://github.com/google/sanitizers/wiki/AddressSanitizerUseAfterScope。
ASan 執行的其中一項檢查是 malloc/free
,而在 C++ 中則是 new/delete
和 new[]/delete[]
,這些檢查會一致地使用(而不是說 free
用來解除配置由 new[]
配置的記憶體)。這在某些系統上很重要,但並非全部:很不幸的是,在某些無關緊要的系統上,系統函式庫136並不一致。可以在 ASAN_OPTIONS
中包含「alloc_dealloc_mismatch=0」來抑制檢查。
ASan 也會檢查系統呼叫,有時報告會提到系統軟體的問題,而非套件或 R。有幾份報告是來自 Tcl/Tk 呼叫的 X11 函式庫中「堆積使用後釋放」的錯誤。
對於 x86_64
Linux,有一個 Leak Sanitizer,即「LSan」:請參閱 https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer。這在最近版本的 gcc
和 clang
中可用,而且在可用的地方會編譯成 ASan 的一部分。
從已啟用 ASan 的建置呼叫這項功能的方法之一是透過環境變數
ASAN_OPTIONS='detect_leaks=1'
不過,這已從 clang
3.5 和 gcc
5.1.0 開始設為預設值。
當 LSan 已啟用時,記憶體外洩會讓處理程序產生失敗錯誤狀態(預設為 23
)。對於 R 套件,這表示 R 處理程序,而且由於剖析器會保留一些記憶體到處理程序結束,如果 R 本身是針對 ASan 建置,所有執行都會有失敗錯誤狀態(這可能包括將 R 作為建置 R 本身的一部分來執行)。
若要停用這項功能,配置錯誤檢查和一些嚴格的 C++ 檢查會使用
setenv ASAN_OPTIONS ‘alloc_dealloc_mismatch=0:detect_leaks=0:detect_odr_violation=0’
LSan 也有「獨立」模式,其中會使用 -fsanitize=leak 編譯,並避免 ASan 的執行時間開銷。
「未定義行為」是指語言標準不要求編譯器有特定行為的地方。範例包括除以零(對於雙精度數,R 要求 ISO/IEC 60559 行為,但 C/C++ 沒有),使用長度為零的陣列、對有號類型進行位移過大(例如 int x, y; y = x << 31;
)、超出範圍的強制轉換、無效的 C++ 轉型和未對齊。R 套件中超出範圍的強制轉換常見範例包括嘗試將 NaN
或無限大強制轉換為 int
類型,或將 NA_INTEGER
轉換為未簽署類型,例如 size_t
。另外常見的錯誤是 y[x - 1]
,忘記 x
可能會是 NA_INTEGER
。
「UBSanitizer」是 C/C++ 原始碼的工具,在適當的 clang
和 GCC137 建置中,由 -fsanitize=undefined 選擇。它的(主要)執行時期函式庫會連結到每個套件的 DLL,因此較少需要包含在 MAIN_LDFLAGS
中。clang
支援的平台列於 https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html#supported-platforms:CRAN 將它用於 C/C++,在「x86_64」Linux 上搭配 GCC 和 clang
:這兩個工具鏈常會突顯不同的地方,且 clang
的報告比 GCC 多。
此消毒程式可與 Address Sanitizer 結合使用,方法為 -fsanitize=undefined,address(兩者皆支援)。
可透過其他選項更精細地控制檢查項目。
對於 clang
,請參閱 https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html#ubsan-checks。目前的設定為(單行)
-fsanitize=alignment,bool,bounds,builtin,enum,float-cast-overflow, float-divide-by-zero,function,implicit-unsigned-integer-truncation, implicit-signed-integer-truncation,implicit-integer-sign-change, integer-divide-by-zero,nonnull-attribute,null,object-size, pointer-overflow,return,returns-nonnull-attribute,shift, signed-integer-overflow,unreachable,unsigned-integer-overflow, unsigned-shift-base,vla-bound,vptr
(加上更具體的版本 shift-base
和 shift-exponent
)其中一個子集可與 address
結合使用,或使用類似
-fsanitize=undefined -fno-sanitize=float-divide-by-zero
選項 function
、return
和 vptr
僅適用於 C++:若要使用 vptr
,其執行時間程式庫需要透過類似以下方式建置後者,才能連結到 R 主執行檔
MAIN_LD="clang++ -fsanitize=undefined"
選項 float-divide-by-zero
不適合與 R 搭配使用,因為 R 允許此類除法作為 IEC 60559 算術的一部分,且自 2019 年 6 月以來,在 clang
的版本中,它不再是 -fsanitize=undefined 的一部分。
對於 GCC,請參閱 https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html(或安裝或 透過 https://gcc.gnu.org/onlinedocs/ 取得您 GCC 版本的手冊:尋找「程式儀器選項」)以了解 GCC 支援的選項:支援 10 個
-fsanitize=alignment,bool,bounds,builtin,enum,integer-divide-by-zero, nonnull-attribute,null,object-size,pointer-overflow,return, returns-nonnull-attribute,shift,signed-integer-overflow, unreachable,vla-bound,vptr
加上更具體的版本 shift-base
和 shift-exponent
以及非預設選項
bound-strict,float-cast-overflow,float-divide-by-zero
其中 float-divide-by-zero
不適合 R 使用,而 bounds-strict
是 bounds
的延伸。
其他有用的旗標包括
-no-fsanitize-recover
這會導致第一個報告為致命(對於 unreachable
和 return
子選項,總是如此)。若要更詳細地了解執行時間錯誤發生在哪裡,請使用
setenv UBSAN_OPTIONS ‘print_stacktrace=1’
將在報告中包含追蹤回溯。除此之外,R 可以在產生消毒程式報告之前,在設定中斷點的情況下執行於偵錯程式中:對於 gdb
或 lldb
,您可以使用
break __ubsan_handle_float_cast_overflow break __ubsan_handle_float_cast_overflow_abort
或類似(每個類型的未定義行為都有處理程式)。
還有編譯器標記 -fcatch-undefined-behavior 和 -ftrapv,據說在 clang
中比 gcc
更可靠。
有關此主題的更多詳細資訊,請參閱 https://blog.regehr.org/archives/213 和 https://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html(分為 3 部分)。
使用 -fsanitize=undefined 建置 R 本身有可能或不可能:過去曾發現使用 gcc
的 OpenMP-using 程式碼有問題,但 clang
已成功使用至版本 16。然而,已發現 clang
17 及更新版本有問題,包括缺少進入點和 R 建置中斷。
在「x86_64」Linux 上的最新版本 clang
具有「ThreadSanitizer」(https://github.com/google/sanitizers/wiki#threadsanitizer),一種「C/C++ 程式的資料競賽偵測器」,以及「MemorySanitizer」(https://clang.llvm.org/docs/MemorySanitizer.html,https://github.com/google/sanitizers),用於偵測未初始化的記憶體。這兩者都基於 valgrind
中的工具,並提供類似的功能。
clang
有一個「靜態分析器」,可以在編譯期間執行於原始檔案:請參閱 https://clang-analyzer.llvm.org/。
GCC 10 引進了一個新的旗標 -fanalyzer,可以在編譯期間進行靜態分析,目前適用於 C 程式碼。它被視為實驗性質,當發現問題時,它可能會大幅降低運算速度(並使用數 GB 的常駐記憶體)。它與未定義行為偵測器偵測到的問題有些重疊,但有些問題只會由這個工具報告,由於它是靜態分析,因此它不依賴於執行程式碼路徑。
請參閱 https://gcc.gnu.org/onlinedocs/gcc-10.1.0/gcc/Static-Analyzer-Options.html(或您 gcc
版本的文件,如果較新)和 https://developers.redhat.com/blog/2020/03/26/static-analysis-in-gcc-10
來自 https://drmemory.org/ 的「Dr. Memory」是一個記憶體檢查器,目前適用於 Windows、Linux 和 macOS,其目標與 valgrind
類似。它適用於未修改的可執行檔138,並可偵測記憶體存取錯誤、未初始化讀取和記憶體外洩。
大多數與 R 搭配使用的 Fortran 編譯器允許編譯程式碼時檢查陣列邊界:例如 gfortran
有選項 -fbounds-check。當超過上限或下限時,這個選項會產生錯誤,例如:
At line 97 of file .../src/appl/dqrdc2.f Fortran runtime error: Index ‘1’ of dimension 1 of array ‘x’ above upper bound of 0
需要知道的是,懶惰的程式設計師通常會將 Fortran 維度指定為 1
,而不是 *
或實際邊界,而這些會被報告(*
維度也可能被報告)
只要在套件中的程式碼上使用這個檢查就很簡單:在 ~/.R/Makevars 中加入類似以下內容(適用於 gfortran
)
FFLAGS = -g -O2 -mtune=native -fbounds-check
執行 R CMD check
時。
這可能會報告 Fortran 字元變數傳遞方式的錯誤,特別是在從 C 程式碼呼叫 Fortran 子程式而沒有傳遞字元長度時(請參閱 Fortran 字元字串)。
遲早程式設計師會面臨到需要除錯載入 R 的編譯程式碼。本節針對使用 gdb
除錯 gcc
編譯的程式碼的平台而設計,但其他除錯工具,例如 lldb
(https://lldb.llvm.org/,用於 macOS)和 Sun 的 dbx
,也可以執行類似的工作:有些除錯工具有可用的圖形前端。
首先考慮「當機」,也就是 R 因非法記憶體存取(「分段錯誤」或「匯流排錯誤」)、非法指令或類似情況而意外終止。類 Unix 版本的 R 使用信號處理常式,其目標是提供一些基本資訊。例如
*** caught segfault *** address 0x20000028, cause ‘memory not mapped’ Traceback: 1: .identC(class1[[1]], class2) 2: possibleExtends(class(sloti), classi, ClassDef2 = getClassDef(classi, where = where)) 3: validObject(t(cu)) 4: stopifnot(validObject(cu <- as(tu, "dtCMatrix")), validObject(t(cu)), validObject(t(tu))) Possible actions: 1: abort (with core dump) 2: normal R exit 3: exit R without saving workspace 4: exit R saving workspace Selection: 3
由於 R 程序可能會損壞,因此唯一真正安全的選項是第一個或第三個。(請注意,核心傾印僅在啟用的情況下產生:shell 中的常見預設值是將其大小限制為 0,從而停用它。)
此類當機的常見原因之一是使用 .C
或 .Fortran
並且寫入傳遞給它的其中一個引數的兩端(任一端)之外的套件。有一個很好的方法可以偵測到這一點:使用 options(CBoundsCheck = TRUE)
(可透過環境變數 R_C_BOUNDS_CHECK=yes)
選擇)來變更 .C
和 .Fortran
的運作方式,以檢查編譯的程式碼是否寫入引數兩端的 64 個位元組。
「當機」的另一個原因是 C 堆疊溢位。R 會嘗試在自己的程式碼中追蹤,但它可能發生在第三方編譯的程式碼中。對於現代符合 POSIX 的作業系統,R 可以安全地捕捉到該問題並返回到頂層提示字元,因此會得到類似
> .C("aaa") Error: segfault from C stack overflow >
然而,在 Windows 下,C 堆疊溢位是致命的,而且通常會導致在該平台上進行偵錯的嘗試失敗。此外,在 Windows 上編譯 R 時會設定堆疊大小,而在 POSIX 作業系統上,可以在啟動 R 的殼層中設定堆疊大小。
如果您有導致核心傾印的當機,您可以使用類似
gdb /path/to/R/bin/exec/R core.12345
來檢查核心傾印。如果已停用核心傾印或要捕捉未產生傾印的錯誤,您可以透過例如
$ R -d gdb --vanilla ... gdb> run
直接在偵錯器下執行 R,此時 R 會正常執行,而且偵錯器有望會捕捉到錯誤並返回到其提示字元。這也可以用於捕捉無限迴圈或中斷執行時間很長的程式碼。對於一個簡單的範例
> for(i in 1:1e7) x <- rnorm(100) [hit Ctrl-C] Program received signal SIGINT, Interrupt. 0x00397682 in _int_free () from /lib/tls/libc.so.6 (gdb) where #0 0x00397682 in _int_free () from /lib/tls/libc.so.6 #1 0x00397eba in free () from /lib/tls/libc.so.6 #2 0xb7cf2551 in R_gc_internal (size_needed=313) at /users/ripley/R/svn/R-devel/src/main/memory.c:743 #3 0xb7cf3617 in Rf_allocVector (type=13, length=626) at /users/ripley/R/svn/R-devel/src/main/memory.c:1906 #4 0xb7c3f6d3 in PutRNGstate () at /users/ripley/R/svn/R-devel/src/main/RNG.c:351 #5 0xb7d6c0a5 in do_random2 (call=0x94bf7d4, op=0x92580e8, args=0x9698f98, rho=0x9698f28) at /users/ripley/R/svn/R-devel/src/main/random.c:183 ...
在許多情況下,可以將偵錯器附加到正在執行的程序:如果使用的是替代前端,或是要調查看似花費太久的任務,這會很有用。這可以透過類似
gdb -p pid
來完成,其中 pid
是 R 可執行檔或前端程序的 ID,而且可以在執行中的 R 程序中透過呼叫 Sys.getpid()
或從程序監視器中找到。這會停止程序,以便檢查其狀態:使用 continue
繼續執行。
以下是一些值得知道的「技巧」
在大部分編譯環境中,動態載入到 R 中的已編譯程式碼在載入之前無法設定中斷點。若要在類 Unix 系統上對此類動態載入程式碼使用符號偵錯器,請使用
dyn.load
或 library
載入您的共用物件。
在 Windows 中可能無法使用訊號,如果是這樣,程序會更複雜。請參閱 rw-FAQ。
檢查編譯程式碼中 R 物件的關鍵在於函數 PrintValue(SEXP s)
,它使用正常的 R 列印機制來列印 s 指向的 R 物件,或更安全的版本 R_PV(SEXP s)
,它只會列印「物件」。
使用 PrintValue
的方法之一是在要除錯的程式碼中插入適當的呼叫。
另一種方法是從符號除錯器呼叫 R_PV
。(PrintValue
被隱藏為 Rf_PrintValue
。)例如,從 gdb
我們可以使用
(gdb) p R_PV(ab)
使用摺積範例中的物件 ab
,如果我們在摺積 C 程式碼中放置適當的中斷點。
要檢查任意的 R 物件,我們需要更努力一點。例如,假設
R> DF <- data.frame(a = 1:3, b = 4:6)
在 do_get
中設定中斷點,並在 R 提示字元中輸入 get("DF"),就可以找出 DF
在記憶體中的位址,例如
Value returned is $1 = (SEXPREC *) 0x40583e1c (gdb) p *$1 $2 = { sxpinfo = {type = 19, obj = 1, named = 1, gp = 0, mark = 0, debug = 0, trace = 0, = 0}, attrib = 0x40583e80, u = { vecsxp = { length = 2, type = {c = 0x40634700 "0>X@D>X@0>X@", i = 0x40634700, f = 0x40634700, z = 0x40634700, s = 0x40634700}, truelength = 1075851272, }, primsxp = {offset = 2}, symsxp = {pname = 0x2, value = 0x40634700, internal = 0x40203008}, listsxp = {carval = 0x2, cdrval = 0x40634700, tagval = 0x40203008}, envsxp = {frame = 0x2, enclos = 0x40634700}, closxp = {formals = 0x2, body = 0x40634700, env = 0x40203008}, promsxp = {value = 0x2, expr = 0x40634700, env = 0x40203008} } }
(重新格式化除錯器輸出以提高可讀性)。
使用 R_PV()
可以「檢查」SEXP 中各種元素的值,例如,
(gdb) p R_PV($1->attrib) $names [1] "a" "b" $row.names [1] "1" "2" "3" $class [1] "data.frame" $3 = void
要找出對應資訊的確切儲存位置,需要「深入」一點
(gdb) set $a = $1->attrib (gdb) p $a->u.listsxp.tagval->u.symsxp.pname->u.vecsxp.type.c $4 = 0x405d40e8 "names" (gdb) p $a->u.listsxp.carval->u.vecsxp.type.s[1]->u.vecsxp.type.c $5 = 0x40634378 "b" (gdb) p $1->u.vecsxp.type.s[0]->u.vecsxp.type.i[0] $6 = 1 (gdb) p $1->u.vecsxp.type.s[1]->u.vecsxp.type.i[1] $7 = 5
另一種選擇是 R_inspect
函數,它會遞迴顯示物件的低階結構(位址與上述範例不同,因為這個範例是在另一部電腦上建立的)
(gdb) p R_inspect($1) @100954d18 19 VECSXP g0c2 [OBJ,NAM(2),ATT] (len=2, tl=0) @100954d50 13 INTSXP g0c2 [NAM(2)] (len=3, tl=0) 1,2,3 @100954d88 13 INTSXP g0c2 [NAM(2)] (len=3, tl=0) 4,5,6 ATTRIB: @102a70140 02 LISTSXP g0c0 [] TAG: @10083c478 01 SYMSXP g0c0 [MARK,NAM(2),gp=0x4000] "names" @100954dc0 16 STRSXP g0c2 [NAM(2)] (len=2, tl=0) @10099df28 09 CHARSXP g0c1 [MARK,gp=0x21] "a" @10095e518 09 CHARSXP g0c1 [MARK,gp=0x21] "b" TAG: @100859e60 01 SYMSXP g0c0 [MARK,NAM(2),gp=0x4000] "row.names" @102a6f868 13 INTSXP g0c1 [NAM(2)] (len=2, tl=1) -2147483648,-3 TAG: @10083c948 01 SYMSXP g0c0 [MARK,gp=0x4000] "class" @102a6f838 16 STRSXP g0c1 [NAM(2)] (len=1, tl=1) @1008c6d48 09 CHARSXP g0c2 [MARK,gp=0x21,ATT] "data.frame"
一般而言,每個物件的表示方式都遵循下列格式
@<address> <type-nr> <type-name> <gc-info> [<flags>] ...
若要更精細地控制遞迴深度和向量輸出,R_inspect3
會採用兩個額外的 character() 參數:最大深度和會列印的純量向量最大元素數。目前 R_inspect
中的預設值分別為 -1(無限制)和 5。
要除錯套件中的程式碼,最簡單的方法是將其解壓縮到目錄中,並使用安裝
R CMD INSTALL --dsym pkgname
因為 macOS 沒有將除錯符號儲存在 .so 檔案中。(不必使用除錯符號建置 R,但編譯套件時應在 CFLAGS
/ CXXFLAGS
/ FFLAGS
/ FCFLAGS
中包含 -g。)
安全性措施可能會阻止在 lldb
下執行 CRAN 的二進制 R 發行版,或將其附加為除錯器 (https://r-cran.dev.org.tw/bin/macosx/RMacOSX-FAQ.html#I-cannot-attach-debugger-to-R),儘管這兩者在 High Sierra 上都是可行的,而且從 R 4.2.0 開始又再次可行。這也可能會影響本機編譯的建置,其中在 Big Sur 或 Monterey 下附加到互動式 R 會話在 2022 年透過彈出式視窗授予管理員權限後可行。(要在 Apple 視為非互動式會話中除錯,例如遠端登入,請參閱 man DevToolsSecurity
。)
在 macOS 上除錯 R 的本機建置可能會增加額外的障礙,因為環境變數(例如 DYLD_FALLBACK_LIBRARY_PATH
)通常不會透過 lldb
程序傳遞139,導致出現類似這樣的訊息
R -d lldb ... (lldb) run Process 16828 launched: ‘/path/to/bin/exec/R’ (x86_64) dyld: Library not loaded: libR.dylib Referenced from: /path/to/bin/exec/R
一個快速的解決方法是將 R_HOME/lib 下的 dylib 符號連結到它們將被找到的地方,例如目前的作業目錄。可以像發行版所做的那樣140,使用 install_name_tool
,但必須對所有 dylib(包括套件中的 dylib)執行此操作。
最簡單的方法可能是將除錯器附加到正在執行的程序(請參閱上方)。具體來說,執行 R,當它在要除錯的命令之前的提示符號時,在終端機中
ps -ef | grep exec/R # identify the pid pid for the next command: it is the second item lldb -p pid (lldb) continue
然後返回 R 主控台。
對於非互動式使用,可能需要 lldb --batch
。
在支援的情況下,連結時間最佳化提供一個全面的方式來檢查 Fortran 檔案或 C 和 Fortran 之間呼叫的一致性。使用這個 via R CMD INSTALL --use-LTO
(但如果有一個 src/Makefile 檔案或 Windows 類比,則不適用)。
要在類 Unix 上設定支援,請參閱 連結時間最佳化 中的 R 安裝和管理。在 Linux 上使用 GCC 而未用 LTO 支援來建置 R 時,設定
LTO_OPT = -flto LTO_FC_OPT = -flto AR = gcc-ar NM = gcc-nm
在個人(或網站)Makevars 檔案中就應該足夠了:請參閱 自訂套件編譯 中的 R 安裝和管理,
對於 Windows,請先編輯檔案 etc/${R_ARCH}/Makeconf 以給予 LTO_OPT
值 -flto
或在個人/網站 Makevars 檔案中這樣做;另請參閱來源中的檔案 src/gnuwin32/README.compilation。
例如
boot.f:61: warning: type of ‘ddot’ does not match original declaration [-Wlto-type-mismatch] y(j,i)=ddot(p,x(j,1),n,b(1,j,i),1) crq.f:1023: note: return value type mismatch
套件作者忘記宣告
double precision ddot external ddot
在 boot.f 中。該套件有自己的 ddot
副本:要偵測 R 的 BLAS 函式庫中的一個錯誤使用,需要使用 --enable-lto=check 來設定 R。
進一步的範例
rkpk2.f:77:5: warning: type of ‘dstup’ does not match original declaration [-Wlto-type-mismatch] *info, wk) rkpk1.f:2565:5: note: type mismatch in parameter 14 subroutine dstup (s, lds, nobs, nnull, qraux, jpvt, y, q, ldqr, rkpk1.f:2565:5: note: ‘dstup’ was previously declared here
呼叫中缺少第十四個引數 dum
。
reg.f:78:33: warning: type of ‘dqrdc’ does not match original declaration [-Wlto-type-mismatch] call dqrdc (sr, nobs, nobs, nnull, wk, dum, dum, 0) dstup.f:20: note: ‘dqrdc’ was previously declared here call dqrdc (s, lds, nobs, nnull, qraux, jpvt, work, 1)
dqrdc
是 R 中的 LINPACK 常式,jpvt
是整數陣列,而 work
是雙精度陣列,因此 dum
無法與兩者相符。(如果已使用 --enable-lto=check,則比較會針對 R 中的定義。)
對於套件中所有 Fortran 檔案,可以透過串接 Fortran 檔案並編譯結果來偵測大多數不一致之處,有時診斷結果會比 LTO 提供的更清楚。對於我們的最後兩個範例,這會產生
all.f:2966:72: *info, work1) 1 Warning: Missing actual argument for argument ‘dum’ at (1)
和
all.f:1663:72: *ipvtwk), wk(ikwk), wk(iwork1), wk(iwork2), info) 1 Warning: Type mismatch in argument ‘jpvt’ at (1); passed REAL(8) to INTEGER(4)
在類 Unix 系統上,對於具有 src/Makefile 檔案的套件,可以在該檔案中包含適當旗標來啟用 LTO,例如
LTO = $(LTO_OPT) LTO_FC = $(LTO_FC_OPT)
並確保這些旗標用於編譯,例如作為 CFLAGS
、CXXFLAGS
或 FCFLAGS
的一部分。如果使用 R CMD SHLIB
進行編譯,請將 --use-LTO 新增到其呼叫中。
在 Windows 上,對於包含 ‘"${R_HOME}/etc${R_ARCH}/Makeconf"’ 的 src/Makefile.ucrt 或 src/Makefile.win 檔案的套件,請包含
LTO = $(LTO_OPT)
或始終使用 LTO,無論 R 如何建置,
LTO = -flto
.C
和 .Fortran
dyn.load
和 dyn.unload
.Call
和 .External
存取作業系統函式是透過 R 函式 system
和 system2
。 細節會因平台而異(請參閱線上說明),唯一可以安全假設的是第一個參數會是一個字串 command
,它將傳遞給執行(不一定透過 shell),而第二個參數傳遞給 system
會是 internal
,如果為真,它會將命令的輸出收集到 R 字元向量中。
在相容於 POSIX 的作業系統上,這些命令會將命令列傳遞給 shell:Windows 不相容於 POSIX,有一個獨立的函式 shell
可以這麼做。
函式 system.time
可用於計時。在子行程上計時僅在類 Unix 系統上可用,而且在那裡可能不可靠。
.C
和 .Fortran
¶這兩個函式提供一個介面,連結到已編譯的程式碼中,這些程式碼已在建置時或透過 dyn.load
連結到 R 中(請參閱 dyn.load
和 dyn.unload
)。它們主要分別用於編譯的 C 和 Fortran 程式碼,但 .C
函式可用於其他可以產生 C 介面的語言,例如 C++(請參閱 介接 C++ 程式碼)。
每個函式的第一個引數是一個字元串,指定 C 或 Fortran 已知的符號名稱141,也就是函式或子程式名稱。(可以透過例如 is.loaded("cg")
來測試符號是否已載入。使用傳遞給 .C
或 .Fortran
的名稱,而不是轉譯後的符號名稱。)
最多可以有 65 個進一步的引數,提供要傳遞給編譯程式碼的 R 物件。通常這些會在傳遞之前複製,並在編譯程式碼傳回時再次複製到 R 清單物件中。如果引數有指定名稱,這些名稱會用於傳回清單物件中元件的名稱(但不會傳遞給編譯程式碼)。
下表提供 R 原子向量模式和 C 函式或 Fortran 子程式的引數類型之間的對應。
R 儲存模式 C 類型 Fortran 類型 邏輯
int *
INTEGER
integer
int *
INTEGER
double
double *
DOUBLE PRECISION
complex
Rcomplex *
DOUBLE COMPLEX
character
char **
CHARACTER(255)
raw
unsigned char *
none
在所有 R 平台上,int
和 INTEGER
都是 32 位元。從 S-PLUS (使用 long *
表示 logical
和 integer
) 移植的程式碼無法在所有 64 位元平台上執行 (儘管它可能在某些平台上執行,包括 Windows)。另外請注意,如果您的已編譯程式碼是 C 函式和 Fortran 子程式,則引數類型必須與上表中所述相符。
C 類型 Rcomplex
是具有 double
成員 r
和 i
的結構,定義在標頭檔 R_ext/Complex.h 中。142 (在大多數平台上,這以與 C99 double complex
類型相容的方式儲存:但是,可能無法將 Rcomplex
傳遞給預期 double complex
引數的 C99 函式。它也不需要與 C++ complex
類型相容。此外,相容性可能取決於為編譯器設定的最佳化層級。)
只能傳遞單一固定長度的字元串至 Fortran 或從 Fortran 傳遞 (長度未傳遞),而且這是否成功取決於編譯器:其使用已於 2019 年正式棄用。其他 R 物件可以傳遞至 .C
,但最好使用其他介面之一。
可以將儲存模式為 double
的數值向量傳遞給 C 作為 float *
或傳遞給 Fortran 作為 REAL
,方法是設定屬性 Csingle
,最方便的方法是使用 R 函數 as.single
、single
或 mode
。這僅用於協助連接現有的 C 或 Fortran 程式碼。
邏輯值會傳送為 0
(FALSE
)、1
(TRUE
) 或 INT_MIN = -2147483648
(NA
,但僅當 NAOK
為 true 時),且編譯的程式碼應傳回這三個值之一。(除了 INT_MIN
以外的非零值會對應到 TRUE
。)請注意,使用 int *
來表示 Fortran 邏輯並非保證可移植 (儘管多年來人們都這樣做):最好傳遞整數,並在 Fortran wrapper 中轉換為/從 Fortran 邏輯。
除非形式參數 NAOK
為 true,否則所有其他參數都會檢查遺失值 NA
和 IEEE 特殊值 NaN
、Inf
和 -Inf
,且如果存在這些值中的任何一個,就會產生錯誤。如果為 true,則會在未檢查的情況下傳遞這些值。
參數 PACKAGE
將符號名稱的搜尋限制在特定共用物件中(或使用 "base"
來編譯成 R 的程式碼)。強烈建議使用,因為無法避免兩個套件寫作者使用相同的符號名稱,而此類名稱衝突通常足以導致 R 崩潰。(如果不存在且呼叫來自套件命名空間中定義的函數主體,則會使用第一個(如果有)useDynLib
指令載入的共用物件。)
請注意,編譯的程式碼不應透過其參數傳回任何內容:C 函數應為 void
類型,Fortran 子程式應為子常式。
為了釐清概念,讓我們考慮一個非常簡單的範例,它會對兩個有限序列進行卷積。(這很難在 R 解譯程式碼中快速執行,但在 C 程式碼中很容易。)我們可以使用 .C
透過下列方式執行此操作:
void convolve(double *a, int *na, double *b, int *nb, double *ab) { int nab = *na + *nb - 1; for(int i = 0; i < nab; i++) ab[i] = 0.0; for(int i = 0; i < *na; i++) for(int j = 0; j < *nb; j++) ab[i + j] += a[i] * b[j]; }
從 R 呼叫:
conv <- function(a, b) .C("convolve", as.double(a), as.integer(length(a)), as.double(b), as.integer(length(b)), ab = double(length(a) + length(b) - 1))$ab
請注意,我們在呼叫 .C
之前會特別將所有參數強制轉換為正確的 R 儲存模式;類型比對錯誤可能會導致錯誤的結果或難以偵測的錯誤。
在 C(或 C++)中處理 character
向量參數時需要特別小心。在進入時,元素的內容會複製並指定給 char **
陣列的元素,在離開時,C 陣列的元素會複製以建立字元向量的元素。這表示 char **
陣列的字串內容可以變更,包括縮短字串的 \0
,但字串無法加長。可以143 透過 R_alloc
分配新的字串,並用新的字串取代 char **
陣列中的項目。但是,當字元向量不以唯讀方式使用時,.Call
介面會更受青睞。
傳遞字元串給 Fortran 程式碼需要更小心,已不建議使用,且應儘量避免。只會傳入字元向量的第一個元素,作為固定長度 (255) 的字元陣列。最多會傳回 255 個字元給長度為一的字元向量。這是否能順利運作 (甚至是否能運作) 取決於每個平台上的 C 和 Fortran 編譯器 (包括其選項)。傳遞給 Fortran 的內容通常是少數可能值之一 (R 術語中的因子),或者也可以傳遞為整數代碼:類似地,想要產生診斷訊息的 Fortran 程式碼可以傳遞整數代碼給 C 或 R wrapper,然後轉換成字元串。
除了原子向量之外,也可以傳遞其他一些 R 物件 via .C
,但這僅支援於歷史相容性:對於此類物件,請使用 .Call
或 .External
介面。任何包含 Rinternals.h 的 C/C++ 程式碼都應該 via .Call
或 .External
呼叫。
.Fortran
主要用於 Fortran 77 程式碼,且遠早於對「現代」Fortran 的任何支援。現今的 Fortran 實作支援 Fortran 2003 模組 iso_c_binding
,使用 .C
並使用 use iso_c_binding
編寫 C 介面是將現代 Fortran 程式碼介面到 R 的更好方法。
dyn.load
和 dyn.unload
¶要與 R 搭配使用的已編譯程式碼會載入為共用物件(類 Unix 系統,包括 macOS,如需更多資訊,請參閱 建立共用物件)或 DLL(Windows)。
共用物件/DLL 會由 dyn.load
載入,並由 dyn.unload
卸載。卸載通常不是必要的,而且通常也不安全,但在某些平台(包括 Windows)上,需要卸載才能重新建置 DLL。卸載 DLL,然後重新載入同名的 DLL 可能無法運作:Solaris 使用載入的第一個版本。註冊 C 完成處理常式的 DLL,但卸載時未註銷這些常式,可能會導致 R 在卸載後當機。
這兩個函數的第一個引數是提供物件路徑的字元串。程式設計人員不應假設物件/DLL 有特定的檔案副檔名(例如 .so),而是使用類似
file.path(path1, path2, paste0("mylib", .Platform$dynlib.ext))
的結構,以確保平台獨立性。在類 Unix 系統上,提供給 dyn.load
的路徑可以是絕對路徑、相對於目前目錄的路徑,或者如果以「~」開頭,則相對於使用者的家目錄路徑。
載入最常根據 NAMESPACE 檔案中的 useDynLib()
宣告自動進行,但也可以透過呼叫 library.dynam
明顯地 進行。 其形式為
library.dynam("libname", package, lib.loc)
其中 libname
是物件/DLL 名稱,不包含副檔名。請注意,第一個引數 chname
不應 是 package
,因為如果套件以其他名稱安裝,這將無法運作。
在某些類 Unix 系統中,當載入物件時,可以選擇如何解析符號,由參數 local
和 now
決定。僅在必要時使用這些參數:特別是使用 now=FALSE
,然後呼叫未解析符號,將會毫不客氣地終止 R。
R 提供一種方式,可以在載入或卸載物件/DLL 時自動執行一些程式碼。例如,這可以用於使用 R 的動態符號機制註冊原生常式、初始化原生程式碼中的一些資料,或初始化第三方程式庫。在載入 DLL 時,R 會在該 DLL 中尋找名為 R_init_lib
的常式,其中 lib 是 DLL 檔案名稱,已移除副檔名。例如,在指令中
library.dynam("mylib", package, lib.loc)
R 尋找名為 R_init_mylib
的符號。類似地,在卸載物件時,R 會尋找名為 R_unload_lib
的常式,例如 R_unload_mylib
。在任一種情況下,如果常式存在,R 會呼叫它並傳遞一個描述 DLL 的單一參數。這是 DllInfo
類型的值,定義在 R_ext 目錄中的 Rdynload.h 檔案中。
請注意,此機制有一些隱含的限制,因為 DLL 的基本檔名既需要是有效的檔案名稱,也需要作為 C 進入點的一部分而有效(例如,它不能包含「.」):對於可攜式程式碼,最好將 DLL 名稱限制為 ASCII 字母數字加上底線。如果找不到進入點 R_init_lib
,也會尋找用「_」取代「.」的進入點。
以下範例顯示 mylib
DLL 的初始化和卸載常式的範本。
#include <R_ext/Rdynload.h> void R_init_mylib(DllInfo *info) { /* Register routines, allocate resources. */ } void R_unload_mylib(DllInfo *info) { /* Release resources. */ }
如果共用物件/DLL 載入多次,會使用最新版本。144 更普遍來說,如果同一個符號名稱出現在多個共用物件中,會使用最近載入的執行個體。 PACKAGE
參數和註冊(請參閱下一節)提供了很好的方法,可以避免任何執行個體的含義不明確。
在類 Unix 系統上,用於解析動態連結的相依函式庫的路徑在程序啟動時固定(出於安全原因),因此 dyn.load
只會在 R shell 腳本(透過 etc/ldpaths)和作業系統特定的預設值所設定的位置尋找此類函式庫。
Windows 允許對相依 DLL 的搜尋位置進行更多控制(以及更少的安全性)。在所有版本中,這包括 PATH
環境變數,但優先順序最低:請注意,它不包括載入 DLL 的目錄。可以透過 dyn.load
的 DLLpath
參數,以相當高的優先順序新增單一路徑。透過 library.dynam
(預設)使用這個參數,以將套件的 libs/x64 目錄包含在 DLL 搜尋路徑中。
「原生」常式是指編譯碼中的進入點。
在呼叫 .C
、.Call
、.Fortran
和 .External
時,R 必須透過在適當的共用物件/DLL 中尋找,來找出指定的原生常式。預設情況下,R 使用作業系統特定的動態載入器,在所有145 已載入的 DLL 和 R 可執行檔或其連結的函式庫中,尋找符號。或者,DLL 的作者可以明確地向 R 註冊常式,並使用單一、與平台無關的機制,在 DLL 中尋找常式。可以使用此註冊機制,提供有關常式的其他資訊,包括引數的數量和類型,並讓 R 程式設計師在不同的名稱下使用常式。
註冊常式有兩個主要優點:它提供一種更快的146方式,透過編譯時儲存在 DLL 中的表格,來尋找進入點的位址,並提供執行時間檢查,以確保進入點以正確的引數數量和(選擇性)正確的引數類型呼叫。
若要向 R 註冊常式,請呼叫 C 常式 R_registerRoutines
。這通常在 DLL 第一次載入時,在 dyn.load
和 dyn.unload
中所述的初始化常式 R_init_dll 名稱
中完成。 R_registerRoutines
採用 5 個引數。第一個是 R 傳遞給初始化常式的 DllInfo
物件。這是 R 儲存有關方法資訊的地方。其餘 4 個引數是描述 4 個不同介面的常式的陣列:.C
、.Call
、.Fortran
和 .External
。每個引數都是下列表格中所列元素類型的 NULL
終止陣列
.C
R_CMethodDef
.Call
R_CallMethodDef
.Fortran
R_FortranMethodDef
.External
R_ExternalMethodDef
目前,R_ExternalMethodDef
類型與 R_CallMethodDef
類型相同,並包含可讓 R 存取的常式名稱欄位、指向實際原生符號 (即常式本身) 的指標,以及常式預期從 R 傳遞的引數數目。例如,如果我們有一個名為 myCall
的常式,定義如下
SEXP myCall(SEXP a, SEXP b, SEXP c);
我們會將其描述為
static const R_CallMethodDef callMethods[] = { {"myCall", (DL_FUNC) &myCall, 3}, {NULL, NULL, 0} };
以及 .Call
介面的任何其他常式。對於透過 .External
介面呼叫的變數引數常式,請指定 -1
作為引數數目,以告知 R 不要檢查實際傳遞的數目。
可與 .C
和 .Fortran
介面搭配使用的常式會以類似的資料結構描述,這些結構有一個選用的附加欄位,用於描述每個引數的類型。如果指定,這個欄位應為一個陣列,其中包含描述常式每個引數預期類型的 SEXP
類型。(技術上來說,類型陣列的元素類型為 R_NativePrimitiveArgType
,這只是一個無符號整數。) R 類型和對應的類型識別碼在下列表格中提供
數字
REALSXP
integer
INTSXP
邏輯
LGLSXP
單一
SINGLESXP
character
STRSXP
清單
VECSXP
考慮一個 C 常式 myC
,宣告如下
void myC(double *x, int *n, char **names, int *status);
我們會將其註冊為
static R_NativePrimitiveArgType myC_type[] = { REALSXP, INTSXP, STRSXP, LGLSXP }; static const R_CMethodDef cMethods[] = { {"myC", (DL_FUNC) &myC, 4, myC_type}, {NULL, NULL, 0, NULL} };
如果註冊類型,請仔細檢查類型數目是否與引數數目相符:由於類型陣列 (這裡是 myC_type
) 在 C 中傳遞為指標,因此註冊機制無法為您檢查這一點。
請注意,.Fortran
進入點會對應到小寫,因此註冊時應僅使用小寫。
建立描述每個常式的陣列後,最後一個步驟是實際將它們註冊到 R。我們透過呼叫 R_registerRoutines
來執行此動作。例如,如果我們有上述存取 .C
和 .Call
常式的描述,我們會使用以下程式碼
void R_init_myLib(DllInfo *info) { R_registerRoutines(info, cMethods, callMethods, NULL, NULL); }
當 R 載入名為 myLib
的共用物件/DLL 時,會呼叫此常式。呼叫 R_registerRoutines
的最後兩個引數是存取 .Fortran
和 .External
介面的常式。在我們的範例中,這些常式指定為 NULL
,因為我們沒有這種類型的常式。
當 R 卸載共用物件/DLL 時,其註冊也會被移除。沒有其他可供註銷符號的機制。
可以在 R 原始碼樹中的不同套件中找到註冊常式的範例(例如,stats 和 graphics)。此外,R News(第 1/3 卷,2001 年 9 月,第 20-23 頁,https://www.r-project.org/doc/Rnews/Rnews_2001-3.pdf)中有一個簡短的高階簡介。
註冊常式後,如果在套件的 NAMESPACE 檔案中的 useDynLib
呼叫中安排此動作,則可以將它們視為 R 物件(請參閱 useDynLib)。因此,例如 stats 套件在其 NAMESPACE 檔案中具有
# Refer to all C/Fortran routines by their name prefixed by C_ useDynLib(stats, .registration = TRUE, .fixes = "C_")
然後 ansari.test
的預設方法可以包含
pansari <- function(q, m, n) .C(C_pansari, as.integer(length(q)), p = as.double(q), as.integer(m), as.integer(n))$p
這可以避免每次使用時查詢進入點的開銷,並確保套件中的進入點是使用的進入點(沒有 PACKAGE = "pkg"
引數)。
R_init_
常式通常具有以下形式
void attribute_visible R_init_mypkg(DllInfo *dll) { R_registerRoutines(dll, CEntries, CallEntries, FortEntries, ExternalEntries); R_useDynamicSymbols(dll, FALSE); R_forceSymbols(dll, TRUE); ... }
R_useDynamicSymbols
呼叫表示不得搜尋 DLL 以尋找字元串指定的進入點,因此 .C
等呼叫只會尋找已註冊的符號:R_forceSymbols
呼叫只允許 .C
等呼叫,這些呼叫會以 R 物件(例如 C_pansari
,而非字元串)指定進入點。每個呼叫都提供一些防護措施,以避免在使用者未提供套件的字元串時意外找到進入點,並避免減慢此類搜尋的速度。(有關能見度屬性,請參閱 控制能見度。)
更詳細地說,如果套件 mypkg
包含進入點 reg
和 unreg
,且第一個已註冊為 0 參數 .Call
常式,我們可以使用(來自套件中的程式碼)
.Call("reg") .Call("unreg")
無論是否註冊,這兩個都會運作。如果 R_init_mypkg
呼叫 R_useDynamicSymbols(dll, FALSE)
,只有第一個會運作。如果除了註冊之外,NAMESPACE
檔案還包含
useDynLib(mypkg, .registration = TRUE, .fixes = "C_")
那麼我們可以呼叫 .Call(C_reg)
。最後,如果 R_init_mypkg
還呼叫 R_forceSymbols(dll, TRUE)
,只有 .Call(C_reg)
會運作(而非 .Call("reg")
)。這通常是我們想要的:它確保我們自己的所有 .Call
呼叫會直接前往套件中預期的程式碼,而且沒有其他人會意外找到我們的進入點。(如果有人需要從套件外部呼叫我們的程式碼,例如用於除錯,他們可以使用 .Call(mypkg:::C_reg)
。)
有時註冊原生常式或使用 PACKAGE
參數會造成很大的差異。結果可能會明顯取決於作業系統(甚至 32 位還是 64 位)、R 的版本以及當時載入 R 的其他內容。
為了固定想法,首先考慮 x86_64
作業系統 10.7 和 R 2.15.2。一個簡單的 .Call
函數可能是
foo <- function(x) .Call("foo", x)
使用 C 程式碼
#include <Rinternals.h> SEXP foo(SEXP x) { return x; }
如果我們透過 R CMD SHLIB foo.c
編譯,透過 dyn.load("foo.so")
載入程式碼並執行 foo(pi)
,大約需要 22 微秒 (us)。透過以下方式指定 DLL
foo2 <- function(x) .Call("foo", x, PACKAGE = "foo")
將時間縮短至 1.7 微秒。
現在考慮讓這些函數成為套件的一部分,其 NAMESPACE 檔案使用 useDynlib(foo)
。這會立即縮短執行時間,因為 "foo"
會優先尋找 foo.dll。不指定 PACKAGE
大約需要 5 微秒(它需要在每次呼叫時找出適當的 DLL,但不需要搜尋所有 DLL),而使用 PACKAGE
參數則再次約為 1.7 微秒。
接下來假設套件已註冊原生常式 foo
。然後 foo()
仍然必須找到適當的 DLL,但可以更快地進入 DLL 中的進入點,大約 4.2 微秒。而 foo2()
現在大約需要 1 微秒。如果我們在 NAMESPACE 檔案中註冊符號並使用
foo3 <- function(x) .Call(C_foo, x)
然後原生常式的位址只會在載入套件時查詢一次,而 foo3(pi)
大約需要 0.8 微秒。
使用 .C()
而不是 .Call()
的版本大約多花費 0.2 微秒。
這些都是相當小的差異,但 C 常式通常會被呼叫數百萬次,每次執行時間為幾微秒,而執行此類操作的人員可能希望了解這些差異。
在 Linux 和 Solaris 上,查詢符號的開銷較小。
Windows 上的符號查詢過去慢得多,因此 R 維護了一個小型快取。如果快取目前足夠空,可以將符號儲存在快取中,則效能與 Linux 和 Solaris 類似:如果不是,則可能會較慢。R 自有的程式碼總是使用已註冊的符號,因此這些符號永遠不會貢獻到快取中:然而,許多其他套件確實依賴符號查詢。
在較新的 R 版本中,所有標準套件都會註冊原生符號,並且不允許符號搜尋,因此在新工作階段中,foo()
只能在 foo.so 中查詢,並且可能與 foo2()
一樣快。當載入許多已貢獻的套件時,這將不再適用,而且通常最後載入的套件會先被搜尋。例如,考慮 x86_64 Linux 上的 R 3.3.2。在空的 R 工作階段中,foo()
和 foo2()
都大約花了 0.75 微秒;然而,在載入套件 igraph 和 spatstat 之後(載入了另外 12 個 DLL),foo()
花了 3.6 微秒,但 foo2()
仍然花了大約 0.80 微秒。在套件中使用註冊將此減少到 0.55 微秒,而 foo3()
花了 0.40 微秒,在載入更多套件時,這些時間沒有改變。
splines 套件在 2001 年轉換為使用符號註冊,但我們可以使用它作為範例147來說明小型套件需要執行的動作。
nm -g /path/to/splines.so | grep " T " 0000000000002670 T _spline_basis 0000000000001ec0 T _spline_value
這表示有兩個相關的進入點。(它們可能會有或沒有前導底線,如下所示。Fortran 進入點將會有尾隨底線。)在 R 程式碼中檢查套件如何呼叫它們:在本例中,它們由 .Call
使用。
或者,檢查套件的 R 程式碼以找出所有 .C
、.Fortran
、.Call
和 .External
呼叫。
extern "C"
)
#include <stdlib.h> // for NULL #include <R_ext/Rdynload.h> #define CALLDEF(name, n) {#name, (DL_FUNC) &name, n} static const R_CallMethodDef R_CallDef[] = { CALLDEF(spline_basis, ?), CALLDEF(spline_value, ?), {NULL, NULL, 0} }; void R_init_splines(DllInfo *dll) { R_registerRoutines(dll, NULL, R_CallDef, NULL, NULL); }
然後將範本中的 ?
替換為實際的引數數量。除非附加到唯一的 C 原始檔,否則您需要新增函數的宣告(也稱為「原型」)。有些套件會在標頭檔中已有這些宣告,或者您可以建立一個並將它包含在 init.c 中,例如包含下列內容的 splines.h
#include <Rinternals.h> // for SEXP extern SEXP spline_basis(SEXP knots, SEXP order, SEXP xvals, SEXP derivs); extern SEXP spline_value(SEXP knots, SEXP coeff, SEXP order, SEXP x, SEXP deriv);
有工具可供萃取宣告,至少適用於 C 和 C++ 程式碼:請參閱套件 tools 中 package_native_routine_registration_skeleton
的說明檔案。在此我們可以使用
cproto -I/path/to/R/include -e splines.c
如需其他呼叫類型註冊範例,請參閱套件 graphics 和 stats。特別是在為 .Fortran
註冊進入點時,需要宣告,就像從 C 呼叫一樣,例如
#include <R_ext/RS.h> void F77_NAME(supsmu)(int *n, double *x, double *y, double *w, int *iper, double *span, double *alpha, double *smo, double *sc, double *edf);
gfortran
8.4、9.2 和更新版本可以使用其旗標 -fc-prototypes-external 產生此類原型(雖然需要用 F77_NAME
巨集取代硬式編碼的尾隨底線)。
宣告中的引數清單不精確是可以的:很容易為 .Call
(全部 SEXP
)和 .External
(一個 SEXP
)指定引數,且由於 .C
和 .Fortran
的引數全部都是指標,將它們指定為 void *
就夠了。(在大部分平台上都可以省略所有引數,雖然連結時間最佳化會發出警告,設定為對嚴格原型發出警告的編譯器也會發出警告,而 C23 會需要正確的引數。)
使用 -fc-prototypes-external 會產生使用 int_least32_t *lgl
的原型,適用於 Fortran LOGICAL LGL
,但這不可移植,傳統上假設 C/C++ 等效項是 int *lgl
。如果只新增一個宣告來註冊 .Fortran
呼叫,最可移植的版本是 void *lgl
。
.Call
等使用您選擇註冊的符號,方法是編輯 src/init.c 以包含
void R_init_splines(DllInfo *dll) { R_registerRoutines(dll, NULL, R_CallDef, NULL, NULL); R_useDynamicSymbols(dll, FALSE); }
可以使用 tools 套件中的 package_native_routine_registration_skeleton
建立目前步驟的架構。這將根據 R 程式碼中的用法,選擇性建立宣告。
其餘步驟為選用,但建議執行。
useDynLib(splines, .registration = TRUE, .fixes = "C_")
temp <- .Call("spline_basis", knots, ord, x, derivs, PACKAGE = "splines") y[accept] <- .Call("spline_value", knots, coeff, ord, x[accept], deriv, PACKAGE = "splines") y = .Call("spline_value", knots, coef(object), ord, x, deriv, PACKAGE = "splines")
為
temp <- .Call(C_spline_basis, knots, ord, x, derivs) y[accept] <- .Call(C_spline_value, knots, coeff, ord, x[accept], deriv) y = .Call(C_spline_value, knots, coef(object), ord, x, deriv)
檢查是否沒有 exportPattern
指令,而意外匯出新建立的 R 物件。
.Call
使用 R 符號,方法是編輯 src/init.c,使其包含
void R_init_splines(DllInfo *dll) { R_registerRoutines(dll, NULL, R_CallDef, NULL, NULL); R_useDynamicSymbols(dll, FALSE); R_forceSymbols(dll, TRUE); }
nm -g /path/to/splines.so | grep " T " 0000000000002e00 T _R_init_splines 00000000000025e0 T _spline_basis 0000000000001e20 T _spline_value
如果有任何進入點並非預期由套件使用,我們應避免匯出它們,例如透過將它們設為 static
。現在,由於兩個相關進入點僅 透過 註冊表格存取,我們可以隱藏它們。在某些類 Unix 系統上,有兩種方法可以執行此操作。我們可以 透過
#include <R_ext/Visibility.h> SEXP attribute_hidden spline_basis(SEXP knots, SEXP order, SEXP xvals, SEXP derivs) ... SEXP attribute_hidden spline_value(SEXP knots, SEXP coeff, SEXP order, SEXP x, SEXP deriv) ...
隱藏個別進入點
PKG_CFLAGS = $(C_VISIBILITY)
或者,我們可以透過納入
#include <R_ext/Visibility.h> void attribute_visible R_init_splines(DllInfo *dll) ...
變更所有 C 符號的預設可見性,在 src/Makevars 中,然後我們需要允許註冊,方法是宣告 R_init_splines
為可見
#include <stdlib.h> #include <R_ext/Rdynload.h> #include <R_ext/Visibility.h> // optional #include "splines.h" #define CALLDEF(name, n) {#name, (DL_FUNC) &name, n} static const R_CallMethodDef R_CallDef[] = { CALLDEF(spline_basis, 4), CALLDEF(spline_value, 5), {NULL, NULL, 0} }; void attribute_visible // optional R_init_splines(DllInfo *dll) { R_registerRoutines(dll, NULL, R_CallDef, NULL, NULL); R_useDynamicSymbols(dll, FALSE); R_forceSymbols(dll, TRUE); }
除了註冊 C 常式供 R 呼叫之外,有時讓一個套件提供一些 C 常式供其他套件中的 C 程式碼呼叫會很有用。介面包含兩個常式,在標頭 R_ext/Rdynload.h 中宣告如下
void R_RegisterCCallable(const char *package, const char *name, DL_FUNC fptr); DL_FUNC R_GetCCallable(const char *package, const char *name);
一個套件 packA 若要讓 C 常式 myCfun
可供其他套件中的 C 程式碼使用,會在它的初始化函式 R_init_packA
中包含呼叫
R_RegisterCCallable("packA", "myCfun", myCfun);
一個套件 packB 若要使用這個常式,會使用下列形式的呼叫來擷取函式指標
p_myCfun = R_GetCCallable("packA", "myCfun");
由於類型 DL_FUNC
僅適用於沒有引數的函式,其他使用者需要轉換為適當的類型。例如
typedef SEXP (*na_omit_xts_func) (SEXP x); ... na_omit_xts_func fun = (na_omit_xts_func) R_GetCCallable("xts", "na_omit_xts"); return fun(x);
packB 的作者負責確保 p_myCfun
有適當的宣告。未來 R 可能提供一些自動化工具來簡化匯出大量常式。
一個套件若要使用其他套件中的標頭檔,需要在 DESCRIPTION 檔案的欄位「LinkingTo」中,以逗號分隔的清單宣告它們。這會安排將已安裝的連結套件中的 include 目錄新增到 C 和 C++ 程式碼的 include 路徑。
它必須指定148這些套件的「Imports」或「Depends」,因為它們必須在此套件之前載入149(因此已註冊其編譯程式碼的路徑)。
CRAN 此機制的範例包括 coxme 連結到 bdsmatrix,以及 xts 連結到 zoo。
注意:此機制很脆弱,因為 packA 提供的介面變更必須由 packB 辨識。未這樣做的後果包括 R 會話的記憶體池嚴重損毀。 packB 必須仰賴 packA 的確切版本,或者需要有讓 packB 在執行階段測試它連結的 packA 版本是否與其編譯版本相符的機制。
假設我們有以下假設的 C++ 函式庫,包含兩個檔案 X.h 和 X.cpp,並實作我們想在 R 中使用的兩個類別 X
和 Y
。
// X.h class X { public: X (); ~X (); }; class Y { public: Y (); ~Y (); };
// X.cpp #include <R.h> #include "X.h" static Y y; X::X() { REprintf("constructor X\n"); } X::~X() { REprintf("destructor X\n"); } Y::Y() { REprintf("constructor Y\n"); } Y::~Y() { REprintf("destructor Y\n"); }
若要搭配 R 使用,我們唯一要做的就是撰寫一個包裝函式,並確保函式封裝在
extern "C" { }
例如,
// X_main.cpp: #include "X.h" extern "C" { void X_main () { X x; } } // extern "C"
編譯和連結應使用 C++ 編譯器連結器(而非 C 編譯器連結器或連結器本身)執行;否則,C++ 初始化程式碼(以及靜態變數 Y
的建構函式)不會被呼叫。在適當設定的系統上,我們可以使用
R CMD SHLIB X.cpp X_main.cpp
建立共用物件,通常是 X.so(檔案名稱副檔名在您的平台上可能不同)。現在啟動 R 會產生
R version 2.14.1 Patched (2012-01-16 r58124) Copyright (C) 2012 The R Foundation for Statistical Computing ... Type "q()" to quit R.
R> dyn.load(paste("X", .Platform$dynlib.ext, sep = "")) constructor Y R> .C("X_main") constructor X destructor X list() R> q() Save workspace image? [y/n/c]: y destructor Y
Windows 版 R FAQ (rw-FAQ) 包含如何在 Windows 下編譯此範例的詳細資料。
此範例的早期版本使用 C++ iostream:最好避免使用。無法保證輸出會顯示在 R 主控台中,而且在 Windows 版 R 主控台中確實不會顯示。如果可能,請對所有 I/O 使用 R 程式碼或 C 進入點(請參閱 列印)。我們看過一些範例,其中載入包含對 C++ I/O 呼叫的 DLL 會影響 R 本身的 C I/O(例如,重設開啟檔案的緩衝區)。
大多數 R 標頭檔案都可以在 C++ 程式中包含,但 不應包含在 extern "C"
區塊中(因為它們包含系統標頭150)。
許多外部 C++ 軟體都是僅標頭檔 (例如,大部分的 Boost「函式庫」,包括套件 BH 提供的所有函式庫,以及套件 RcppArmadillo 提供的大部分 Armadillo),因此會在安裝使用它的 R 套件時編譯。這會造成一些問題。
R 套件中使用的少數外部函式庫具有編譯程式碼函式庫的 C++ 介面,例如套件 sf 和 rjags。這會產生更多問題!C++ 介面使用名稱混淆,而 ABI151 可能取決於編譯器、版本,甚至 C++ 定義152,因此需要以與函式庫完全相同的方式編譯套件 C++ 程式碼 (而這通常未記錄)。
更少的外部函式庫在內部使用 C++ 但提供 C 介面,例如 rgeos。這些函式庫需要將 C++ 執行時期函式庫連結到套件的共用物件/DLL,而最好的方法是在套件來源中包含一個虛擬 C++ 檔案。
有一個趨勢是連結到 C 軟體提供的 C++ 介面,例如 hdf5、pcre 和 ImageMagick。它們的 C 介面在可攜性方面更受青睞 (且可從 C++ 程式碼使用)。此外,C++ 介面在軟體建置中通常是選用的,或個別封裝,因此從套件來源安裝的使用者不太可能已經安裝它們。
我們已經警告不要使用 C++ iostreams,最主要的原因是輸出無法保證會顯示在 R 主控台上,而且此警告同樣適用於 Fortran 輸出到單元 *
和 6
。請參閱 從 Fortran 列印,其中說明了解決方法。
過去,大多數 Fortran 編譯器在 C I/O 系統之上實作 I/O,因此兩者可以成功地交互運作。這適用於 g77
,但對於 gcc
4 和更新版本中使用的 gfortran
來說則不然。特別是,任何使用 Fortran I/O 的套件在 Windows 上編譯時會干擾 C I/O:當 Fortran I/O 支援程式碼初始化時(通常在載入套件時),C stdout
和 stderr
會切換為 LF 行尾。(檔案 src/modules/lapack/init_win.c 中的函式 init
顯示如何減輕此問題。在套件中,這看起來像
#ifdef _WIN32 # include <fcntl.h> #endif void R_init_mypkgname(DllInfo *dll) { // Native symbol registration calls #ifdef _WIN32 // gfortran I/O initialization sets these to _O_BINARY setmode(1, _O_TEXT); /* stdout */ setmode(2, _O_TEXT); /* stderr */ #endif }
在用於原生符號註冊的檔案中。)
通常無法將套件 packA 中的 DLL 連結到套件 packB 提供的 DLL(原因是 dyn.load
和 dyn.unload
中提到的安全性原因,而且也因為有些平台會區分共用物件和動態函式庫),但在 Windows 上可以。
請注意,這裡可能會出現棘手的版本問題,因為套件 packB 可以在套件 packA 之後重新安裝 — 最好套件 packB 提供的 API 保持向下相容性。
在套件 packB 中運送靜態函式庫以供其他套件連結,可避免大部分的困難。
在類 Unix 作業系統上,在有限的狀況下,可以將套件 packA 中的共用物件連結到套件 packB 提供的函式庫。存在嚴重的可攜性問題,因此不建議用於分散式套件。
如果 packB 提供靜態函式庫 packB/lib/libpackB.a,這會是最簡單的。(請注意,慣例上會使用目錄 lib 而不是 libs,而且可能需要特定於架構的子目錄,並假設在以下範例程式碼中。在重要的平台上,靜態函式庫中的程式碼需要使用 PIC
旗標編譯。)然後,當套件 packA 安裝時,會納入套件 packB 中的程式碼,我們只需要在套件 packA 的安裝時間找到靜態函式庫。唯一的問題是找到套件 packB,而且我們可以透過類似以下方式詢問 R(此處折行以利顯示)
PKGB_PATH=‘echo ’library(packB); cat(system.file("lib", package="packB", mustWork=TRUE))' \ | "${R_HOME}/bin/R" --vanilla --no-echo` PKG_LIBS="$(PKGB_PATH)$(R_ARCH)/libpackB.a"
對於動態函式庫 packB/lib/libpackB.so(macOS 上的 packB/lib/libpackB.dylib:請注意,您無法在該平台上連結到共用物件 .so),我們可以使用
PKGB_PATH=‘echo ’library(packB); cat(system.file("lib", package="packB", mustWork=TRUE))' \ | "${R_HOME}/bin/R" --vanilla --no-echo` PKG_LIBS=-L"$(PKGB_PATH)$(R_ARCH)" -lpackB
這適用於安裝,但當載入套件 packB
時很可能不適用,因為套件 packB 的 lib 目錄的路徑不在 ld.so
153 搜尋路徑中。您可以在 R 啟動 之前 透過設定(在某些平台上)LD_RUN_PATH
或 LD_LIBRARY_PATH
或新增至 ld.so
快取(請參閱 man ldconfig
)來安排將其置於其中。在支援的平台上,包含動態函式庫的目錄路徑可以在安裝時硬編碼(假設套件 packB 的位置不會變更,也不會更新為已變更的 API)。在具有 gcc
或 clang
和 GNU 連結器(例如 Linux)和其他一些系統上,這可以使用例如以下方式來完成:
PKGB_PATH=‘echo ’library(packB); cat(system.file("lib", package="packB", mustWork=TRUE)))' \ | "${R_HOME}/bin/R" --vanilla --no-echo` PKG_LIBS=-L"$(PKGB_PATH)$(R_ARCH)" -Wl,-rpath,"$(PKGB_PATH)$(R_ARCH)" -lpackB
一些其他系統(例如具有原生連結器的 Solaris)使用 -Rdir 而不是 -rpath,dir(而且編譯器和連結器都接受這一點)。
有可能從 R CMD libtool --config
的結果中半自動找出需要什麼(尋找「hardcode」)。
讓套件 packB 提供的標頭可供套件 packA 中要編譯的程式碼使用,可以使用 LinkingTo
機制(請參閱 註冊原生常式)。
假設套件 packA 要使用 packB 在 DLL packB/libs/exB.dll 中提供的已編譯程式碼,可能是套件的 DLL packB/libs/packB.dll。(這可以延伸到以類似的方式連結到多個套件。)有三個問題需要解決
這是透過 LinkingTo
機制來完成的(請參閱 註冊原生常式)。
packA.dll
以連結到 packB/libs/exB.dll。
這需要在 Makevars.win 或 Makevars.ucrt 中有下列形式的項目
PKG_LIBS= -L<something> -lexB
而一種可能性是 <something>
是已安裝的 pkgB/libs 目錄的路徑。要找出我們需要詢問 R 它在哪裡,例如
PKGB_PATH=‘echo ’library(packB); cat(system.file("libs", package="packB", mustWork=TRUE))' \ | rterm --vanilla --no-echo` PKG_LIBS= -L"$(PKGB_PATH)$(R_ARCH)" -lexB
另一種可能性是使用匯入函式庫,將套件 packA 與匯出檔案 exB.def 一起提供。然後 Makevars.win(或 Makevars.ucrt)可以包含
PKG_LIBS= -L. -lexB all: $(SHLIB) before before: libexB.dll.a libexB.dll.a: exB.def
然後安裝套件 packA 將建立並使用 exB.dll 的匯入函式庫。(準備匯出檔案的一種方法是使用 pexports.exe。)
如果 exB.dll
由套件 packB 使用(因為它實際上是 packB.dll 或 packB.dll 依賴它),而 packB 已在 packA 之前載入,則不需要執行任何動作,因為 exB.dll 已載入 R 可執行檔中。(這是最常見的情況。)
更一般來說,我們可以使用 DLLpath
參數傳遞給 library.dynam
,以確保找到 exB.dll
,例如透過設定
library.dynam("packA", pkg, lib, DLLpath = system.file("libs", package="packB"))
請注意,DLLpath
只可以設定一個路徑,因此要連結到兩個或更多個套件,您需要設定環境變數 PATH
。
使用 C 程式碼加速 R 函式的執行通常非常有成效。傳統上這是透過 R 中的 .C
函式來完成的。但是,如果使用者想要使用內部 R 資料結構撰寫 C 程式碼,則可以使用 .Call
和 .External
函式來完成。在每種情況下,R 中呼叫函式的語法都類似於 .C
,但這兩個函式具有不同的 C 介面。一般來說,.Call
介面比較容易使用,但 .External
比較通用。
呼叫 .Call
與 .C
非常類似,例如
.Call("convolve2", a, b)
第一個引數應為字元串,提供已載入 R 的程式碼的 C 符號名稱。最多可傳遞 65 個 R 物件作為引數。介面的 C 端為
#include <R.h> #include <Rinternals.h> SEXP convolve2(SEXP a, SEXP b) ...
呼叫 .External
幾乎相同
.External("convolveE", a, b)
但介面的 C 端不同,只有一個引數
#include <R.h> #include <Rinternals.h> SEXP convolveE(SEXP args) ...
在此,args
是 LISTSXP
,一種 Lisp 型態的配對清單,可從中擷取引數。
在每種情況下,R 物件都可透過標頭檔案 Rinternals.h 中定義的一組函式和巨集或一些 S 相容性巨集154 來進行操作。請參閱 介面函式 .Call
和 .External
以取得 .Call
和 .External
的詳細資料。
在決定使用 .Call
或 .External
之前,您應查看其他替代方案。首先,考慮在已詮釋的 R 程式碼中工作;如果夠快,這通常是最佳選項。您還應查看使用 .C
是否足夠。如果在 C 中執行的任務夠簡單,只涉及原子向量且不需要呼叫 R,則 .C
就足夠了。在 .Call
和 .External
可用之前,已使用 .C
編寫了許多有用的程式碼。這些介面允許更多控制,但它們也賦予更大的責任,因此需要小心使用。 .Call
和 .External
都不會複製其引數:您應將透過這些介面接收的引數視為唯讀。
若要從 C 程式碼中處理 R 物件,我們會使用巨集和函式,這些巨集和函式已用於實作 R 的核心部分。這些巨集和函式的一個公開155子集定義在標頭檔 Rinternals.h 中,該標頭檔位於目錄 R_INCLUDE_DIR (預設為 R_HOME/include) 中,任何 R 安裝都應該有這個目錄。
大量的 R,包括標準套件,都是使用這裡說明的函式和巨集實作的,因此 R 原始碼提供了豐富的範例和「如何執行」的來源:請善用原始碼作為靈感的範例。
有必要了解 R 物件在 C 程式碼中是如何處理的。您將處理的所有 R 物件都將使用類型 SEXP156 處理,它是指向具有 typedef SEXPREC
的結構的指標。將此結構視為一個變異類型,它可以處理所有常見的 R 物件類型,也就是各種模式的向量、函式、環境、語言物件等。詳細資訊會在稍後本節和 R 內部結構 中的 R 內部結構 中提供,但對於大多數目的而言,程式設計師不需要知道這些資訊。不如考慮一個類似 Visual Basic 所使用的模型,其中 R 物件在 C 程式碼中(就像在已詮釋的 R 程式碼中一樣)以變異類型傳遞,並且僅在需要時擷取適當的部分,例如數值計算。就像在已詮釋的 R 程式碼中一樣,會大量使用強制轉換,以將變異物件轉換為正確的類型。
我們需要稍微了解 R 處理記憶體配置的方式。R 物件配置的記憶體並非由使用者釋放;相反地,記憶體會不時進行垃圾回收。也就是說,部分或全部未使用的配置記憶體會被釋放或標記為可重複使用。
R 物件型別由 C 結構表示,由 Rinternals.h 中的 typedef SEXPREC
定義。它包含許多內容,其中包括指向資料區塊和其它 SEXPREC
的指標。SEXP
僅為指向 SEXPREC
的指標。
如果您在 C 程式碼中建立 R 物件,您必須使用物件指標上的 PROTECT
巨集來告訴 R 您正在使用該物件。這會告訴 R 該物件正在使用中,因此不會在垃圾回收期間被銷毀。請注意,受到保護的是物件,而不是指標變數。一般人常誤以為只要在某個時間點呼叫 PROTECT(p)
,p 就會從此受到保護,但一旦將新物件指定給 p,就不再成立。
保護 R 物件會自動保護對應 SEXPREC
中所指向的所有 R 物件,例如受保護清單中的所有元素都會自動受到保護。
程式設計人員需自行負責維護呼叫 PROTECT
。有一個對應的巨集 UNPROTECT
,它將 int
作為引數,提供不再需要時要解除保護的物件數目。保護機制基於堆疊,因此 UNPROTECT(n)
會解除保護最後 n 個受保護的物件。當使用者的程式碼傳回時,呼叫 PROTECT
和 UNPROTECT
必須取得平衡,且應在所有函式中取得平衡。如果維護錯誤,R 會針對 「.Call 中的堆疊不平衡」
(或 .External
)發出警告。
以下是在 C 程式碼中建立 R 數值向量的簡短範例
#include <R.h> #include <Rinternals.h> SEXP ab; .... ab = PROTECT(allocVector(REALSXP, 2)); REAL(ab)[0] = 123.45; REAL(ab)[1] = 67.89; UNPROTECT(1);
現在,讀者可能會問,由於執行的是我們的 C 程式碼,R 物件怎麼可能在這些操作期間被移除。事實上,我們可以在這個範例中不使用保護,但一般來說我們不知道(也不想知道)我們使用的 R 巨集和函式背後隱藏了什麼,其中任何一個都可能導致記憶體配置,因此垃圾回收,進而移除我們的物件 ab
。通常最好謹慎行事,並假設任何 R 巨集和函式都可能移除物件。
在某些情況下,有必要更仔細追蹤是否真的需要保護。特別注意產生大量物件的情況。指標保護堆疊具有固定的尺寸(預設為 10,000),且可能會滿。因此,最好不要只是 PROTECT
所有可見的物件,並在最後 UNPROTECT
數千個物件。幾乎總是有可能將物件指定為另一個物件的一部分(這會自動保護它們),或在使用後立即解除它們的保護。
有一個較少使用的巨集 UNPROTECT_PTR(s)
,它會解除由 SEXP
s 指向的物件的保護,即使它不是指標保護堆疊中的頂端項目。此巨集是為了在剖析器中使用而引入的,其中會產生與 R 堆疊介接的程式碼,而且無法將產生器設定為插入適當的呼叫至 PROTECT
和 UNPROTECT
。不過,當同一物件已受保護多次時,UNPROTECT_PTR
與 UNPROTECT
搭配使用很危險。它已被基於多組的函數 R_PreserveInMSet
和 R_ReleaseFromMSet
取代,這些函數會保護由 R_NewPreciousMSet
建立的多組中的物件,而且通常會使用 PROTECT
來保護它本身。剖析器外部不應需要這些函數。
有時會變更物件(例如複製、強制轉換或增加),但需要保護目前的數值。在這些情況下,PROTECT_WITH_INDEX
會儲存保護位置的索引,可以使用 REPROTECT
來使用此索引取代受保護的數值。 例如(來自 optim
的內部程式碼)
PROTECT_INDEX ipx; .... PROTECT_WITH_INDEX(s = eval(OS->R_fcall, OS->R_env), &ipx); REPROTECT(s = coerceVector(s, REALSXP), ipx);
請注意,將 UNPROTECT_PTR
與 PROTECT_WITH_INDEX
混用也很危險,因為前者會變更在受保護物件之後受保護的物件的保護位置。
避免垃圾回收效應的另一種方法:呼叫 R_PreserveObject
會將物件新增至內部不收集物件的清單中,而後續呼叫 R_ReleaseObject
會將其從清單中移除。這提供了一種方式,讓未傳回為 R 物件一部分的物件在呼叫編譯碼時受到保護:另一方面,使用者有責任在不再需要時釋放這些物件(這通常需要使用完成處理函式)。這比一般的保護機制效率較低,應謹慎使用。
為了讓來自套件和 R 的函式在保護物件時安全地共同運作,必須遵守特定規則
PROTECT
和 UNPROTECT
應取得平衡。函式只能對其自身保護的物件呼叫 UNPROTECT
或 REPROTECT
。請注意,指標保護堆疊平衡會在非區域控制轉移時自動還原(請參閱 條件處理和清理碼),就像呼叫 UNPROTECT
並使用正確的引數一樣。
PROTECT
和 UNPROTECT
呼叫來達成。
遵循這些規則總是安全的,而且建議這麼做。事實上,有幾個 R 函式和巨集會保護它們自己的引數,有些函式不會配置,或者在以特定方式使用時不會配置,但這可能會變更,因此依賴它可能會很脆弱。PROTECT
和 PROTECT_WITH_INDEX
可以安全地使用未受保護的引數呼叫,而 UNPROTECT
也不會配置。
對於許多目的來說,配置 R 物件並操作它們就已足夠。在 Rinternals.h 中定義了相當多的 allocXxx
函式,您可能想探索它們。
一個常用的函數是 allocVector
,它是 R 層級 vector()
及其包裝函數(例如 integer()
和 character()
)在 C 層級的等效函數。兩者之間的一個區別是,R 函數總是會初始化向量的元素,而 allocVector
僅會對清單、表達式和字元向量執行此動作(元素本身是 R 物件的情況)。
如果計算期間需要為 C 物件儲存資料,最佳的配置方式是呼叫 R_alloc
;請參閱 記憶體配置。這些記憶體配置常式都會執行自己的錯誤檢查,因此程式設計人員可以假設如果無法配置記憶體,它們會引發錯誤,而不會傳回。
使用 Rinternals.h 巨集的使用者需要知道 R 類型在內部是如何表示的。不同的 R 資料類型在 C 中由 SEXPTYPE 表示。其中一些類型在 R 中很常見,而另一些則是內部資料類型。下表提供了常見的 R 物件模式。
SEXPTYPE R 等效類型 REALSXP
儲存模式為 double
的數值INTSXP
integer CPLXSXP
complex LGLSXP
邏輯 STRSXP
character VECSXP
清單(一般向量) LISTSXP
配對清單 DOTSXP
‘…’ 物件 NILSXP
NULL SYMSXP
名稱/符號 CLOSXP
函數或函數閉包 ENVSXP
環境
在重要的內部 SEXPTYPE
中有 LANGSXP
、CHARSXP
、PROMSXP
等。(注意:儘管可以傳回內部類型的物件,但這麼做並不安全,因為假設了處理方式,而這些假設可能會在使用者層級評估時遭到破壞。)在 R 內部結構 的 R 內部結構 中提供了更多詳細資料。
除非您非常確定引數的類型,否則程式碼應該檢查資料類型。有時也可能需要檢查由評估 C 程式碼中的 R 表達式所建立的物件的資料類型。您可以使用 isReal
、isInteger
和 isString
等函式來進行類型檢查。 在標頭檔 Rinternals.h 中宣告的其他此類函式包括 isNull
、isSymbol
、isLogical
、isComplex
、isExpression
和 isEnvironment
。 這些函式都將 SEXP
作為引數,並傳回 1 或 0 來表示 TRUE 或 FALSE。
如果 SEXP
不是正確的類型會發生什麼事?有時你沒有其他選擇,只能產生錯誤。你可以使用函式 error
來執行此操作。通常強制轉換物件為正確的類型會更好。例如,如果你發現 SEXP
是 INTEGER
類型,但你需要 REAL
物件,你可以使用下列方式變更類型
newSexp = PROTECT(coerceVector(oldSexp, REALSXP));
需要保護,因為會建立新的 物件;先前由 SEXP
指向的物件仍然受到保護,但現在未使用。157
所有強制轉換函式都會執行自己的錯誤檢查,並產生 NA
,並視情況發出警告或停止錯誤。
請注意,這些強制轉換函式與在 R 程式碼中呼叫 as.numeric
(等等)不同,因為它們不會針對物件的類別進行分派。因此,通常最好在呼叫 R 程式碼中執行強制轉換。
到目前為止,我們只看過如何從 C 程式碼建立和強制轉換 R 物件,以及如何從數值 R 向量中擷取數值資料。這些足以讓我們在將 R 物件與數值演算法介接時走很長一段路,但我們可能需要知道更多才能建立有用的回傳物件。
許多 R 物件都有屬性:最有用的一些是類別,以及將物件標記為矩陣或陣列的 dim
和 dimnames
。與向量的 names
屬性一起使用也很有幫助。
為了說明這一點,讓我們撰寫程式碼來取得兩個向量的外部乘積(outer
和 %o%
已經執行了)。R 程式碼通常很簡單
out <- function(x, y) { storage.mode(x) <- storage.mode(y) <- "double" .Call("out", x, y) }
我們預期 x
和 y
是數值向量(可能是整數),可能帶有名稱。這次我們在呼叫 R 程式碼時進行強制轉換。
執行運算的 C 程式碼為
#include <R.h> #include <Rinternals.h> SEXP out(SEXP x, SEXP y) { int nx = length(x), ny = length(y); SEXP ans = PROTECT(allocMatrix(REALSXP, nx, ny)); double *rx = REAL(x), *ry = REAL(y), *rans = REAL(ans); for(int i = 0; i < nx; i++) { double tmp = rx[i]; for(int j = 0; j < ny; j++) rans[i + nx*j] = tmp * ry[j]; } UNPROTECT(1); return ans; }
請注意 REAL
的使用方式:由於它是函式呼叫,因此儲存結果並對其編制索引會快很多。
不過,我們想要設定結果的 dimnames
。我們可以使用
#include <R.h> #include <Rinternals.h>
SEXP out(SEXP x, SEXP y) { int nx = length(x), ny = length(y); SEXP ans = PROTECT(allocMatrix(REALSXP, nx, ny)); double *rx = REAL(x), *ry = REAL(y), *rans = REAL(ans); for(int i = 0; i < nx; i++) { double tmp = rx[i]; for(int j = 0; j < ny; j++) rans[i + nx*j] = tmp * ry[j]; } SEXP dimnames = PROTECT(allocVector(VECSXP, 2)); SET_VECTOR_ELT(dimnames, 0, getAttrib(x, R_NamesSymbol)); SET_VECTOR_ELT(dimnames, 1, getAttrib(y, R_NamesSymbol)); setAttrib(ans, R_DimNamesSymbol, dimnames);
UNPROTECT(2); return ans; }
此範例介紹了幾個新功能。 getAttrib
和 setAttrib
函式會取得和設定個別屬性。它們的第二個引數是 SEXP
,用於定義我們想要的屬性在符號表中的名稱;這些符號和許多此類符號都定義在標頭檔 Rinternals.h 中。
這裡也有捷徑:函式 namesgets
、dimgets
和 dimnamesgets
是 names<-
、dim<-
和 dimnames<-
(針對向量和陣列)的預設方法的內部版本,而且有函式,例如 GetColNames
、GetRowNames
、GetMatrixDimnames
和 GetArrayDimnames
。
如果我們想要新增未預先定義的屬性,會發生什麼事?我們需要透過呼叫 install
為其新增符號。假設為了說明,我們想要新增屬性 "version"
,其值為 3.0
。我們可以使用
SEXP version; version = PROTECT(allocVector(REALSXP, 1)); REAL(version)[0] = 3.0; setAttrib(ans, install("version"), version); UNPROTECT(1);
在不需要時使用 install
是無害的,而且如果符號已經安裝,它提供從符號表中擷取符號的簡單方式。不過,查詢會花費大量時間,因此請考慮使用以下程式碼
static SEXP VerSymbol = NULL; ... if (VerSymbol == NULL) VerSymbol = install("version");
如果要頻繁執行。
這個範例可以用另一個方便的函數簡化
SEXP version = PROTECT(ScalarReal(3.0)); setAttrib(ans, install("version"), version); UNPROTECT(1);
在 R 中,類別只是一個名為 "class"
的屬性,因此可以這樣處理,但有一個捷徑 classgets
。假設我們要給範例中的回傳值類別 "mat"
。我們可以使用
#include <R.h> #include <Rinternals.h> .... SEXP ans, dim, dimnames, class; .... class = PROTECT(allocVector(STRSXP, 1)); SET_STRING_ELT(class, 0, mkChar("mat")); classgets(ans, class); UNPROTECT(4); return ans; }
由於值是一個字元向量,我們必須知道如何從 C 字元陣列建立該值,我們使用函數 mkChar
來執行此操作。
處理清單時需要小心,因為 R 很早就從使用類似 LISP 的清單(現在稱為「配對清單」)轉移到類似 S 的通用向量。因此,測試模式為 list
的物件的適當測試為 isNewList
,我們需要 allocVector(VECSXP, n
) 和 而不是 allocList(n)
。
清單元素可以透過直接存取通用向量的元素來擷取或設定。假設我們有一個清單物件
a <- list(f = 1, g = 2, h = 3)
然後我們可以透過 a[[2]]
存取 a$g
double g; .... g = REAL(VECTOR_ELT(a, 1))[0];
這可能會很快地變得繁瑣,而下列函數(基於 stats 套件中的函數)非常有用
/* get the list element named str, or return NULL */ SEXP getListElement(SEXP list, const char *str) { SEXP elmt = R_NilValue, names = getAttrib(list, R_NamesSymbol);
for (int i = 0; i < length(list); i++) if(strcmp(CHAR(STRING_ELT(names, i)), str) == 0) { elmt = VECTOR_ELT(list, i); break; } return elmt; }
並讓我們可以說
double g; g = REAL(getListElement(a, "g"))[0];
R 字元向量儲存在 STRSXP
中,這是一個類似 VECSXP
的向量類型,其中每個元素都是 CHARSXP
類型。STRSXP
的 CHARSXP
元素使用 STRING_ELT
和 SET_STRING_ELT
存取。
CHARSXP
是唯讀物件,絕不能修改。特別是,CHARSXP
中包含的 C 式字串應視為唯讀,因此用於存取 CHARSXP
字元資料的 CHAR
函數會傳回 (const char *)
(這也允許編譯器針對不當使用發出警告)。由於 CHARSXP
是不可變的,因此任何需要表示相同字串元素的 STRSXP
都可以使用相同的 CHARSXP
。R 維護 CHARSXP
的全球快取,因此在記憶體中只會有一個 CHARSXP
表示一個給定的字串。
您可以透過呼叫 mkChar
並提供以空字元結尾的 C 式字串來取得 CHARSXP
。如果已存在符合字串的 CHARSXP
,此函式會傳回現有的 CHARSXP
,否則會建立新的 CHARSXP
並將其加入快取,再傳回給您。變數 mkCharLen
可用來從緩衝區的一部分建立 CHARSXP
,並會確保以空字元結尾。
請注意,R 字元串限制在 2^31 - 1
位元組,因此 mkChar
的輸入也應如此(C 在 64 位元平台允許較長的字串)。
通常,C 計算中所需的所有 R 物件都會作為引數傳遞給 .Call
或 .External
,但也可以在 C 中根據物件名稱找出 R 物件的值。下列程式碼等同於 get(name, envir = rho)
。
SEXP getvar(SEXP name, SEXP rho) { SEXP ans; if(!isString(name) || length(name) != 1) error("name is not a single string"); if(!isEnvironment(rho)) error("rho should be an environment"); ans = findVar(installChar(STRING_ELT(name, 0)), rho); Rprintf("first value is %f\n", REAL(ans)[0]); return R_NilValue; }
主要工作由 findVar
執行,但要使用它,我們需要將 name
安裝為符號表中的名稱。由於我們想要內部使用的值,因此我們傳回 NULL
。
具有語法的類似函式
void defineVar(SEXP symbol, SEXP value, SEXP rho) void setVar(SEXP symbol, SEXP value, SEXP rho)
可用於將值指定給 R 變數。 defineVar
會建立新的繫結或變更指定環境框架中現有繫結的值;它類似於 assign(symbol, value, envir = rho, inherits = FALSE)
,但與 assign
不同的是, defineVar
不會複製物件 value
。158 setVar
會在 rho
或其封裝環境中搜尋 symbol
的現有繫結。如果找到繫結,其值會變更為 value
。否則,會在全域環境中建立具有指定值的新繫結。這對應於 assign(symbol, value, envir = rho, inherits = TRUE)
。
有時在 C 程式碼中建立新的環境框架也可能很有用。 R_NewEnv
是 R 函數 new.env
的 C 版本。
SEXP R_NewEnv(SEXP enclos, int hash, int size)
有些作業執行得非常頻繁,因此有方便的函數來處理它們。(所有這些都透過標頭檔 Rinternals.h 提供。)
假設我們想要傳遞單一邏輯引數 ignore_quotes
:我們可以使用
int ign = asLogical(ignore_quotes); if(ign == NA_LOGICAL) error("'ignore_quotes' must be TRUE or FALSE");
它會執行任何必要的強制轉換(至少從向量引數中),如果傳遞的值為 NA
或強制轉換失敗,則傳回 NA_LOGICAL
。還有 asInteger
、asReal
和 asComplex
。函式 asChar
傳回 CHARSXP
。這些函式都會略過輸入向量中第一個之後的所有元素。
若要傳回長度為一的實數向量,我們可以使用
double x; ... return ScalarReal(x);
所有原子向量類型都有此類型的版本(長度為一字元向量的版本為 ScalarString
,引數為 CHARSXP
,而 mkString
的引數為 const char *
)。
SEXP ScalarReal(double); SEXP ScalarInteger(int); SEXP ScalarLogical(int) SEXP ScalarRaw(Rbyte); SEXP ScalarComplex(Rcomplex); SEXP ScalarString(SEXP); SEXP mkString(const char *);
部分 isXXXX
函數與它們明顯的 R 層級對應函數不同:例如 isVector
對任何原子向量類型 (isVectorAtomic
) 和對清單和表達式 (isVectorList
) (不檢查屬性) 皆為真。 isMatrix
是長度為 2 的 "dim"
屬性的測試。
Rboolean isVector(SEXP); Rboolean isVectorAtomic(SEXP); Rboolean isVectorList(SEXP); Rboolean isMatrix(SEXP); Rboolean isPairList(SEXP); Rboolean isPrimitive(SEXP); Rboolean isTs(SEXP); Rboolean isNumeric(SEXP); Rboolean isArray(SEXP); Rboolean isFactor(SEXP); Rboolean isObject(SEXP); Rboolean isFunction(SEXP); Rboolean isLanguage(SEXP); Rboolean isNewList(SEXP); Rboolean isList(SEXP); Rboolean isOrdered(SEXP); Rboolean isUnordered(SEXP);
有一系列的小巨集/函數可協助建構配對清單和語言物件(其內部結構僅由 SEXPTYPE
不同)。函數 CONS(u, v)
是基本建構區塊:它從 u
後接 v
(它是一個配對清單或 R_NilValue
)建構一個配對清單。 LCONS
是一個變體,用於建構語言物件。函數 list1
至 list6
從一到六個項目建構一個配對清單,而 lang1
至 lang6
對語言物件執行相同的動作(一個函數呼叫加上零到五個引數)。 函數 elt
和 lastElt
找出配對清單中的第 i 個元素和最後一個元素,而 nthcdr
傳回一個指標,指向配對清單中的第 n 個位置(其 CAR
是第 n 個項目)。
函數 str2type
和 type2str
將 R 長度為一的字元字串對應到 SEXPTYPE
數字,反之亦然,而 type2char
將數字對應到 C 字元字串。
如果您願意適應罕見的「API」變更,您的 C 程式碼中可以使用相當多的函數。這些函數通常包含其 R 對應項的「主力」。
函數 any_duplicated
和 any_duplicated3
是 R 的 any(duplicated(.))
的快速版本。
函數 R_compute_identical
對應於 R 的 identical
函數。
[NAMED
機制已被參考計數取代。]
當在 R 中進行指定時,例如
x <- 1:10 y <- x
命名物件不一定會被複製,因此在上述兩個指定之後,y
和 x
會繫結到同一個 SEXPREC
(SEXP
指向的結構)。這表示任何會變更其中一個的程式碼都必須在修改複製項之前先建立複製項,才能套用一般的 R 語意。請注意,.C
和 .Fortran
會複製其引數(除非使用危險的 dup = FALSE
),但 .Call
和 .External
卻不會。因此,通常會在修改引數之前對 .Call
的引數呼叫 duplicate
。
然而,至少有些複製是不必要的。在第一個顯示的指派中,x <- 1:10
,R 首先建立一個值為 1:10
的物件,然後將其指派給 x
,但如果修改 x
,則不需要複製,因為無法再參考值為 1:10
的暫時物件。R 透過 SEXPREC
中的欄位區分命名和未命名物件,可以 透過 巨集 NAMED
和 SET_NAMED
存取該欄位。它可以採用下列值:
0
物件未繫結到任何符號
1
物件已繫結到恰好一個符號
>= 2
物件可能已繫結到兩個或更多符號,而且應該表現得好像另一個變數目前繫結到這個值。最大值為 NAMEDMAX
。
請注意過去式:R 目前不執行完整的參考計數,而且目前可能繫結較少。
修改任何 SEXP
的值是安全的,只要 NAMED(foo)
為零,而且如果 NAMED(foo)
為二或更多,則應該在修改之前複製值(透過 呼叫 duplicate
)。請注意,負責修改程式碼的作者有責任執行複製,即使是在 y <- x
之後修改 x
的值。
案例 NAMED(foo) == 1
允許一些最佳化,但可以忽略(而且在 NAMED(foo) > 0
時執行複製)。(使用者程式碼目前無法使用這個最佳化。)它用於替換函式中。假設我們使用
x <- 1:10 foo(x) <- 3
計算方式如下
x <- 1:10 x <- "foo<-"(x, 3)
然後在 "foo<-"
內部指向 x
目前值之物件會將 NAMED(foo)
設定為 1,且修改該值是安全的,因為唯一繫結到該值的符號為 x
,且該值會立即重新繫結。(假設 "foo<-"
中的剩餘程式碼未參照 x
,且沒有人會嘗試直接呼叫,例如 y <- "foo<-"(x)
。)
此機制已於 R 4.0.0 中取代。為了支援未來的變更,套件程式碼應使用巨集 MAYBE_REFERENCED
、MAYBE_SHARED
和 MARK_NOT_MUTABLE
。這些目前對應到
MAYBE_REFERENCED(x)
NAMED(x) > 0
MAYBE_SHARED(x)
NAMED(x) > 1
MARK_NOT_MUTABLE(x)
SET_NAMED(x, NAMEDMAX)
.Call
和 .External
¶在本節中,我們將探討 R/C 介面的詳細資訊。
這兩個介面具有幾乎相同的功能。.Call
是基於 S 版本 4 中的同名介面,而 .External
則基於 R 的 .Internal
。.External
較為複雜,但允許多個變數的引數。
.Call
¶讓我們將有限摺積範例轉換為使用 .Call
。R 中的呼叫函數為
conv <- function(a, b) .Call("convolve2", a, b)
這幾乎不可能更簡單,但正如我們所見,所有類型強制轉換都傳輸到 C 程式碼,如下所示
#include <R.h> #include <Rinternals.h> SEXP convolve2(SEXP a, SEXP b) { int na, nb, nab; double *xa, *xb, *xab; SEXP ab; a = PROTECT(coerceVector(a, REALSXP)); b = PROTECT(coerceVector(b, REALSXP)); na = length(a); nb = length(b); nab = na + nb - 1; ab = PROTECT(allocVector(REALSXP, nab)); xa = REAL(a); xb = REAL(b); xab = REAL(ab); for(int i = 0; i < nab; i++) xab[i] = 0.0; for(int i = 0; i < na; i++) for(int j = 0; j < nb; j++) xab[i + j] += xa[i] * xb[j]; UNPROTECT(3); return ab; }
.External
¶我們可以使用相同的範例來說明 .External
。R 程式碼只變更將 .Call
取代為 .External
conv <- function(a, b) .External("convolveE", a, b)
但是主要變更為如何將參數傳遞給 C 程式碼,這次是作為單一 SEXP。C 程式碼的唯一變更為我們處理參數的方式。
#include <R.h> #include <Rinternals.h> SEXP convolveE(SEXP args) { int i, j, na, nb, nab; double *xa, *xb, *xab; SEXP a, b, ab; a = PROTECT(coerceVector(CADR(args), REALSXP)); b = PROTECT(coerceVector(CADDR(args), REALSXP)); ... }
再次,我們不需要保護參數,因為在介面的 R 端,它們是已經在使用的物件。巨集
first = CADR(args); second = CADDR(args); third = CADDDR(args); fourth = CAD4R(args); fifth = CAD5R(args);
提供方便的方式來存取前四個參數。更普遍地,我們可以使用 CDR
和 CAR
巨集,如下所示
args = CDR(args); a = CAR(args); args = CDR(args); b = CAR(args);
這清楚地允許我們擷取無限數量的參數(而 .Call
有限制,儘管在 65 時並非很小)。
更有用的是,.External
介面提供一種處理具有變數數量參數的呼叫的簡便方式,因為 length(args)
會提供所提供參數的數量(其中第一個會被忽略)。我們可能需要知道給予實際參數的名稱(「標籤」),我們可以使用 TAG
巨集並使用類似下列範例的東西,如果它們是向量類型,則會列印名稱和其參數的第一個值。
SEXP showArgs(SEXP args) { args = CDR(args); /* skip ‘name’ */ for(int i = 0; args != R_NilValue; i++, args = CDR(args)) { const char *name = isNull(TAG(args)) ? "" : CHAR(PRINTNAME(TAG(args))); SEXP el = CAR(args); if (length(el) == 0) { Rprintf("[%d] ‘%s’ R type, length 0\n", i+1, name); continue; }
switch(TYPEOF(el)) { case REALSXP: Rprintf("[%d] ‘%s’ %f\n", i+1, name, REAL(el)[0]); break;
case LGLSXP: case INTSXP: Rprintf("[%d] ‘%s’ %d\n", i+1, name, INTEGER(el)[0]); break;
case CPLXSXP: { Rcomplex cpl = COMPLEX(el)[0]; Rprintf("[%d] ‘%s’ %f + %fi\n", i+1, name, cpl.r, cpl.i); } break;
case STRSXP: Rprintf("[%d] ‘%s’ %s\n", i+1, name, CHAR(STRING_ELT(el, 0))); break;
default: Rprintf("[%d] ‘%s’ R type\n", i+1, name); } } return R_NilValue; }
這可以由包裝函式呼叫
showArgs <- function(...) invisible(.External("showArgs", ...))
請注意,這種程式設計風格很方便,但不是必要的,因為替代風格是
showArgs1 <- function(...) invisible(.Call("showArgs1", list(...)))
(非常相似的)C 程式碼在指令碼中。
存取 pairlist 組件的附加函數為 CAAR
、CDAR
、CDDR
和 CDDDR
。 這些組件可以使用 SETCAR
、SETCDR
、SETCADR
、SETCADDR
、SETCADDDR
和 SETCAD4R
進行修改。
錯誤檢查 .C
呼叫所做的其中一件事(除非 NAOK
為真)是檢查遺失 (NA
) 和 IEEE 特殊值 (Inf
、-Inf
和 NaN
),並在找到任何值時傳回錯誤。使用 .Call
介面時,這些值會傳遞給我們的程式碼。在此範例中,特殊值不會造成問題,因為 IEC60559 算術會正確處理這些值。在目前的實作中,NA
也是如此,因為它是一種 NaN
,但依賴此類細節並不明智。因此,我們將重新編寫程式碼,使用 R.h 包含的 R_ext/Arith.h 中定義的巨集來處理 NA
。
在任何版本的 convolve2
或 convolveE
中,程式碼變更都是相同的。
... for(int i = 0; i < na; i++) for(int j = 0; j < nb; j++) if(ISNA(xa[i]) || ISNA(xb[j]) || ISNA(xab[i + j])) xab[i + j] = NA_REAL; else xab[i + j] += xa[i] * xb[j]; ...
請注意,ISNA
巨集,以及類似的巨集 ISNAN
(檢查 NaN
或 NA
)和 R_FINITE
(對 NA
和所有特殊值為假),僅適用於類型為 double
的數字值。整數、邏輯值和字元字串的遺失值可以透過與常數 NA_INTEGER
、NA_LOGICAL
和 NA_STRING
相等來測試。這些常數和 NA_REAL
可用於將 R 向量元素設定為 NA
。
常數 R_NaN
、R_PosInf
和 R_NegInf
可用於將 double
設定為特殊值。
我們將使用的主要函數是
SEXP eval(SEXP expr, SEXP rho);
等同於已詮釋 R 程式碼 eval(expr, envir = rho)
(因此 rho
必須是環境),儘管我們也可以使用 findVar
、defineVar
和 findFun
(將搜尋限制在函數)。
若要了解如何套用,以下是 lapply
的簡化內部版本,用於表達式,使用方式為
a <- list(a = 1:5, b = rnorm(10), test = runif(100)) .Call("lapply", a, quote(sum(x)), new.env())
使用 C 程式碼
SEXP lapply(SEXP list, SEXP expr, SEXP rho) { int n = length(list); SEXP ans; if(!isNewList(list)) error("'list' must be a list"); if(!isEnvironment(rho)) error("'rho' should be an environment"); ans = PROTECT(allocVector(VECSXP, n)); for(int i = 0; i < n; i++) { defineVar(install("x"), VECTOR_ELT(list, i), rho); SET_VECTOR_ELT(ans, i, eval(expr, rho)); } setAttrib(ans, R_NamesSymbol, getAttrib(list, R_NamesSymbol)); UNPROTECT(1); return ans; }
如果我們能傳遞函數而非表達式,將會更接近 lapply
。執行此操作的一種方法是透過已詮釋 R 程式碼,如以下範例所示,但也可以在 C 程式碼中執行此操作 (儘管有點難懂)。以下是根據 src/main/optimize.c 中的程式碼。
SEXP lapply2(SEXP list, SEXP fn, SEXP rho) { int n = length(list); SEXP R_fcall, ans; if(!isNewList(list)) error("'list' must be a list"); if(!isFunction(fn)) error("'fn' must be a function"); if(!isEnvironment(rho)) error("'rho' should be an environment"); R_fcall = PROTECT(lang2(fn, R_NilValue)); ans = PROTECT(allocVector(VECSXP, n)); for(int i = 0; i < n; i++) { SETCADR(R_fcall, VECTOR_ELT(list, i)); SET_VECTOR_ELT(ans, i, eval(R_fcall, rho)); } setAttrib(ans, R_NamesSymbol, getAttrib(list, R_NamesSymbol)); UNPROTECT(2); return ans; }
由下列程式碼使用
.Call("lapply2", a, sum, new.env())
函數 lang2
建立兩個元素的可執行配對清單,但這僅對了解類似 LISP 語言的人來說才清楚。
作為在 C 程式碼中建構 R 呼叫並評估的更全面範例,請考慮 src/main/print.c 中 printAttributes
的下列片段。
/* Need to construct a call to print(CAR(a), digits=digits) based on the R_print structure, then eval(call, env). See do_docall for the template for this sort of thing. */ SEXP s, t; t = s = PROTECT(allocList(3)); SET_TYPEOF(s, LANGSXP); SETCAR(t, install("print")); t = CDR(t); SETCAR(t, CAR(a)); t = CDR(t); SETCAR(t, ScalarInteger(digits)); SET_TAG(t, install("digits")); eval(s, env); UNPROTECT(1);
此時 CAR(a)
是要列印的 R 物件,即目前的屬性。有三個步驟:呼叫建構為長度為 3 的配對清單、填入清單,以及評估配對清單所代表的表達式。
配對清單與 R 中唯一的使用者可見清單形式,也就是一般向量清單,相當不同。配對清單是一種連結清單(CDR(t)
計算下一個項目),其項目(透過 CAR(t)
存取)和名稱或標籤(由 SET_TAG
設定)。在此呼叫中會有三個項目,一個符號(指向要呼叫的函式)和兩個引數值,第一個沒有命名,第二個有命名。將類型設定為 LANGSXP
會讓這個呼叫可以評估。
通常,評估環境會從呼叫 R 程式碼傳遞(請參閱上方的 rho
)。在特殊情況下,C 程式碼可能需要取得目前的評估環境,這可以用 R_GetCurrentEnv()
函式來完成。
在這個區段中,我們重新處理 Becker、Chambers 和 Wilks (1988, pp.~205–10) 關於尋找單變數函式零點的範例。R 程式碼和範例如下:
zero <- function(f, guesses, tol = 1e-7) { f.check <- function(x) { x <- f(x) if(!is.numeric(x)) stop("Need a numeric result") as.double(x) } .Call("zero", body(f.check), as.double(guesses), as.double(tol), new.env()) } cube1 <- function(x) (x^2 + 1) * (x - 1.5) zero(cube1, c(0, 5))
這次我們在 R 程式碼中進行強制轉換和錯誤檢查。C 程式碼如下:
SEXP mkans(double x) { // no need for PROTECT() here, as REAL(.) does not allocate: SEXP ans = allocVector(REALSXP, 1); REAL(ans)[0] = x; return ans; }
double feval(double x, SEXP f, SEXP rho) { // a version with (too) much PROTECT()ion .. "better safe than sorry" SEXP symbol, value; PROTECT(symbol = install("x")); PROTECT(value = mkans(x)); defineVar(symbol, value, rho); UNPROTECT(2); return(REAL(eval(f, rho))[0]); }
SEXP zero(SEXP f, SEXP guesses, SEXP stol, SEXP rho) { double x0 = REAL(guesses)[0], x1 = REAL(guesses)[1], tol = REAL(stol)[0]; double f0, f1, fc, xc;
if(tol <= 0.0) error("non-positive tol value"); f0 = feval(x0, f, rho); f1 = feval(x1, f, rho); if(f0 == 0.0) return mkans(x0); if(f1 == 0.0) return mkans(x1); if(f0*f1 > 0.0) error("x[0] and x[1] have the same sign");
for(;;) { xc = 0.5*(x0+x1); if(fabs(x0-x1) < tol) return mkans(xc); fc = feval(xc, f, rho); if(fc == 0) return mkans(xc); if(f0*fc > 0.0) { x0 = xc; f0 = fc; } else { x1 = xc; f1 = fc; } } }
我們將使用一個較長的範例(由 Saikat DebRoy 提供)來說明評估和 .External
的使用方式。這會計算數值導數,這可以用解釋的 R 程式碼有效地完成,但可能需要作為較大型 C 計算的一部分。
解釋的 R 版本和範例為
numeric.deriv <- function(expr, theta, rho=sys.frame(sys.parent())) { eps <- sqrt(.Machine$double.eps) ans <- eval(substitute(expr), rho) grad <- matrix(, length(ans), length(theta), dimnames=list(NULL, theta)) for (i in seq_along(theta)) { old <- get(theta[i], envir=rho) delta <- eps * max(1, abs(old)) assign(theta[i], old+delta, envir=rho) ans1 <- eval(substitute(expr), rho) assign(theta[i], old, envir=rho) grad[, i] <- (ans1 - ans)/delta } attr(ans, "gradient") <- grad ans } omega <- 1:5; x <- 1; y <- 2 numeric.deriv(sin(omega*x*y), c("x", "y"))
其中 expr
是表達式,theta
是變數名稱的字元向量,而 rho
是要使用的環境。
對於編譯版本,R 的呼叫會是
.External("numeric_deriv", expr, theta, rho)
範例用法為
.External("numeric_deriv", quote(sin(omega*x*y)), c("x", "y"), .GlobalEnv)
請注意需要引用表達式,以停止在呼叫方評估它。
以下是完整的 C 程式碼,我們將逐一說明。
#include <R.h> #include <Rinternals.h> #include <float.h> /* for DBL_EPSILON */ SEXP numeric_deriv(SEXP args) { SEXP theta, expr, rho, ans, ans1, gradient, par, dimnames; double tt, xx, delta, eps = sqrt(DBL_EPSILON), *rgr, *rans; int i, start;
expr = CADR(args); if(!isString(theta = CADDR(args))) error("theta should be of type character"); if(!isEnvironment(rho = CADDDR(args))) error("rho should be an environment");
ans = PROTECT(coerceVector(eval(expr, rho), REALSXP)); gradient = PROTECT(allocMatrix(REALSXP, LENGTH(ans), LENGTH(theta))); rgr = REAL(gradient); rans = REAL(ans);
for(i = 0, start = 0; i < LENGTH(theta); i++, start += LENGTH(ans)) { par = PROTECT(findVar(installChar(STRING_ELT(theta, i)), rho)); tt = REAL(par)[0]; xx = fabs(tt); delta = (xx < 1) ? eps : xx*eps; REAL(par)[0] += delta; ans1 = PROTECT(coerceVector(eval(expr, rho), REALSXP)); for(int j = 0; j < LENGTH(ans); j++) rgr[j + start] = (REAL(ans1)[j] - rans[j])/delta; REAL(par)[0] = tt; UNPROTECT(2); /* par, ans1 */ }
dimnames = PROTECT(allocVector(VECSXP, 2)); SET_VECTOR_ELT(dimnames, 1, theta); dimnamesgets(gradient, dimnames); setAttrib(ans, install("gradient"), gradient); UNPROTECT(3); /* ans gradient dimnames */ return ans; }
處理引數的程式碼為
expr = CADR(args); if(!isString(theta = CADDR(args))) error("theta should be of type character"); if(!isEnvironment(rho = CADDDR(args))) error("rho should be an environment");
請注意,我們會檢查 theta
和 rho
的正確型別,但不會檢查 expr
的型別。這是因為 eval
可以處理許多 R 物件型別,而這些型別不只是 EXPRSXP
。我們無法進行有用的強制轉換,因此如果引數不是正確的模式,我們會停止並顯示錯誤訊息。
程式碼中的第一步是在環境 rho
中評估表達式,方法為
ans = PROTECT(coerceVector(eval(expr, rho), REALSXP));
接著,我們透過以下方式為計算的導數配置空間
gradient = PROTECT(allocMatrix(REALSXP, LENGTH(ans), LENGTH(theta)));
傳遞給 allocMatrix
的第一個引數會提供矩陣的 SEXPTYPE
:在此我們希望它是 REALSXP
。其他兩個引數是列數和欄數。(請注意 LENGTH
打算用於向量:length
的適用性較廣泛。)
for(i = 0, start = 0; i < LENGTH(theta); i++, start += LENGTH(ans)) { par = PROTECT(findVar(installChar(STRING_ELT(theta, i)), rho));
在此,我們進入一個 for 迴圈。我們迴圈遍歷每個變數。在 for
迴圈中,我們首先建立一個符號對應於 STRSXP
theta
的第 i
個元素。在此,STRING_ELT(theta, i)
存取 STRSXP
theta
的第 i
個元素。巨集 CHAR()
萃取其實際的字元表示法159:它會傳回一個指標。然後我們安裝名稱並使用 findVar
來尋找其值。
tt = REAL(par)[0]; xx = fabs(tt); delta = (xx < 1) ? eps : xx*eps; REAL(par)[0] += delta; ans1 = PROTECT(coerceVector(eval(expr, rho), REALSXP));
我們首先萃取參數的實際值,然後計算 delta
,這是用於逼近數值導數的增量。然後我們將儲存在 par
(在環境 rho
中)中的值變更為 delta
,並再次在環境 rho
中評估 expr
。由於我們在此直接處理原始 R 記憶體位置,因此 R 會針對已變更的參數值進行評估。
for(int j = 0; j < LENGTH(ans); j++) rgr[j + start] = (REAL(ans1)[j] - rans[j])/delta; REAL(par)[0] = tt; UNPROTECT(2); }
現在,我們計算梯度矩陣的第 i
個欄。請注意其存取方式:R 會按欄儲存矩陣(類似 Fortran)。
dimnames = PROTECT(allocVector(VECSXP, 2)); SET_VECTOR_ELT(dimnames, 1, theta); dimnamesgets(gradient, dimnames); setAttrib(ans, install("gradient"), gradient); UNPROTECT(3); return ans; }
首先我們將欄名稱新增至梯度矩陣。這是透過配置一個清單(一個 VECSXP
)來完成的,其第一個元素(即列名稱)為 NULL
(預設值),而第二個元素(即欄名稱)則設定為 theta
。然後將此清單指定為具有符號 R_DimNamesSymbol
的屬性。最後我們將梯度矩陣設定為 ans
的梯度屬性,取消保護其餘受保護的位置,並傳回答案 ans
。
假設 R 擴充套件想要從使用者那裡接受一個 R 表達式並對其進行評估。前一節介紹了評估,但表達式將作為文字輸入,並且需要首先進行解析。R 的解析介面的很小一部分在標頭檔案 R_ext/Parse.h160 中宣告。
可以在(範例)Windows 套件 windlgs 中找到使用範例,該套件包含在 R 原始碼樹中。必要的部份是
#include <R.h> #include <Rinternals.h> #include <R_ext/Parse.h> SEXP menu_ttest3() { char cmd[256]; SEXP cmdSexp, cmdexpr, ans = R_NilValue; ParseStatus status; ... if(done == 1) { cmdSexp = PROTECT(allocVector(STRSXP, 1)); SET_STRING_ELT(cmdSexp, 0, mkChar(cmd)); cmdexpr = PROTECT(R_ParseVector(cmdSexp, -1, &status, R_NilValue)); if (status != PARSE_OK) { UNPROTECT(2); error("invalid call %s", cmd); } /* Loop is needed here as EXPSEXP will be of length > 1 */ for(int i = 0; i < length(cmdexpr); i++) ans = eval(VECTOR_ELT(cmdexpr, i), R_GlobalEnv); UNPROTECT(2); } return ans; }
請注意,單一行文字可能會產生多個 R 表達式。
R_ParseVector
本質上是 parse(text=)
在 R 層級實作時使用的程式碼。第一個參數是字元向量(對應到 text
),第二個參數是要剖析的最大表達式數量(對應到 n
)。第三個參數是指向列舉型變數的指標,而且通常(就像 parse
所做的那樣)將 PARSE_OK
以外的所有值視為錯誤。其他可能傳回的值包括 PARSE_INCOMPLETE
(找到不完整的表達式)和 PARSE_ERROR
(語法錯誤),這兩種情況傳回的值都是 R_NilValue
。第四個參數是長度為 1 的字元向量,用於在錯誤訊息中當作檔案名稱,一個 srcfile
物件或 R NULL
物件(如上方的範例)。如果使用 srcfile
物件,會將 srcref
屬性附加到結果,其中包含長度與表達式相同的 srcref
物件清單,以便讓它能以原始格式顯示。
解析器新增的來源參照會由 R 的評估器在評估程式碼時記錄下來。兩個函數讓這些參照可供執行 C 程式碼的偵錯器使用:
SEXP R_GetCurrentSrcref(int skip);
此函數會檢查 R_Srcref
和目前的評估堆疊,以找出包含來源參照資訊的項目。 skip
參數會指出在傳回 srcref
物件的 SEXP
之前要略過多少來源參照,從堆疊頂端開始算起。如果 skip < 0
,則會從堆疊底部算起 abs(skip)
個位置。如果找不到來源參照,或來源參照太少,則會傳回 NULL
。
SEXP R_GetSrcFilename(SEXP srcref);
此函數會從來源參照中萃取檔案名稱以顯示,傳回一個長度為 1 的字元向量,其中包含檔案名稱。如果找不到名稱,則會傳回 ""
。
SEXPTYPE
的 EXTPTRSXP
和 WEAKREFSXP
可以在 R 層級中遇到,但會在 C 程式碼中建立。
外部指標 SEXP
用於處理對 C 結構的參照,例如「控制代碼」,並用於套件 RODBC 中。它們在複製語意上很特別,因為當複製 R 物件時,不會複製外部指標物件。(基於這個原因,外部指標只能用於具有正常語意的物件中,例如屬性或清單元素。)
外部指標是由
SEXP R_MakeExternalPtr(void *p, SEXP tag, SEXP prot);
建立,其中 p
是指標(因此這無法移植為函式指標),而 tag
和 prot
是對一般 R 物件的參照,這些物件會在外部指標物件的生命週期內持續存在(免於垃圾回收)。一個有用的慣例是將 tag
欄位用於某種形式的類型識別,而將 prot
欄位用於保護外部指標所代表的記憶體,如果該記憶體是由 R 堆疊配置的。 tag
和 prot
都可以是 R_NilValue
,而且通常都是。
從函式指標建立外部指標的另一種方法是
typedef void * (*R_DL_FUNC)(); SEXP R_MakeExternalPtrFn(R_DL_FUNC p, SEXP tag, SEXP prot);
外部指標的元素可透過經由
void *R_ExternalPtrAddr(SEXP s); DL_FUNC R_ExternalPtrAddrFn(SEXP s); SEXP R_ExternalPtrTag(SEXP s); SEXP R_ExternalPtrProtected(SEXP s); void R_ClearExternalPtr(SEXP s); void R_SetExternalPtrAddr(SEXP s, void *p); void R_SetExternalPtrTag(SEXP s, SEXP tag); void R_SetExternalPtrProtected(SEXP s, SEXP p);
清除指標會將其值設定為 C NULL
指標。
外部指標物件可以有一個完成函式,這是一段程式碼,會在物件被垃圾回收時執行。這可以是 R 程式碼或 C 程式碼,而各種介面分別為。
void R_RegisterFinalizer(SEXP s, SEXP fun); void R_RegisterFinalizerEx(SEXP s, SEXP fun, Rboolean onexit); typedef void (*R_CFinalizer_t)(SEXP); void R_RegisterCFinalizer(SEXP s, R_CFinalizer_t fun); void R_RegisterCFinalizerEx(SEXP s, R_CFinalizer_t fun, Rboolean onexit);
fun
所指示的 R 函式應該是單一引數的函式,也就是要完成的物件。R 在關閉時不會執行垃圾回收,而延伸形式的 onexit
引數可用於要求在 R 工作階段正常關閉時執行完成函式。建議在完成時清除指標是一種良好的做法。
與外部指標互動的唯一 R 層級函式是 reg.finalizer
,可用於設定完成函式。
允許外部指標被save
然後重新載入可能不是個好主意,但如果這樣做,指標將會被設定為 C NULL
指標。
終結函式可以在程式碼庫的許多地方執行,包括 R 解譯器在內的許多部分都不是可重入的。因此在選擇要在終結函式中執行的程式碼時需要非常小心。終結函式標記為在垃圾回收時執行,但只在之後的某個較為安全的時間點執行。
弱參考用於允許程式設計人員維護實體的資訊,同時不阻止實體在無法存取後進行垃圾回收。
弱參考包含一個鍵和一個值。如果值可以透過可存取的鍵直接存取或透過弱參考存取,則該值可以存取。一旦在垃圾回收期間確定某個值無法存取,鍵和值就會設定為R_NilValue
,終結函式將會在垃圾回收的稍後時間執行。
弱參考物件是由下列其中一個建立的
SEXP R_MakeWeakRef(SEXP key, SEXP val, SEXP fin, Rboolean onexit); SEXP R_MakeWeakRefC(SEXP key, SEXP val, R_CFinalizer_t fin, Rboolean onexit);
其中 R 或 C 終結函式的指定方式與外部指標物件(其終結介面是透過弱參考實作的)完全相同。
這些部分可以透過
SEXP R_WeakRefKey(SEXP w); SEXP R_WeakRefValue(SEXP w); void R_RunWeakRefFinalizer(SEXP w);
弱參照的玩具範例可以在 https://homepage.stat.uiowa.edu/~luke/R/references/weakfinex.html 找到,但那是用來為外部指標新增完成處理程序,現在可以更直接地執行。在撰寫本文時,沒有 CRAN 或 Bioconductor 套件使用弱參照。
套件 RODBC 使用外部指標來維護其通道,即與資料庫的連線。可以同時開啟多個連線,而每個連線的狀態資訊都儲存在 C 結構中(由下方程式碼摘錄中的 thisHandle
指向),並透過外部指標作為 RODBC「通道」的一部分(作為 "handle_ptr"
屬性)傳回。外部指標是由下列程式碼建立的:
SEXP ans, ptr; ans = PROTECT(allocVector(INTSXP, 1)); ptr = R_MakeExternalPtr(thisHandle, install("RODBC_channel"), R_NilValue); PROTECT(ptr); R_RegisterCFinalizerEx(ptr, chanFinalizer, TRUE); ... /* return the channel no */ INTEGER(ans)[0] = nChannels; /* and the connection string as an attribute */ setAttrib(ans, install("connection.string"), constr); setAttrib(ans, install("handle_ptr"), ptr); UNPROTECT(3); return ans;
請注意用來識別外部指標用法的符號,以及完成處理程序的使用方式。由於註冊完成處理程序時的最後一個引數是 TRUE
,因此完成處理程序會在 R 工作階段結束時執行(除非它發生異常終止)。這用於關閉並清除與資料庫的連線。完成處理程序的程式碼很簡單:
static void chanFinalizer(SEXP ptr) { if(!R_ExternalPtrAddr(ptr)) return; inRODBCClose(R_ExternalPtrAddr(ptr)); R_ClearExternalPtr(ptr); /* not really needed */ }
清除指標並檢查 NULL
指標,可以避免嘗試關閉已關閉的通道。
R 的連線提供了另一個使用外部指標的範例,在這種情況下純粹是為了能夠使用完成處理程序來關閉和銷毀不再使用的連線。
當在 R 擴充套件中使用時,像 REAL
、INTEGER
、LOGICAL
、RAW
、COMPLEX
和 VECTOR_ELT
這樣的向量存取器是函式。(為了效率,當在 R 原始碼中使用時,除了始終是函式的 SET_STRING_ELT
和 SET_VECTOR_ELT
之外,它們可能是巨集或內嵌函式。)
存取器函式會檢查它們是否用於適當類型的 SEXP
。
以前,套件可以在包含 Rinternals.h 之前定義「USE_RINTERNALS」,以取得某些存取器的內部版本。現在不再是這樣。定義「USE_RINTERNALS」現在沒有作用。
CHARSXP
可標示為來自已知編碼(Latin-1 或 UTF-8)。這主要用於人類可讀的輸出,而大多數套件可以將此類 CHARSXP
視為一個整體。但是,如果需要將其解釋為字元或在 C 層級輸出,通常正確的做法是確保將其轉換為目前區域設定的編碼:這可透過 translateChar
而不是 CHAR
存取 CHARSXP
中的資料來完成。如果需要重新編碼,這會使用 R_alloc
配置記憶體,因此會持續到 .Call
/.External
呼叫結束,除非使用 vmaxset
(請參閱 暫時儲存空間配置)。
有一個類似的函式 translateCharUTF8
可轉換為 UTF-8:這有以下優點:幾乎總是能忠實地轉換(而只有少數語言可以用目前區域設定的編碼表示,除非那是 UTF-8)。
translateChar
和 translateCharUTF8
都會轉換任何輸入,使用 ' <A9> ' 和 ' <U+0093> ' 等跳脫字元來表示輸入中無法轉換的部分。
有一個公開介面可透過 via
typedef enum {CE_NATIVE, CE_UTF8, CE_LATIN1, CE_BYTES, CE_SYMBOL, CE_ANY} cetype_t; cetype_t getCharCE(SEXP); SEXP mkCharCE(const char *, cetype_t);
僅 CE_UTF8
和 CE_LATIN1
會標示在 CHARSXPs
上(因此 Rf_getCharCE
僅會傳回前三個之一),且這些僅應使用於非 ASCII 字串。值 CE_BYTES
用於建立 CHARSXP
,應視為一組位元組,且不翻譯。值 CE_SYMBOL
內部用於指示 Adobe Symbol 編碼。值 CE_ANY
用於指示不需重新編碼的字元字串,這是用於已知為 ASCII 的字元字串,且也可作為輸入參數,其中意圖是將字串視為一連串位元組。(請參閱 mkChar
下的註解,了解允許輸入的長度。)
函數
const char *reEnc(const char *x, cetype_t ce_in, cetype_t ce_out, int subst);
可用於重新編碼字元字串:類似 translateChar
,它會傳回由 R_alloc
分配的字串。這可以從 CE_SYMBOL
翻譯成 CE_UTF8
,但反之則不行。引數 subst
控制如何處理無法翻譯的字元或無效輸入:這會逐位元組執行,1
表示以 <a0>
形式輸出十六進位,2
表示以 .
取代,任何其他值都會導致位元組不產生輸出。
還有
SEXP mkCharLenCE(const char *, size_t, cetype_t);
用於建立特定長度的標記字元字串。
R 可執行檔/DLL 中有許多進入點,可從 C 程式碼(以及一些可從 Fortran 程式碼)呼叫。只有在此處記錄的文件才夠穩定,僅會在有相當的通知後才會變更。
建議使用這些進入點的方法是在 C 程式碼中包含標頭檔 R.h,方法如下:
#include <R.h>
這將包含目錄 R_INCLUDE_DIR/R_ext 中的其他幾個標頭檔,而且還有其他標頭檔也可以包含,但它們所包含的許多功能應視為未記錄且不穩定。
這些標頭檔中的大多數,包括 R.h 所包含的所有標頭檔,都可以從 C++ 程式碼使用。
注意:由於 R 重新對應許多外部名稱以避免與系統或使用者程式碼衝突,因此在使用這些進入點時,包含適當的標頭檔是必要的。
此重新對應可能會造成問題161,而且可以透過定義 R_NO_REMAP
(在包含任何 R 標頭之前)並在從 Rinternals.h 和 R_ext/Error.h 使用的所有函數名稱之前加上「Rf_」來消除。通常可以透過在任何 R 標頭之前包含其他標頭(例如系統標頭和套件所使用的外部軟體標頭)來避免這些問題。(來自其他套件的標頭可能會直接包含 R 標頭或透過從其他套件包含的方式,而且可能會定義 R_NO_REMAP
,而不管是否包含 Rinternals.h。)
我們可以將進入點分類為
在此手冊中記錄且在已安裝標頭檔中宣告的進入點。這些進入點可以在已散發的套件中使用,而且僅會在不建議使用後才會變更。
在已安裝的標頭檔中宣告的進入點,在所有 R 平台上都已匯出,但沒有文件記載,且如有變更,恕不另行通知。
在建置 R 時使用的進入點,在所有 R 平台上都已匯出,但未在已安裝的標頭檔中宣告。請勿在已散發的程式碼中使用這些進入點。
在可能的情況下(Windows 和一些現代類 Unix 編譯器/載入器,在使用 R 作為共用程式庫時)不會匯出的進入點。
C 程式設計人員可以使用兩種記憶體配置,一種由 R 管理清理,另一種由使用者完全控制(並負責)。
這些函數在標頭 R_ext/RS.h 中宣告,此標頭由 R.h 包含。
在呼叫 .C
、.Call
或 .External
的結尾,R 將會回收記憶體。使用
char *R_alloc(size_t n, int size)
配置 n 個單位,每個單位 size 位元組。典型的用法(來自套件 stats)為
x = (int *) R_alloc(nrows(merge)+2, sizeof(int));
(size_t
定義在 stddef.h 中,這是定義 R_alloc
的標頭檔。)
有一個類似的呼叫 S_alloc
(命名為與舊版 S 相容),它會將配置的記憶體歸零,
char *S_alloc(long n, int size)
和
char *S_realloc(char *p, long new, long old, int size)
(對於 new > old
)將配置大小從 old 變更為 new 個單位,並將額外的單位歸零。注意:最好避免這些呼叫,因為在 64 位元的 Windows 上,long
不足以進行大型記憶體配置(在那裡它限制在 2^31-1 位元組)。
這個記憶體從堆疊中取得,並在 .C
、.Call
或 .External
呼叫的結尾釋放。使用者也可以透過呼叫 vmaxget
來記錄目前的位址,然後清除由呼叫 vmaxset
配置的記憶體來管理它。一個範例可能是
void *vmax = vmaxget() // a loop involving the use of R_alloc at each iteration vmaxset(vmax)
這僅建議給專家使用。
請注意,此記憶體會在錯誤或使用者中斷時釋放(如果允許:請參閱允許中斷)。
保證傳回的記憶體僅與double
指標所需的對齊方式相同:如果轉換為需要更多對齊方式的指標,請採取預防措施。另外還有
long double *R_allocLD(size_t n)
保證在某些平台上具有long double
指標所需的 16 位元組對齊方式。
這些函數僅應在由.C
等呼叫的程式碼中使用,絕不應從前端呼叫。它們不是執行緒安全的。
另一種形式的記憶體配置是malloc
介面,此介面提供 R 錯誤訊號。此記憶體會持續存在,直到使用者釋放為止,而且是額外配置給 R 工作區的記憶體。
介面巨集為
type* R_Calloc(size_t n, type) type* R_Realloc(any *p, size_t n, type) void R_Free(any *p)
提供calloc
、realloc
和free
的類比。如果配置期間發生錯誤,則由 R 處理,因此如果這些傳回,表示記憶體已成功配置或釋放。R_Free
會將指標p設定為NULL
。(S 的某些版本會這樣做,但並非全部版本。)
使用者應安排在不再需要時R_Free
此記憶體,包括在錯誤或使用者中斷時。通常最方便的方式是從呼叫 R 函數的on.exit
動作執行此動作,請參閱pwilcox
以取得範例。
請勿假設由R_Calloc
/R_Realloc
配置的記憶體來自與malloc
使用的相同池:162特別是請勿對其使用free
或strdup
。
透過這些巨集取得的記憶體應與 malloc
對齊方式相同,亦即「適合任何類型的變數對齊」。
在歷史上,巨集 Calloc
、Free
和 Realloc
已被使用,且這些巨集仍可用,除非在包含標頭之前已定義 STRICT_R_HEADERS
。
char * CallocCharBuf(size_t n) void * Memcpy(q, p, n) void * Memzero(p, n)
CallocCharBuf(n)
是 R_Calloc(n+1, char)
的簡寫,可允許 nul
終止符。 Memcpy
和 Memzero
會從陣列 p
中取得 n
個項目,並將它們複製到陣列 q
或將它們設為零。
基本錯誤訊號常式是 R 程式碼中 stop
和 warning
的等效常式,且使用相同的介面。
void error(const char * format, ...); void warning(const char * format, ...); void errorcall(SEXP call, const char * format, ...); void warningcall(SEXP call, const char * format, ...); void warningcall_immediate(SEXP call, const char * format, ...);
這些常式的呼叫順序與呼叫 printf
的順序相同,但在最簡單的情況下,可以使用單一字元字串引數呼叫,提供錯誤訊息。(如果字串包含「%」或可能被解釋為格式,請勿執行此操作。)
這些常式定義在標頭 R_ext/Error.h 中,由 R.h 包含。
提供兩個介面函數,以從 Fortran 程式碼呼叫 error
和 warning
,每個函數都有一個簡單的字元串引數。它們定義如下
subroutine rexit(message) subroutine rwarn(message)
超過 255 個字元的訊息會被截斷,並顯示警告。
R 內部亂數產生常式的介面為
double unif_rand(); double norm_rand(); double exp_rand(); double R_unif_index(double);
提供一個均勻、常態或指數的偽亂數變異數。不過,在使用這些變異數之前,使用者必須呼叫
GetRNGstate();
在產生所有必要的變異數後,呼叫
PutRNGstate();
這些常式基本上會讀取(或建立).Random.seed
,並在使用後寫出。
這些常式定義在標頭 R_ext/Random.h 中。
亂數產生器是 R 的私有功能;除了評估對 R 函數的呼叫之外,沒有辦法選擇 RNG 類型或設定種子。
R 的 rxxx
函數背後的 C 程式碼可以透過包含標頭檔 Rmath.h 來存取;請參閱 分配函數。這些呼叫也應該在呼叫 GetRNGstate
和 PutRNGstate
之前和之後。
前面已說明,Fortran 亂數產生器不應使用在 R 套件中,最主要的原因是套件無法安全地初始化它們。相反地,套件應呼叫 R 內建的產生器:一種方法是使用 C 封裝函式,例如
#include <R_ext/RS.h> #include <R_ext/Random.h> void F77_SUB(getRNGseed)(void) { GetRNGstate(); } void F77_SUB(putRNGseed)(void) { PutRNGstate(); } double F77_SUB(unifRand)(void) { return(unif_rand()); }
從 Fortran 程式碼呼叫,如下所示
... double precision X call getRNGseed() X = unifRand() ... call putRNGseed()
或者,也可以使用 Fortran 2003 的 iso_c_binding
模組,如下所示(固定格式 Fortran 90 程式碼)
module rngfuncs use iso_c_binding interface double precision * function unifRand() bind(C, name = "unif_rand") end function unifRand subroutine getRNGseed() bind(C, name = "GetRNGstate") end subroutine getRNGseed subroutine putRNGseed() bind(C, name = "PutRNGstate") end subroutine putRNGseed end interface end module subroutine testit use rngfuncs double precision X call getRNGseed() X = unifRand() print *, X call putRNGSeed() end
提供一組函式來測試 NA
、Inf
、-Inf
和 NaN
。這些函式可透過巨集存取
ISNA(x) True for R’sNA
only ISNAN(x) True for R’sNA
and IEEENaN
R_FINITE(x) False forInf
,-Inf
,NA
,NaN
以及透過函式 R_IsNaN
,對 NaN
為真,但對 NA
為假。
請使用 R_FINITE
,而不是 isfinite
或 finite
;後者經常會失真,而且 isfinite
僅在某些平臺上可用,而在這些平臺上 R_FINITE
是擴充為 isfinite
的巨集。
目前在 C 程式碼中,ISNAN
是呼叫 isnan
的巨集。(由於這會在某些 C++ 系統上產生問題,如果 R 標頭是由 C++ 程式碼呼叫的,則會使用函式呼叫。)
你可以透過測試是否等於 R_PosInf
或 R_NegInf
來檢查 Inf
或 -Inf
,並將 NA
設定(但不測試)為 NA_REAL
。
以上所有內容僅適用於雙精度變數。對於整數變數,有一個可透過巨集 NA_INTEGER
存取的變數,可用於設定或測試遺失值。
這些定義在標頭 R_ext/Arith.h 中,由 R.h 包含。
從編譯到 R 的 C 常式列印最實用的函式是 Rprintf
。這與 printf
的使用方式完全相同,但保證會寫入 R 的輸出(可能是 GUI 主控台,而不是檔案,且可以透過 sink
重新導向)。在返回 R 之前,建議寫入完整的行(包括 "\n"
)。它定義在 R_ext/Print.h 中。
函式 REprintf
類似,但會寫入錯誤串流(stderr
),這可能與標準輸出串流不同,也可能相同。
函數 Rvprintf
和 REvprintf
是使用 vprintf
介面的類比。由於那是 C99163 介面,它們僅由 R_ext/Print.h 在 C++ 程式碼中定義,前提是在包含它之前定義巨集 R_USE_C99_IN_CXX
,或(從 R 4.0.0 開始)使用 C++11 編譯器。
另一個可能需要使用這些函數的情況是在計算節點叢集上使用平行運算時,因為它們的輸出會適當地重新導向/記錄。
在許多系統上,Fortran write
和 print
陳述式可以使用,但輸出可能無法與 C 的輸出交錯,而且在 GUI 介面上可能不可見。它們不可移植,最好避免使用。
提供了一些子常式,以簡化從 Fortran 程式碼輸出的資訊。
subroutine dblepr(label, nchar, data, ndata) subroutine realpr(label, nchar, data, ndata) subroutine intpr (label, nchar, data, ndata)
subroutine labelpr(label, nchar) subroutine dblepr1(label, nchar, var) subroutine realpr1(label, nchar, var) subroutine intpr1 (label, nchar, var)
在此處,標籤 是最多 255 個字元的字元標籤,nchar 是其長度(如果要使用整個標籤,則可以是 -1
),資料 是長度至少為 ndata 的適當類型陣列(分別為 雙精度
、實數
和 整數
),而 var 是(純量)變數。這些常式在第一行列印標籤,然後在後續行中列印 資料 或 var,就像它是 R 向量一樣。請注意,除非 資料 是陣列,否則有些編譯器會產生錯誤或警告:當 ndata 的值為一或零時,其他編譯器會接受純量。注意:不會檢查 資料 或 var 的類型,因此使用 實數
(包括實數常數)而不是 雙精度
會產生不正確的答案。
intpr
使用零 ndata 運作,因此可用於在較早版本的 R 中列印標籤。
Fortran 產生的符號命名慣例因平台而異:假設 Fortran 名稱會以尾隨底線顯示在 C 中並不安全。為了協助隱藏平台特定的差異,有一組巨集164,應該使用這些巨集。
F77_SUB(名稱)
在 C 中定義一個函數,以便從 Fortran 呼叫
F77_NAME(name)
在使用前在 C 中宣告 Fortran 常式
F77_CALL(name)
從 C 呼叫 Fortran 常式
F77_COMDECL(name)
在 C 中宣告 Fortran 共用區塊
F77_COM(name)
從 C 存取 Fortran 共用區塊
在大部分目前的平台上,這些都是相同的,但依賴這一點並不明智。請注意,在 Fortran 77 中,包含底線的名稱是不合法的,而且上述巨集不會以可攜式方式處理這些名稱。(此外,所有供 R 使用的 Fortran 名稱都是小寫,但巨集不會強制執行這一點。)
例如,假設我們要從 Fortran 呼叫 R 的常態亂數。我們需要一個類似以下的 C 封裝函數
#include <R.h> void F77_SUB(rndstart)(void) { GetRNGstate(); } void F77_SUB(rndend)(void) { PutRNGstate(); } double F77_SUB(normrnd)(void) { return norm_rand(); }
以便在 Fortran 中呼叫,如下所示
subroutine testit() double precision normrnd, x call rndstart() x = normrnd() call dblepr("X was", 5, x, 1) call rndend() end
請注意,這無法保證可攜性,因為回傳慣例可能與所使用的 C 和 Fortran 編譯器不相容。(透過 參數傳遞值較為安全。)
標準套件,例如 stats,是進一步範例的豐富來源。
在受支援的情況下,連結時間最佳化 提供一種可靠的方式來檢查從 Fortran 到 C 或 反之亦然 的呼叫的一致性。請參閱 使用連結時間最佳化。這會發生的一個地方是在 C 程式碼中註冊 .Fortran
呼叫(請參閱 註冊原生常式)。例如
init.c:10:13: warning: type of ‘vsom_’ does not match original declaration [-Wlto-type-mismatch] extern void F77_NAME(vsom)(void *, void *, void *, void *, void *, void *, void *, void *, void *); vsom.f90:20:33: note: type mismatch in parameter 9 subroutine vsom(neurons,dt,dtrows,dtcols,xdim,ydim,alpha,train) vsom.f90:20:33: note: ‘vsom’ was previously declared here
顯示已註冊一個具有 9 個參數的子常式(因為這是 .Fortran
呼叫所使用的),但實際上只有 8 個。
傳遞字元串從 C 到 Fortran 或反之亦然並非可攜式的,但可以小心執行。內部表示方式不同:C (或 C++) 中的字元陣列以 null 結束,因此其長度可透過 strlen
計算。Fortran 字元陣列通常儲存為位元組陣列和長度。當傳遞字元串從 C 到 Fortran 或反之亦然時,這很重要:在許多情況下,人們可以傳遞字元串,但無法傳遞長度。然而,在 2019 年,這對 gfortran
發生了變化,從版本 9 開始,但回溯移植到版本 7 和 8。幾個月後,gfortran
9.2 引入了選項
-ftail-call-workaround
並使其成為目前的預設值,但表示未來可能會取消。
假設我們想要一個函式來報告訊息從 Fortran 到 R 的主控台 (可以使用 labelpr
,或 intpr
搭配虛擬資料,但這可能是自訂報告函式的基礎)。假設 Fortran 中的等效函式為
subroutine rmsg(msg) character*(*) msg print *.msg end
在檔案 rmsg.f 中。使用 gfortran
9.2 及更新版本,我們可以透過以下方式擷取 C 檢視:
gfortran -c -fc-prototypes-external rmsg.f
將提供
void rmsg_ (char *msg, size_t msg_len);
(其中 size_t
適用於版本 8 和更高版本)。我們可以用 C 語言以可移植的方式改寫為
#ifndef USE_FC_LEN_T # define USE_FC_LEN_T #endif #include <Rconfig.h> // included by R.h, so define USE_FC_LEN_T early void F77_NAME(rmsg)(char *msg, FC_LEN_T msg_len) { char cmsg[msg_len+1]; strncpy(cmsg, msg, msg_len); cmsg[msg_len] = ‘\0’; // nul-terminate the string, to be sure // do something with ‘cmsg’ }
在依賴於 R(>= 3.6.2)
的程式碼中。對於較早版本的 R,我們可以假設 msg
以空值終止(無法保證,但多年來人們一直這樣做),因此完整的 C 端可能是
#ifndef USE_FC_LEN_T # define USE_FC_LEN_T #endif #include <Rconfig.h> #ifdef FC_LEN_T void F77_NAME(rmsg)(char *msg, FC_LEN_T msg_len) { char cmsg[msg_len+1]; strncpy(cmsg, msg, msg_len); cmsg[msg_len] = ‘\0’; // do something with ‘cmsg’ } #else void F77_NAME(rmsg)(char *msg) { // do something with ‘msg’ } #endif
(USE_FC_LEN_T
是從 R 4.3.0 開始的預設值。)
另一種方法是使用 Fortran 2003 功能設定 Fortran 常式以傳遞與 C 相容的字元字串。我們可以使用類似
module cfuncs use iso_c_binding, only: c_char, c_null_char interface subroutine cmsg(msg) bind(C, name = ‘cmsg’) use iso_c_binding, only: c_char character(kind = c_char):: msg(*) end subroutine cmsg end interface end module subroutine rmsg(msg) use cfuncs character(*) msg call cmsg(msg//c_null_char) ! need to concatenate a nul terminator end subroutine rmsg
其中 C 端只是
void cmsg(const char *msg) { // do something with nul-terminated string ‘msg’ }
如果您在此處將 bind
與 C 函數結合使用,檢查綁定定義是否正確的唯一方法是使用 LTO 編譯套件(這需要相容的 C 和 Fortran 編譯器,通常是 gcc
和 gfortran
)。
將變長字串從 C 傳遞到 Fortran 較為棘手,但 https://www.intel.com/content/www/us/en/docs/fortran-compiler/developer-guide-reference/2023-0/bind-c.html 提供了一個範例。不過,BLAS 和 LAPACK 中的所有用法都是單一字元,對於這些用法,我們可以沿著以下方向在 Fortran 中撰寫一個包裝函式
subroutine c_dgemm(transa, transb, m, n, k, alpha, + a, lda, b, ldb, beta, c, ldc) + bind(C, name = ‘Cdgemm’) use iso_c_binding, only : c_char, c_int, c_double character(c_char), intent(in) :: transa, transb integer(c_int), intent(in) :: m, n, k, lda, ldb, ldc real(c_double), intent(in) :: alpha, beta, a(lda, *), b(ldb, *) real(c_double), intent(out) :: c(ldc, *) call dgemm(transa, transb, m, n, k, alpha, + a, lda, b, ldb, beta, c, ldc) end subroutine c_dgemm
然後使用宣告從 C 呼叫
void Cdgemm(const char *transa, const char *transb, const int *m, const int *n, const int *k, const double *alpha, const double *a, const int *lda, const double *b, const int *ldb, const double *beta, double *c, const int *ldc);
或者,從版本 3.6.2 開始,執行 R 所做的動作,並將字元長度從 C 傳遞到 Fortran。這樣做的一個可移植方法是
// before any R headers, or define in PKG_CPPFLAGS #ifndef USE_FC_LEN_T # define USE_FC_LEN_T #endif #include <Rconfig.h> #include <R_ext/BLAS.h> #ifndef FCONE # define FCONE #endif ... F77_CALL(dgemm)("N", "T", &nrx, &ncy, &ncx, &one, x, &nrx, y, &nry, &zero, z, &nrx FCONE FCONE);
(注意,在 FCONE
呼叫之前或之間沒有逗號。)強烈建議呼叫具有字元引數的 C/C++ BLAS/LAPACK 常式的套件採用這種方法:從 R 4.3.0 開始,未使用的套件將無法安裝。
傳遞 Fortran LOGICAL 變數至/自 C/C++,可能會依編譯器而定。Fortran 編譯器長期以來使用 32 位元整數型別,因此在 C/C++ 端使用 int *
具有相當高的可移植性。然而,最近版本的 gfortran
透過 選項 -fc-prototypes-external 指出 C 等效項為 int_least32_t *
:『連結時間最佳化』會回報 int *
為不符。可以在 Fortran 2003 中使用 iso_c_binding
將 LOGICAL 變數對應至 C99 型別 _Bool
,但通常傳遞整數會比較簡單。
許多套件會呼叫傳遞至 Fortran 程式碼的 C 函式,類似下列範例
c subroutine fcn(m,n,x,fvec,iflag) c integer m,n,iflag c double precision x(n),fvec(m) ... subroutine lmdif(fcn, ...
其中 C 宣告和呼叫為
void fcn_lmdif(int *m, int *n, double *par, double *fvec, int *iflag); void F77_NAME(lmdif)(void (*fcn_lmdif)(int *m, int *n, double *par, double *fvec, int *iflag), ... F77_CALL(lmdif)(&fcn_lmdif, ...
這在大部分平台上都能運作,但依賴於 C 和 Fortran 編譯器在呼叫慣例上達成共識:這已被發現會失敗。最具可移植性的解決方案似乎是將 Fortran 程式碼轉換為 C,或許使用 f2c
。
R 包含大量供其自身使用的數學函數,例如數值線性代數運算和特殊函數。
標頭檔 R_ext/BLAS.h、R_ext/Lapack.h 和 R_ext/Linpack.h 包含 R 中包含的 BLAS、LAPACK 和 LINPACK 線性代數函數的宣告。這些函數表示為對 Fortran 子常式的呼叫,使用者也可以從 Fortran 程式碼使用這些函數。儘管這組子常式並非官方 API 的一部分,但不太可能變更(但可能會補充)。
標頭檔 Rmath.h 列出許多其他函數,這些函數在以下小節中提供說明和文件。其中許多函數是 R 函數背後程式碼的 C 介面,因此 R 函數文件可能會提供更多詳細資訊。
用於計算標準統計分配的密度、累積分配函數和分位數函數的常式可用作進入點。
進入點的引數遵循常態分配的引數模式
double dnorm(double x, double mu, double sigma, int give_log); double pnorm(double x, double mu, double sigma, int lower_tail, int give_log); double qnorm(double p, double mu, double sigma, int lower_tail, int log_p); double rnorm(double mu, double sigma);
亦即,第一個參數提供密度和 CDF 的位置,以及分位數函數的機率,接著是分配的參數。參數 lower_tail 應為 TRUE
(或 1
)以正常使用,但如果需要或指定上尾機率,則可以為 FALSE
(或 0
)。
最後,如果需要對數尺度的結果,則 give_log 應為非零,如果 p 已在對數尺度中指定,則 log_p 應為非零。
請注意,您可以使用下列方式直接取得累積(或「整合」)風險函數 H(t) = - log(1 - F(t)):
- pdist(t, ..., /*lower_tail = */ FALSE, /* give_log = */ TRUE)
或較簡短(且較難懂)的 - pdist(t, ..., 0, 1)
。
隨機變異產生常式 rnorm
會傳回一個常態變異。請參閱 亂數產生,以了解使用隨機變異常式的協定。
請注意,這些參數順序(除了名稱和 rnorm
沒有 n)基本上與相同名稱的 R 函數相同,因此可以使用 R 函數的文件。請注意,指數和伽瑪分配是由 scale
而不是 rate
參數化的。
供參考,下表提供基本名稱(除了註明的例外,前面要加上「d」、「p」、「q」或「r」)和完整分配集的分配特定參數。
貝他 貝他
a
、b
非中心貝他 nbeta
a
、b
、ncp
二項式 二項分布
n
,p
柯西分布 cauchy
location
,scale
卡方分布 chisq
df
非中心卡方分布 nchisq
df
,ncp
指數分布 exp
scale
(而非rate
)F 分布 f
n1
,n2
非中心 F 分布 nf
n1
,n2
,ncp
伽馬分布 伽馬分布
shape
,scale
幾何分布 geom
p
超幾何分布 hyper
NR
,NB
,n
邏輯分布 logis
location
,scale
對數常態分布 lnorm
logmean
,logsd
負二項分布 nbinom
size
,prob
常態分布 norm
mu
,sigma
泊松分布 pois
lambda
學生 t 分布 t
n
非中心 t 分布 nt
df
,delta
學生化範圍 tukey
(*)rr
,cc
,df
均勻分布 unif
a
、b
威布爾分布 weibull
shape
,scale
威爾科克森秩和檢定 wilcox
m
,n
威爾科克森符號秩檢定 signrank
n
標記星號的項目僅有 ‘p’ 和 ‘q’ 函數可用,且所有非中心分佈都沒有 ‘r’ 函數。
(如果抑制重新對應,常態分佈名稱為 Rf_dnorm4
、Rf_pnorm5
和 Rf_qnorm5
。)
此外,多變數 RNG for the multinomial distribution 為
void rmultinom(int n, double* prob, int K, int* rN)
其中 K = length(prob)
、sum(prob[.]) == 1,且 rN
必須指向長度為 K
的整數向量 n1 n2 .. nK,其中每個項目 nj=rN[j]
由 Bin(n; prob[j]) 中的隨機二項式「填滿」,約束為 sum(rN[.]) == n。
呼叫 dwilcox
、pwilcox
或 qwilcox
之後,應呼叫函數 wilcox_free()
,而簽署排名函數則類似地呼叫 signrank_free()
。 由於 wilcox_free()
和 signrank_free()
僅新增至 R 4.2.0 中的 Rmath.h,因此使用它們需要類似於
#include "Rmath.h" #include "Rversion.h" #if R_VERSION < R_Version(4, 2, 0) extern void wilcox_free(void); extern void signrank_free(void); #endif
對於負二項式分佈(‘nbinom’),除了 (size, prob)
參數化之外,函數 ‘[dpqr]nbinom_mu()’ 也提供替代的 (size, mu)
參數化,請參閱 R 中的 ?NegBinomial。
函數 dpois_raw(x, *)
和 dbinom_raw(x, *)
是 Poisson 和二項式機率質量函數的版本,在 x
中連續運作,而 dbinom(x,*)
和 dpois(x,*)
僅對整數 x
傳回非零值。
double dbinom_raw(double x, double n, double p, double q, int give_log) double dpois_raw (double x, double lambda, int give_log)
請注意,dbinom_raw()
會同時傳回 p 和 q = 1-p,當其中一個接近 1 時,這可能會很有幫助。
double
gammafn (double x)
¶double
lgammafn (double x)
¶double
digamma (double x)
¶double
trigamma (double x)
¶double
tetragamma (double x)
¶double
pentagamma (double x)
¶double
psigamma (double x, double deriv)
¶void
dpsifn (double x, int n, int kode, int m, double* ans, int* nz, int* ierr)
¶Gamma 函數、其絕對值的自然對數和前四個導數,以及 Psi 的第 n 個導數,即 digamma 函數,它是 lgammafn
的導數。換句話說,digamma(x)
與 psigamma(x,0)
相同,trigamma(x) == psigamma(x,1)
,等等。底層主力 dpsifn()
很實用,例如當需要 log Gamma=lgammafn
的幾個導數時。它會計算並在 ans[]
中傳回長度為 m 的序列 (-1)^(k+1) / gamma(k+1) * psi(k;x),其中 k = n ... n+m-1,而 psi(k;x) 是 Psi(x) 的第 k 個導數,即 psigamma(x,k)
。有關更多詳細資訊,請參閱 src/nmath/polygamma.c 中的註解。
double
choose (double n, double k)
¶double
lchoose (double n, double k)
¶從 n 中選取 k 個項目組合數目及其絕對值的自然對數,推廣至任意的實數 n。 k 會四捨五入至最接近的整數(如有需要會發出警告)。
double
bessel_i (double x, double nu, double expo)
¶double
bessel_j (double x, double nu)
¶double
bessel_k (double x, double nu, double expo)
¶double
bessel_y (double x, double nu)
¶類型 I、J、K 和 Y 的貝塞爾函數,其指標為 nu。對於 bessel_i
和 bessel_k
,如果 expo 為 2,則有選項傳回 exp(-x) I(x; nu) 或 exp(x) K(x; nu)。(對於未縮放的值,請使用 expo == 1
。)
有幾個其他數值工具函數可用做進入點。
double
R_pow (double x, double y)
¶double
R_pow_di (double x, int i)
¶R_pow(x, y)
和 R_pow_di(x, i)
分別計算 x^y
和 x^i
,分別使用 R_FINITE
檢查並傳回適當的結果(與 R 相同),針對 x、y 或 i 為 0 或遺失或無限大或 NaN
的情況。
double
log1p (double x)
¶計算 log(1 + x)
(log 1 plus x),即使對於小的 x 也很精確,即 |x| << 1。
這應該由您的平台提供,在這種情況下它不包含在 Rmath.h 中,但(可能)在 math.h 中,其中包含 Rmath.h(C++ 除外,因此它可能未宣告為 C++98)。
double
log1pmx (double x)
¶計算 log(1 + x) - x
(log 1 plus x minus x),即使對於較小的 x 亦準確,即 |x| << 1。
double
log1pexp (double x)
¶計算 log(1 + exp(x))
(log 1 plus exp),準確,特別是對於較大的 x,例如 x > 720。
double
log1mexp (double x)
¶計算 log(1 - exp(-x))
(log 1 minus exp),準確,小心處理 x 的兩個區域,最佳切斷點為 log 2 (= 0.693147..),使用 ((-x) > -M_LN2 ? log(-expm1(-x)) : log1p(-exp(-x)))
。
double
expm1 (double x)
¶計算 exp(x) - 1
(exp x minus 1),即使對於較小的 x 亦準確,即 |x| << 1。
這應該由您的平台提供,在這種情況下它不包含在 Rmath.h 中,但(可能)在 math.h 中,其中包含 Rmath.h(C++ 除外,因此它可能未宣告為 C++98)。
double
lgamma1p (double x)
¶計算 log(gamma(x + 1))
(log(gamma(1 plus x))),即使對於較小的 x 亦準確,即 0 < x < 0.5。
double
cospi (double x)
¶精確計算 cos(pi * x)
(其中 pi
為 3.14159...),特別是對於半整數 x。
這可能會由您的平台提供165,這種情況下它不會包含在 Rmath.h 中,但會包含在 math.h 中,而 Rmath.h 包含 math.h。(確保在 Rmath.h 之前未包含 math.h 或 cmath,或在第一次包含之前定義
#define __STDC_WANT_IEC_60559_FUNCS_EXT__ 1
)
double
sinpi (double x)
¶精確計算 sin(pi * x)
,特別是對於(半)整數 x。
這可能會由您的平台提供,這種情況下它不會包含在 Rmath.h 中,但會包含在 math.h 中,而 Rmath.h 包含 math.h(但請參閱 cospi
的註解)。
double
Rtanpi (double x)
¶精確計算 tan(pi * x)
,特別是對於整數 x,對於半整數 x 給出 NaN,對於(非半)四分之一整數給出 +1 或 -1。
double
tanpi (double x)
¶計算 tan(pi * x)
,對於整數 x 準確,但對於半整數(和四分之一整數)的行為可能依平台而定。這可能是由您的平台提供的,這種情況下它不會包含在 Rmath.h 中,但會包含在 math.h 中,而 Rmath.h 會包含它(但請參閱 cospi
的註解)。
double
logspace_add (double logx, double logy)
¶double
logspace_sub (double logx, double logy)
¶double
logspace_sum (const double* logx, int n)
¶從項的對數計算和或差的對數,即「x + y」為 log (exp(logx) + exp(logy))
,「x - y」為 log (exp(logx) - exp(logy))
,「sum_i x[i]」為 log (sum[i = 1:n exp(logx[i])] )
,不會造成不必要的溢位或丟失太多精度。
int
imax2 (int x, int y)
¶int
imin2 (int x, int y)
¶double
fmax2 (double x, double y)
¶double
fmin2 (double x, double y)
¶分別傳回兩個整數或雙精度數字中較大 (max
) 或較小 (min
) 的數字。請注意,當其中一個參數為 NaN
時,fmax2
和 fmin2
與 C99/C++11 的 fmax
和 fmin
不同:這些版本會傳回 NaN
。
double
sign (double x)
¶計算 符號 函數,其中 sign(x) 為 1、0 或 -1,分別表示 x 為正、0 或負,如果 x
為 NaN
,則為 NaN
。
double
fsign (double x, double y)
¶執行「符號傳遞」,定義為 |x| * sign(y)。
double
fprec (double x, double digits)
¶傳回 x 四捨五入到 digits 位小數 (小數點後)。
這是 R 的 signif()
使用的函式。
double
fround (double x, double digits)
¶傳回 x 四捨五入到 digits 位有效小數。
這是 R 的 round()
使用的函式。(注意 C99/C++11 提供 round
函式,但 C++98 不一定提供。)
double
ftrunc (double x)
¶傳回 x 截斷 (為整數值) 到 0。
R 有一組常用的數學常數,包含 POSIX 定義的常數,通常在標頭檔 math.h 和 cmath 中找到,以及統計運算中使用的其他常數。這些常數在 Rmath.h 中定義為(至少) 30 位數精確度。下列定義使用 ln(x)
表示自然對數(R 中的 log(x)
)。
名稱 定義 ( ln = log
)round(值, 7) M_E
e 2.7182818 M_LOG2E
log2(e) 1.4426950 M_LOG10E
log10(e) 0.4342945 M_LN2
ln(2) 0.6931472 M_LN10
ln(10) 2.3025851 M_PI
pi 3.1415927 M_PI_2
pi/2 1.5707963 M_PI_4
pi/4 0.7853982 M_1_PI
1/pi 0.3183099 M_2_PI
2/pi 0.6366198 M_2_SQRTPI
2/sqrt(pi) 1.1283792 M_SQRT2
sqrt(2) 1.4142136 M_SQRT1_2
1/sqrt(2) 0.7071068 M_SQRT_3
sqrt(3) 1.7320508 M_SQRT_32
sqrt(32) 5.6568542 M_LOG10_2
log10(2) 0.3010300 M_2PI
2*pi 6.2831853 M_SQRT_PI
sqrt(pi) 1.7724539 M_1_SQRT_2PI
1/sqrt(2*pi) 0.3989423 M_SQRT_2dPI
sqrt(2/pi) 0.7978846 M_LN_SQRT_PI
ln(sqrt(pi)) 0.5723649 M_LN_SQRT_2PI
ln(sqrt(2*pi)) 0.9189385 M_LN_SQRT_PId2
ln(sqrt(pi/2)) 0.2257914
在所包含的標頭 R_ext/Constants.h 中定義了一組常數 (PI
、DOUBLE_EPS
)(等等)(除非定義了 STRICT_R_HEADERS
),主要用於與 S 相容。除了 PI
之外,所有都已棄用,應替換為該檔案中使用的 C99/C++11 版本。
此外,所包含的標頭 R_ext/Boolean.h 具有列舉常數 TRUE
和 FALSE
,類型為 Rboolean
,目的是提供一種在 C 中一致使用「邏輯」變數的方法。這可能會與其他軟體產生衝突:例如,它會與 IJG 的 jpeg-9
中的標頭產生衝突(但不會與較早版本產生衝突)。
可以直接存取 optim
下方的 C 程式碼。使用者需要提供一個函式來計算要最小化的函式,類型為
typedef double optimfn(int n, double *par, void *ex);
其中第一個參數是第二個參數中參數的數量。第三個參數是一個從呼叫常式傳遞下來的指標,通常用於傳遞輔助資訊。
有些方法也需要一個梯度函數
typedef void optimgr(int n, double *par, double *gr, void *ex);
它會在 gr
參數中傳回梯度。沒有提供有限差分函數,也沒有提供在結果中逼近海森矩陣的函數。
介面(定義在標頭 R_ext/Applic.h 中)為
void nmmin(int n, double *xin, double *x, double *Fmin, optimfn fn, int *fail, double abstol, double intol, void *ex, double alpha, double beta, double gamma, int trace, int *fncount, int maxit);
void vmmin(int n, double *x, double *Fmin, optimfn fn, optimgr gr, int maxit, int trace, int *mask, double abstol, double reltol, int nREPORT, void *ex, int *fncount, int *grcount, int *fail);
void cgmin(int n, double *xin, double *x, double *Fmin, optimfn fn, optimgr gr, int *fail, double abstol, double intol, void *ex, int type, int trace, int *fncount, int *grcount, int maxit);
void lbfgsb(int n, int lmm, double *x, double *lower, double *upper, int *nbd, double *Fmin, optimfn fn, optimgr gr, int *fail, void *ex, double factr, double pgtol, int *fncount, int *grcount, int maxit, char *msg, int trace, int nREPORT);
void samin(int n, double *x, double *Fmin, optimfn fn, int maxit, int tmax, double temp, int trace, void *ex);
許多參數在各種方法中都是通用的。 n
是參數的數量, x
或 xin
是輸入時的起始參數, x
是輸出時的最終參數,最終值會傳回 Fmin
。大多數其他參數可以在 optim
的說明頁面中找到:請參閱原始碼 src/appl/lbfgsb.c 以取得 nbd
的值,它會指定要使用哪些邊界。
可以用直接存取的方式存取 integrate
底層的 C 程式碼。使用者需要提供一個 向量化 C 函數來計算要積分的函數,類型為
typedef void integr_fn(double *x, int n, void *ex);
其中 x[]
為輸入和輸出,長度為 n
,亦即類型為 integr_fn
的 C 函數,例如 fn
,基本上會執行 for(i in 1:n) x[i] := f(x[i], ex)
。向量化需求可用於加速被積分函數,而非呼叫它 n
次。請注意,在目前建構於 QUADPACK 的實作中,n
會是 15 或 21。ex
參數是由呼叫常式傳遞的指標,通常用於傳遞輔助資訊。
有介面(定義於標頭 R_ext/Applic.h)用於有限和無限區間(或「範圍」或「積分邊界」)的積分。
void Rdqags(integr_fn f, void *ex, double *a, double *b, double *epsabs, double *epsrel, double *result, double *abserr, int *neval, int *ier, int *limit, int *lenw, int *last, int *iwork, double *work);
void Rdqagi(integr_fn f, void *ex, double *bound, int *inf, double *epsabs, double *epsrel, double *result, double *abserr, int *neval, int *ier, int *limit, int *lenw, int *last, int *iwork, double *work);
這兩個積分器僅在第 3 和第 4 個參數不同;對於使用 Rdqags
的有限範圍積分,a
和 b
是積分區間邊界,而對於使用 Rdqagi
的無限範圍積分,bound
是積分的有限邊界(如果積分不是雙無限),而 inf
是指示積分範圍類型的程式碼,
inf = 1
對應於 (bound, +Inf),
inf = -1
對應於 (-Inf, bound),
inf = 2
對應於 (-Inf, +Inf),
f
和 ex
定義積分函數,見上文;epsabs
和 epsrel
指定請求的絕對和相對精度,result
、abserr
和 last
是 R 函數 integrate 的輸出組成部分 value
、abs.err
和 subdivisions
,其中 neval
給出積分函數評估的數量,錯誤代碼 ier
被轉換為 R 的 integrate() $ message
,查看該函數定義。limit
對應於 integrate(..., subdivisions = *)
。看來您應始終將兩個工作陣列和第二個陣列的長度定義為
lenw = 4 * limit; iwork = (int *) R_alloc(limit, sizeof(int)); work = (double *) R_alloc(lenw, sizeof(double));
src/appl/integrate.c 中的源代碼中的註解給出了更多詳細資訊,特別是關於失敗的原因 (ier >= 1
)。
R 有一組相當全面的排序例程,可供使用者 C 代碼使用。以下在標頭檔 Rinternals.h 中宣告。
void
R_orderVector (int* indx, int n, SEXP arglist, Rboolean nalast, Rboolean decreasing)
¶void
R_orderVector1 (int* indx, int n, SEXP x, Rboolean nalast, Rboolean decreasing)
¶R_orderVector()
對應到 R 的 order(..., na.last, decreasing)
。更具體地說,indx <- order(x, y, na.last, decreasing)
對應到 R_orderVector(indx, n, Rf_lang2(x, y), nalast, decreasing)
而對於三個向量,Rf_lang3(x,y,z)
被用作 arglist。
R_orderVector
和 R_orderVector1
都假設向量 indx
被分配到長度 >= n。在回傳時,indx[]
包含 0:(n-1)
的排列,也就是以 0 為基礎的 C 指數(而不是以 1 為基礎的 R 指數,如同 R 的 order()
)。
當只排序一個向量時,R_orderVector1
較快,且對應到 R 的 indx <- order(x, na.last, decreasing)
(但它是以 0 為基礎的)。它在 R 3.3.0 中被加入。
所有其他排序常式都在標頭檔 R_ext/Utils.h 中宣告(由 R.h 包含),且包括以下內容。
void
R_isort (int* x, int n)
¶void
R_rsort (double* x, int n)
¶void
R_csort (Rcomplex* x, int n)
¶void
rsort_with_index (double* x, int* index, int n)
¶前三個分別對整數、實數 (雙精度) 和複數資料進行排序。(複數先依實部排序,再依虛部排序。)NA
會排在最後。
rsort_with_index
對 x 進行排序,並將相同的置換套用至 index。 NA
會排在最後。
void
revsort (double* x, int* index, int n)
¶類似於 rsort_with_index
,但會按遞減順序排序,且不會處理 NA
。
void
iPsort (int* x, int n, int k)
¶void
rPsort (double* x, int n, int k)
¶void
cPsort (Rcomplex* x, int n, int k)
¶這些函數都提供(非常)部分的排序:它們會置換 x,使得 x[k]
處於正確的位置,較小的值在左側,較大的值在右側。
void
R_qsort (double *v, size_t i, size_t j)
¶void
R_qsort_I (double *v, int *I, int i, int j)
¶void
R_qsort_int (int *iv, size_t i, size_t j)
¶void
R_qsort_int_I (int *iv, int *I, int i, int j)
¶這些常式會根據 R 的 sort(v, method = "quick")
所使用的快速排序演算法,對 v[i:j]
或 iv[i:j]
(使用 1 索引,亦即 v[1]
是第一個元素)進行排序,並記錄在 R 函數 sort
的說明頁面上。 ..._I()
版本也會在 I
中傳回 sort.index()
向量。請注意,排序並不穩定,因此繫結值可能會被置換。
請注意,(明確地)未處理 NA
,如果可能出現 NA
,您應該使用不同的排序函數。
subroutine
qsort4 (double precision v, integer indx, integer ii, integer jj)
¶subroutine
qsort3 (double precision v, integer ii, integer jj)
¶雙精度向量的 Fortran 介面常式為 qsort3
和 qsort4
,分別等於 R_qsort
和 R_qsort_I
。
void
R_max_col (double* matrix, int* nr, int* nc, int* maxes, int* ties_meth)
¶給定以欄優先(「Fortran」)順序排列的 nr 乘以 nc 矩陣 matrix
,R_max_col()
會在 maxes[i-1]
中傳回第 i 列最大元素的欄位號碼(與 R 的 max.col()
函數相同)。如果有多個最大值(平手),*ties_meth
是 1:3
中的整數代碼,用於決定方法:1 =「隨機」、2 =「第一個」和 3 =「最後一個」。請參閱 R 的說明頁面 ?max.col
。
int
findInterval (double* xt, int n, double x, Rboolean rightmost_closed, Rboolean all_inside, int ilo, int* mflag)
¶int
findInterval2(double* xt, int n, double x, Rboolean rightmost_closed, Rboolean all_inside, Rboolean left_open, int ilo, int* mflag)
¶給定長度為 n 的已排序向量 xt,傳回 x 在 xt[]
中的區間或索引,通常為 max(i; 1 <= i <= n & xt[i] <= x),其中我們使用 1 為索引,如同 R 和 Fortran(但 C 不使用)。如果 rightmost_closed 為 true,如果 x 等於 xt[n],也會傳回 n-1。如果 all_inside 不為 0,即使 x 超出 xt[] 範圍,結果也會強制設定在 1:(n-1)
中。傳回時,*mflag
等於 -1,如果 x < xt[1],等於 +1,如果 x >= xt[n],,否則等於 0。
當 ilo 設為 findInterval()
的最後結果,且 x 是在後續呼叫中遞增或遞減的序列值時,演算法特別快。
findInterval2()
是 findInterval()
的廣義化,具有額外的 Rboolean
參數 left_open。設定 left_open = TRUE
基本上會將所有左閉右開區間 t) 替換為左開區間 t],有關詳細資訊,請參閱 R 函數 findInterval
的說明頁面。
也有 F77_CALL(interv)()
版本的 findInterval()
,其參數相同,但都是指標。
提供一個與系統無關的介面來產生暫存檔的名稱,如下所示
char *
R_tmpnam (const char *prefix, const char *tmpdir)
¶char *
R_tmpnam2 (const char *prefix, const char *tmpdir, const char *fileext)
¶void
R_free_tmpnam (char *name)
¶傳回暫存檔的路徑名稱,其名稱以 prefix 開頭,以 fileext 結尾,且位於 tmpdir 目錄中。以 NULL
作為前置詞或擴充功能會被 ""
取代。請注意,傳回值是動態配置的,且在不再需要時應使用 R_free_tmpnam
釋放(與系統呼叫 tmpnam
不同)。不再建議使用 free
釋放結果。
還有用於擴充數個 R 函數中檔案名稱的內部函數,並由 path.expand
直接呼叫。
const char *
R_ExpandFileName (const char *fn)
¶透過將開頭的波浪符號替換為使用者的家目錄(如果已定義)來擴充路徑名稱 fn。確切的意義取決於平台;如果已定義,通常會從環境變數 HOME
取得。
基於歷史原因,有 Fortran 介面可呼叫函數 D1MACH
和 I1MACH
。這些介面可以從 C 程式碼呼叫,例如 F77_CALL(d1mach)(4)
。請注意,這些是 Fox、Hall 和 Schryer 在 NetLib 上針對 IEC 60559 算術(R 所需)所提供的原始函數的模擬,網址為 https://netlib.org/slatec/src/。
R 有自己的 C 層級介面,可使用 iconv
提供的編碼轉換功能,因為不同 iconv
實作中的宣告不相容。
這些宣告在標頭檔 R_ext/Riconv.h 中。
void *
Riconv_open (const char *to, const char *from)
¶設定編碼物件的指標,用於在兩個編碼之間轉換:""
表示目前的區域設定。
size_t
Riconv (void *cd, const char **inbuf, size_t *inbytesleft, char **outbuf, size_t *outbytesleft)
¶盡可能將 inbuf
轉換為 outbuf
。最初,size_t
變數表示緩衝區中可用的位元組數,它們會更新(且 char
指標會更新為指向緩衝區中的下一個可用位元組)。傳回值是已轉換的字元數,或 (size_t)-1
(注意:size_t
通常是未簽署的類型)。假設錯誤條件將 errno
設定為 E2BIG
(輸出緩衝區已滿)、EILSEQ
(無法轉換輸入,且在指定的編碼中可能是無效的)或 EINVAL
(輸入未以完整的多位元組字元結尾)應是安全的。
int
Riconv_close (void * cd)
¶釋放編碼物件的資源。
有三個函式可供在 C 程式碼中建立條件處理常式
#include <Rinternals.h> SEXP R_tryCatchError(SEXP (*fun)(void *data), void *data, SEXP (*hndlr)(SEXP cond, void *hdata), void *hdata); SEXP R_tryCatch(SEXP (*fun)(void *data), void *data, SEXP, SEXP (*hndlr)(SEXP cond, void *hdata), void *hdata, void (*clean)(void *cdata), void *cdata); SEXP R_withCallingErrorHandler(SEXP (*fun)(void *data), void *data, SEXP (*hndlr)(SEXP cond, void *hdata), void *hdata)
R_tryCatchError
為繼承 error
類別的條件建立離開處理常式。
R_tryCatch
可用於為其他條件建立處理常式並註冊清理動作。要處理的條件指定為字元向量 (STRSXP
)。如果不需要條件處理或清理,可以將 NULL
指標傳遞為 fun
或 clean
。
這些目前使用 R 層級的 tryCatch
機制實作,因此會產生一些負擔。
R_withCallingErrorHandler
為繼承 error
類別的條件建立呼叫處理常式。它在不呼叫回 R 的情況下建立處理常式,因此會更有效率。
函式 R_UnwindProtect
可用於確保清理動作在一般回傳以及非區域控制轉移(R 以 longjmp
實作)時執行。
SEXP R_UnwindProtect(SEXP (*fun)(void *data), void *data, void (*clean)(void *data, Rboolean jump), void *cdata, SEXP cont);
R_UnwindProtect
可用於兩種方式。較為簡單的用法,適用於 C 程式碼,傳遞 NULL
給 cont
參數。R_UnwindProtect
會呼叫 fun(data)
。如果 fun
傳回一個值,則 R_UnwindProtect
會在傳回 fun
傳回的值之前呼叫 clean(cleandata, FALSE)
。如果 fun
執行非區域控制轉移,則會呼叫 clean(cleandata, TRUE)
,並且非區域控制轉移會繼續執行。
第二種使用模式,適用於支援 C++ 堆疊解開,使用兩個額外的函式
SEXP R_MakeUnwindCont(); NORET void R_ContinueUnwind(SEXP cont);
R_MakeUnwindCont
分配一個 延續權杖 cont
傳遞給 R_UnwindProtect
。這個權杖應該在呼叫 R_UnwindProtect
之前使用 PROTECT
保護。當 clean
函式以 jump == TRUE
呼叫,表示 R 正在執行非區域控制轉移,它可以對 C++ catch
拋出一個 C++ 例外,在要解開的 C++ 程式碼之外,然後在呼叫 R_ContinueUnwind(cont)
中使用延續權杖來繼續 R 內部的非區域控制轉移。
在編譯程式碼中執行長運算時,R 的任何部分都不能中斷,因此程式設計人員應該透過從 C 呼叫,在適當的點提供中斷程式碼的準備
#include <R_ext/Utils.h> void R_CheckUserInterrupt(void);
和 Fortran
subroutine rchkusr()
這些會檢查使用者是否已要求中斷,如果是,則會分支到 R 的錯誤訊號函式。
請注意,如果從 C 或 Fortran 程式碼呼叫,則在此定義的其中一個進入點背後的程式碼可能會中斷或產生錯誤,因此不會傳回您的程式碼。
標頭檔案定義 USING_R
,可用於測試程式碼是否確實與 R 一起使用。
標頭檔案 Rconfig.h(由 R.h 包含)用於定義特定於平台的巨集,主要用於其他標頭檔案。巨集 WORDS_BIGENDIAN
定義在 big-endian166 系統(例如 Sparc 和 PowerPC 硬體上的大多數作業系統)上,而不在 little-endian 系統(現在所有較常見的 R 平台)上。在處理二進位檔案時,這可能會很有用。注意:這些巨集僅適用於用於建置 R 的 C 編譯器,不一定要適用於其他 C 或 C++ 編譯器。
標頭檔 Rversion.h(未包含在 R.h 中)定義巨集 R_VERSION
,提供以整數編碼的版本號碼,加上巨集 R_Version
來進行編碼。這可以用來測試 R 的版本是否夠新,或包含向後相容性功能。為了防範沒有這個巨集的非常舊版 R,請使用類似這樣的結構
#if defined(R_VERSION) && R_VERSION >= R_Version(3, 1, 0) ... #endif
更詳細的資訊可在巨集中取得 R_MAJOR
、R_MINOR
、R_YEAR
、R_MONTH
和 R_DAY
:請參閱標頭檔 Rversion.h 以取得其格式。請注意次要版本包含修補程式等級(如「2.2」)。
使用 alloca
的套件需要確保已定義:由於它既不是 C 也不是 POSIX 的一部分,因此沒有標準方法可以做到這一點。可以使用
#include <Rconfig.h> // for HAVE_ALLOCA_H #ifdef __GNUC__ // this covers gcc, clang, icc # undef alloca # define alloca(x) __builtin_alloca((x)) #elif defined(HAVE_ALLOCA_H) // needed for native compilers on Solaris and AIX # include <alloca.h> #endif
(這應該包含在標準 C 標頭之前,例如 stdlib.h,因為在某些平台上,這些標頭包含 malloc.h,而 malloc.h 可能有衝突的定義),這對於已知的 R 平台來說就足夠了。
C99 關鍵字 inline
應當會被所有當今用於建置 R 的編譯器識別。可能用於較早版本 R 的可攜式程式碼可以使用巨集 R_INLINE
(定義在由 R.h 包含的檔案 Rconfig.h 中)撰寫,例如來自套件 cluster
#include <R.h> static R_INLINE int ind_2(int l, int j) { ... }
請注意,在多個編譯單元中使用內聯函數幾乎不可能做到可攜式,請參閱 https://www.greenend.org.uk/rjk/tech/inline.html,因此此用法適用於範例中的 static
函數。所有 R 組態程式碼已檢查 R_INLINE
是否可用於用於建置 R 的編譯器中的單一 C 檔案。我們建議廣泛使用內聯的套件包含自己的組態程式碼。
標頭 R_ext/Visibility.h 有一些定義用於控制進入點的可見性。這些定義僅在定義「HAVE_VISIBILITY_ATTRIBUTE」時有效,這會在組態 R 時檢查並記錄在標頭 Rconfig.h 中(由 R_ext/Visibility.h 包含)。它通常定義在具有最新編譯器的現代類 Unix 系統上167,但不支援 macOS 或 Windows。最小化共用函式庫中符號的可見性將加速其載入(不太可能顯著)並降低連結到同名的其他進入點的可能性。
以 attribute_hidden
為前綴的 C/C++ 進入點在共用物件中將不可見。Fortran 進入點沒有可比較的機制,但有一個更全面的方案由套件 stats 使用。大多數允許控制可見性的編譯器都允許透過旗標控制所有符號 via 的可見性,已知的旗標封裝在巨集中,分別為 C、C++ 和 Fortran 編譯器的「C_VISIBILITY」、「CXX_VISIBILITY」168 和「F_VISIBILITY」169。這些定義在 etc/Makeconf 中,因此可用於套件程式碼的正常編譯。例如,src/Makevars 可以包含一些
PKG_CFLAGS=$(C_VISIBILITY) PKG_CXXFLAGS=$(CXX_VISIBILITY) PKG_FFLAGS=$(F_VISIBILITY)
這最終會導致 沒有 可見進入點,這毫無意義。但是,可以使用 attribute_visible
前綴來覆寫旗標的效果。註冊其進入點的共用物件只需要有一個可見進入點,也就是其初始化程式,因此例如套件 stats 具有
void attribute_visible R_init_stats(DllInfo *dll) { R_registerRoutines(dll, CEntries, CallEntries, FortEntries, NULL); R_useDynamicSymbols(dll, FALSE); ... }
因為「C_VISIBILITY」機制僅與 attribute_visible
結合使用時才有用,因此除非定義了「HAVE_VISIBILITY_ATTRIBUTE」,否則不會啟用。通常的可見性旗標為 -fvisibility=hidden:有些編譯器也支援 -fvisibility-inlines-hidden,可以在建立 R 時覆寫 config.site 中的「C_VISIBILITY」和「CXX_VISIBILITY」,或編輯 R 安裝中的 etc/Makeconf 時使用。
請注意,configure
只檢查可見性屬性和旗標是否被接受,而不是實際隱藏符號。
Windows 上沒有可見性機制,但有一種同樣有效的方法來控制哪些進入點可見,方法是提供定義檔 pkgnme/src/pkgname-win.def:只有該檔中列出的進入點才會可見。再次使用 stats 作為範例,它具有
LIBRARY stats.dll EXPORTS R_init_stats
可以將 Mathlib
(R 中的數學函數集,記載於 Rmath.h 中)建置為獨立函式庫 libRmath,在類 Unix 系統和 Windows 中皆可建置。(這包括記載於 數值分析子常式 中的函數,因為來自該標頭檔。)
在安裝 R 時不會自動建置函式庫,但可以在 R 原始碼中的 src/nmath/standalone 目錄中建置:請參閱其中的 README 檔案。若要在您自己的 C 程式中使用程式碼,請包含
#define MATHLIB_STANDALONE #include <Rmath.h>
並連結到 ‘-lRmath’(以及可能 ‘-lm’)。有一個範例檔案 test.c。
使用亂數常式時需要小心。您需要提供均勻亂數產生器
double unif_rand(void)
或使用提供的亂數產生器(對於動態函式庫或 DLL,您必須使用提供的亂數產生器,也就是 Marsaglia-multicarry,其進入點為
set_seed(unsigned int, unsigned int)
用於設定種子,而
get_seed(unsigned int *, unsigned int *)
用於讀取種子)。
R 安裝的標頭檔位於目錄 R_INCLUDE_DIR(預設 R_HOME/include)。目前包括
R.h 包含許多其他檔案 Rinternals.h 用於 R 內部結構的定義 Rdefines.h 用於上述內容的 S 式介面的巨集(不再維護) Rmath.h 獨立的數學函式庫 Rversion.h R 版本資訊 Rinterface.h 用於外掛前端(僅類 Unix) Rembedded.h 用於外掛前端 R_ext/Applic.h 最佳化和整合 R_ext/BLAS.h BLAS 常式的 C 定義 R_ext/Callbacks.h C(和 R 函式)頂層任務處理常式 R_ext/GetX11Image.h 套件 trkplot 使用的 X11Image 介面 R_ext/Lapack.h 一些 LAPACK 常式的 C 定義 R_ext/Linpack.h 一些 LINPACK 常式的 C 定義,並非全部都包含在 R 中 R_ext/Parse.h R 分析介面的其中一小部分:不是穩定 API 的一部分。 R_ext/RStartup.h 用於外掛前端 R_ext/Rdynload.h 需要在套件中註冊已編譯的程式碼 R_ext/Riconv.h 與 iconv
的介面R_ext/Visibility.h 控制可見度的定義 R_ext/eventloop.h 用於外掛前端和需要共用 R 事件迴圈的套件(非 Windows)
下列標頭檔由 R.h 包含
Rconfig.h 已公開組態資訊 R_ext/Arith.h 處理 NA
、NaN
、Inf
/-Inf
R_ext/Boolean.h TRUE
/FALSE
類型R_ext/Complex.h C 為 R 的 complex
定義類型R_ext/Constants.h 常數 R_ext/Error.h 錯誤訊號 R_ext/Memory.h 記憶體配置 R_ext/Print.h Rprintf
及其變體。R_ext/RS.h 定義 R.h 與先前的 S.h 共通的定義,包括 F77_CALL
等。R_ext/Random.h 亂數產生 R_ext/Utils.h 排序及其他公用程式 R_ext/libextern.h Windows 上 R.dll 匯出的定義。
圖形系統在標頭 R_ext/GraphicsEngine.h、R_ext/GraphicsDevice.h(它包含)和 R_ext/QuartzDevice.h 中公開。在 R_ext/Connections.h 中提供定義自訂連線實作的設施,但在使用前請務必參閱該檔案。
讓我們再次重申建議,在 R 標頭檔案之前包含系統標頭,特別是 Rinternals.h(由 Rdefines.h 包含)和 Rmath.h,它們重新定義可能會在系統標頭中使用的名稱(如果在包含之前定義 'R_NO_REMAP',或針對 Rmath.h 定義 'R_NO_REMAP_RMATH',則較少)。
R 程式設計師通常會想要為現有的通用函數新增方法,而且可能會想要新增新的通用函數或讓現有的函數變成通用函數。在本章中,我們將提供這樣做的準則,並舉出不遵守這些準則所造成的問題範例。
此章節僅涵蓋從 S3 複製而來的「非正式」類別系統,而非套件 methods 的 S4(正式)方法。
首先,一個名為 gen.cl
的函式將會被泛函 gen
呼叫,以取得類別 cl
,因此請勿以這種樣式命名函式,除非它們打算成為方法。
方法的主要函式為 NextMethod
,它會派送下一個方法。方法函式通常會對其引數做一些變更,派送至下一個方法,接收結果並稍加修改。一個範例為
t.data.frame <- function(x) { x <- as.matrix(x) NextMethod("t") }
請注意,以上範例之所以可行,是因為有一個 next 方法(預設方法),而不是在變更類別時選取一個新方法。
任何 程式設計師編寫的方法都可能透過 NextMethod
從另一個方法呼叫,並使用適當於前一個方法的引數。此外,程式設計師無法預測 NextMethod
會選取哪個方法(它可能是尚未想到的方法),而呼叫泛函的最終使用者需要能夠將引數傳遞至下一個方法。為此
一個方法必須具備泛函的所有引數,包括
…
(如果泛函有此引數)。
認為方法只需要接受它需要的參數,這是一個嚴重的誤解。predict.lm
的原始 S 版本沒有 …
參數,儘管 predict
有。很快地,很明顯地 predict.glm
需要一個 dispersion
參數來處理過度離散。由於 predict.lm
既沒有 dispersion
參數,也沒有 …
參數,因此 NextMethod
無法再使用。(舊版,對 predict.lm
的兩個直接呼叫,在 R 中的 predict.glm
中繼續存在,這基於 Venables 和 Ripley 編寫的 S3 解決方法。)
此外,使用者有權在呼叫通用函數時使用位置匹配,而 UseMethod
呼叫的方法的參數是對通用函數的呼叫。因此
方法必須與通用函數完全相同的順序排列參數。
要了解此問題的規模,請考慮定義為
scale <- function (x, center = TRUE, scale = TRUE) UseMethod("scale")
假設一個不假思索的套件撰寫者建立了諸如
scale.foo <- function(x, scale = FALSE, ...) { }
然後對於類別為 "foo"
的 x
,呼叫
scale(x, , TRUE) scale(x, scale = TRUE)
很可能會執行不同的操作,這會讓最終使用者感到合理困惑。
再增加一個曲折,在我們的範例中,當使用者呼叫 scale(x)
時,會使用哪個預設值?如果
scale.bar <- function(x, center, scale = TRUE) NextMethod("scale")
且 x
的類別為 c("bar", "foo")
呢?它會使用在方法中指定的預設值,但通用函數中指定的預設值可能是使用者看到的預設值。這會導致以下建議
如果通用函數指定預設值,則所有方法都應使用相同的預設值。
遵循這些建議的簡單方法是始終保持通用函數簡單,例如
scale <- function(x, ...) UseMethod("scale")
僅在所有可能實作它的方法中都有意義時,才將參數和預設值新增至通用函數。
在建立新的泛函時,請記住其參數清單將會是方法的最大參數組,包括未來幾年後所寫入的參數。因此,選擇一組良好的參數可能是一個重要的設計議題,而且必須有良好的論點不包含 …
參數。
如果提供了 …
參數,應考慮其在參數順序中的位置。在 …
之後的參數必須在呼叫函數時命名,而且必須完整命名(在 …
之後會抑制部分比對)。在 …
之前的形式參數可以部分比對,因此可能會「吞掉」預計用於 …
的實際參數。雖然通常會將 …
參數設為最後一個,但這並不總是正確的選擇。
有時套件撰寫者希望將基本套件中的函數設為泛函,並要求變更 R。這可能是合理的,但將函數設為泛函,並使用舊定義作為預設方法,確實會造成一些效能成本。這並非必要,因為套件可以接管基本套件中的函數,並透過類似下列方式將其設為泛函
foo <- function(object, ...) UseMethod("foo") foo.default <- function(object, ...) base::foo(object)
本手冊的早期版本建議指定 foo.default <- base::foo
。這不是一個好主意,因為它會在安裝時擷取基本函數,而且可能會在 R 修補或更新時變更。
同樣的觀念也可以套用在其他套件中的函數。
有許多方法可以建立 R 的前端:我們將其視為具有提交命令給 R 以及可能接收回傳結果(不一定為文字格式)的能力的 GUI 或其他應用程式。除了這裡所描述的途徑之外,還有其他途徑,例如套件 Rserve(來自 CRAN,另請參閱 https://www.rforge.net/Rserve/)以及在「JRI」中連線到 Java(rJava 套件在 CRAN 上的一部分)。
請注意,本章中所述的 API 僅供在替代前端中使用:它們並非 R 套件可用的 API 的一部分,且在傳統套件中使用它們可能會很危險(儘管套件可能包含替代前端)。相反地,API 中的某些函數(例如 R_alloc
)不應在前端中使用。
如果使用 --enable-R-shlib 進行設定,R 可以建置成共用函式庫170。這個共用函式庫可以用來從其他前端程式執行 R。我們假設本節的其餘部分都已完成此動作。此外,如果使用 --enable-R-static-lib 進行設定,R 可以建置成靜態函式庫,而且可以使用非常類似的程式碼(至少在 Linux 上是如此:在其他平台上,需要確保將 libR.a 匯出的所有符號都連結到前端)。
命令列 R 前端 R_HOME/bin/exec/R 就是一個範例,而之前的 GNOME(請參閱 CRAN 的「檔案」區域中的套件 gnomeGUI)和 macOS 主控台則是其他範例。R_HOME/bin/exec/R 的原始程式碼位於 src/main/Rmain.c 檔案中,而且非常簡單
int Rf_initialize_R(int ac, char **av); /* in ../unix/system.c */ void Rf_mainloop(); /* in main.c */ extern int R_running_as_main_program; /* in ../unix/system.c */ int main(int ac, char **av) { R_running_as_main_program = 1; Rf_initialize_R(ac, av); Rf_mainloop(); /* does not return */ return 0; }
事實上,簡單得令人誤解。請記住,R_HOME/bin/exec/R 是從 shell 指令碼 R_HOME/bin/R 執行的,而這個 shell 指令碼會設定可執行檔的環境,並用於
R_HOME
並檢查其是否有效,以及已安裝 share 和 doc 目錄樹的 R_SHARE_DIR
和 R_DOC_DIR
路徑。如果需要,也會設定 R_ARCH
。
LD_LIBRARY_PATH
以包含連結 R 時使用的目錄。這會記錄為 shell 指令碼 R_HOME/etcR_ARCH/ldpaths 中 R_LD_LIBRARY_PATH
的預設設定。
前兩個動作可以透過透過 R CMD
執行前端來達成。因此,例如
R CMD /usr/local/lib/R/bin/exec/R R CMD exec/R
都會在標準 R 安裝中運作。(R CMD
會先在 R_HOME/bin 中尋找可執行檔。如果使用子架構,則需要修改這些命令列。)如果您不想以這種方式執行前端,則需要確保已設定 R_HOME
,且 LD_LIBRARY_PATH
適用。(後者很可能是,但現代 Unix/Linux 系統通常不包含 /usr/local/lib(在某些架構上為 /usr/local/lib64),而 R 會在那裡尋找系統元件。)
此範例過於簡化的其他意義在於,它使用了所有內部預設值,且已將控制權移交給 R 主迴圈。在 tests/Embedding 目錄中有許多小型範例171。這些範例使用 src/main/Rembedded.c 中的 Rf_initEmbeddedR
,且基本上使用
#include <Rembedded.h> int main(int ac, char **av) { /* do some setup */ Rf_initEmbeddedR(argc, argv); /* do some more setup */ /* submit some code to R, which is done interactively via run_Rmainloop(); A possible substitute for a pseudo-console is R_ReplDLLinit(); while(R_ReplDLLdo1() > 0) { /* add user actions here if desired */ } */ Rf_endEmbeddedR(0); /* final tidying up after R is shutdown */ return 0; }
如果您不想傳遞 R 引數,您可以偽造一個 argv
陣列,例如透過
char *argv[]= {"REmbeddedPostgres", "--silent"}; Rf_initEmbeddedR(sizeof(argv)/sizeof(argv[0]), argv);
不過,為了建立 GUI,我們通常會希望在設定 R 的各個部分與我們的 GUI 對話,並安排在 R 主迴圈期間呼叫我們的 GUI 回呼後,執行 run_Rmainloop
。
在某些平台上,需要留意一個問題,Rf_initEmbeddedR
和 Rf_endEmbeddedR
會變更 FPU 的設定(例如允許捕捉錯誤,並使用延伸精度的暫存器)。
標準程式碼會以一般的方式設定暫存的階段性目錄,除非 在呼叫 Rf_initEmbeddedR
之前,已將 R_TempDir
設定為非 NULL 值。在這種情況下,假設該值包含現有的可寫入目錄,且在關閉 R 時不會清除該目錄。
Rf_initEmbeddedR
會將 R 設定為互動模式:您可以在之後設定 R_Interactive
(在 Rinterface.h 中定義)來變更此設定。
請注意,R 預期會在區域類別「LC_NUMERIC」設定為其預設值 C
的情況下執行,因此不應將其嵌入會變更該設定的應用程式中。
使用者有責任嘗試僅初始化一次。為了保護 R 解譯器,Rf_initialize_R
會在嘗試重新初始化時結束處理程序。
可透過以下方式找到適合用於編譯和連結 R(共用或靜態)函式庫的旗標
R CMD config --cppflags R CMD config --ldflags
(這些旗標僅適用於未安裝的副本或標準安裝。)
如果已安裝 R,且可以使用 pkg-config
,且未曾使用子架構或 macOS 架構,則共用 R 函式庫的替代方案為
pkg-config --cflags libR pkg-config --libs libR
而靜態 R 函式庫的替代方案為
pkg-config --cflags libR pkg-config --static --libs libR
(如果已教導 pkg-config
在何處尋找 libR.pc,則這項方法可能適用於已安裝的作業系統架構:它安裝在架構內。)
不過,更全面的方式是設定 Makefile 來編譯前端。假設檔案 myfe.c 要編譯成 myfe。適合的 Makefile 可能為
## WARNING: does not work when ${R_HOME} contains spaces include ${R_HOME}/etc${R_ARCH}/Makeconf all: myfe ## The following is not needed, but avoids PIC flags. myfe.o: myfe.c $(CC) $(ALL_CPPFLAGS) $(CFLAGS) -c myfe.c -o $@ ## replace $(LIBR) $(LIBS) by $(STATIC_LIBR) if R was build with a static libR myfe: myfe.o $(MAIN_LINK) -o $@ myfe.o $(LIBR) $(LIBS)
呼叫方式為
R CMD make R CMD myfe
儘管不建議,但 ${R_HOME}
可能包含空白。在這種情況下,無法將其傳遞為 makefile 中 include
的引數。相反地,可以使用 -f
選項指示 make
包含 Makeconf,例如透過 make
的遞迴呼叫,請參閱 撰寫可攜式套件。
all: $(MAKE) -f "${R_HOME}/etc${R_ARCH}/Makeconf" -f Makefile.inner
其他 $(MAIN_LINK)
包含的旗標包括在某些平台上用於選擇 OpenMP 和 GNU 連結器 --export-dynamic 的旗標。原則上,使用共用 R 函式庫時不需要 $(LIBS)
,因為 libR 已連結到這些函式庫,但有些平台需要可執行檔也連結到這些函式庫。
對於類 Unix 系統,有一個公開標頭檔 Rinterface.h,它可以讓您以有文件記載的方式變更 R 使用的標準回呼。這會定義指標 (如果定義了 R_INTERFACE_PTRS
)
extern void (*ptr_R_Suicide)(const char *); extern void (*ptr_R_ShowMessage)(const char *); extern int (*ptr_R_ReadConsole)(const char *, unsigned char *, int, int); extern void (*ptr_R_WriteConsole)(const char *, int); extern void (*ptr_R_WriteConsoleEx)(const char *, int, int); extern void (*ptr_R_ResetConsole)(); extern void (*ptr_R_FlushConsole)(); extern void (*ptr_R_ClearerrConsole)(); extern void (*ptr_R_Busy)(int); extern void (*ptr_R_CleanUp)(SA_TYPE, int, int); extern int (*ptr_R_ShowFiles)(int, const char **, const char **, const char *, Rboolean, const char *); extern int (*ptr_R_ChooseFile)(int, char *, int); extern int (*ptr_R_EditFile)(const char *); extern void (*ptr_R_loadhistory)(SEXP, SEXP, SEXP, SEXP); extern void (*ptr_R_savehistory)(SEXP, SEXP, SEXP, SEXP); extern void (*ptr_R_addhistory)(SEXP, SEXP, SEXP, SEXP); extern int (*ptr_R_EditFiles)(int, const char **, const char **, const char *); extern SEXP (*ptr_do_selectlist)(SEXP, SEXP, SEXP, SEXP); extern SEXP (*ptr_do_dataentry)(SEXP, SEXP, SEXP, SEXP); extern SEXP (*ptr_do_dataviewer)(SEXP, SEXP, SEXP, SEXP); extern void (*ptr_R_ProcessEvents)();
它允許將標準 R 回呼重新導向至您的 GUI。這些回呼通常會記載在 src/unix/system.txt 檔案中。
void
R_ShowMessage (char *message)
¶這應該會顯示訊息,訊息可能有多行:它應該立即引起使用者的注意。
void
R_Busy (int which)
¶當 R 開始執行延伸運算 (which=1
) 和此狀態結束 (which=0
) 時,此函式會呼叫動作 (例如變更游標)。
int
R_ReadConsole (const char *prompt, unsigned char *buf, int buflen, int hist)
¶void
R_WriteConsole (const char *buf, int buflen)
¶void
R_WriteConsoleEx (const char *buf, int buflen, int otype)
¶void
R_ResetConsole ()
¶void
R_FlushConsole ()
¶void
R_ClearerrConsole ()
¶這些函式與主控台互動。
R_ReadConsole
會在主控台列印指定的提示字元,然後執行類似 fgets(3)
的操作,將最多 buflen 個字元傳輸到緩衝區 buf 中。最後兩個位元組應設定為 ‘"\n\0"’ 以保持健全性。如果 hist 為非零,則應將該行新增到任何正在維護的命令記錄中。如果沒有可用的輸入,傳回值為 0,否則大於 0。
R_WriteConsoleEx
將指定的緩衝區寫入主控台,otype 指定輸出類型(一般輸出或警告/錯誤)。呼叫 R_WriteConsole(buf, buflen)
等同於 R_WriteConsoleEx(buf, buflen, 0)
。為了確保呼叫回函的向下相容性,ptr_R_WriteConsoleEx
僅在 ptr_R_WriteConsole
設為 NULL
時使用。為了確保 stdout()
和 stderr()
連線指向主控台,請將對應的檔案設為 NULL
經由
R_Outputfile = NULL; R_Consolefile = NULL;
R_ResetConsole
在系統因錯誤而重設時呼叫。呼叫 R_FlushConsole
以將任何待處理的輸出沖至系統主控台。呼叫 R_ClearerrConsole
以清除與從主控台讀取相關的任何錯誤。
int
R_ShowFiles (int nfile, const char **file, const char **headers, const char *wtitle, Rboolean del, const char *pager)
¶此函數用於顯示檔案內容。
int
R_ChooseFile (int new, char *buf, int len)
¶選擇一個檔案,並將其名稱傳回長度為 len 的 buf 中。傳回值為 0 表示成功,否則大於 0。
int
R_EditFile (const char *buf)
¶將檔案傳送至編輯器視窗。
int
R_EditFiles (int nfile, const char **file, const char **title, const char *editor)
¶將 nfile 個檔案傳送至編輯器,標題可能用於編輯器視窗。
SEXP
R_loadhistory (SEXP, SEXP, SEXP, SEXP);
¶SEXP
R_savehistory (SEXP, SEXP, SEXP, SEXP);
¶SEXP
R_addhistory (SEXP, SEXP, SEXP, SEXP);
¶.Internal
函數,用於 loadhistory
、savehistory
和 timestamp
。
如果主控台沒有歷史記錄機制,這些機制可以像以下一樣簡單
SEXP R_loadhistory (SEXP call, SEXP op, SEXP args, SEXP env) { errorcall(call, "loadhistory is not implemented"); return R_NilValue; } SEXP R_savehistory (SEXP call, SEXP op , SEXP args, SEXP env) { errorcall(call, "savehistory is not implemented"); return R_NilValue; } SEXP R_addhistory (SEXP call, SEXP op , SEXP args, SEXP env) { return R_NilValue; }
如果沒有歷史記錄機制,R_addhistory
函數應靜默傳回,因為使用者可能呼叫 timestamp
純粹是為了將時間戳記寫入主控台。
void
R_Suicide (const char *message)
¶這應該盡可能快速地中止 R,並顯示訊息。可能的實作方式為
void R_Suicide (const char *message) { char pp[1024]; snprintf(pp, 1024, "Fatal error: %s\n", message); R_ShowMessage(pp); R_CleanUp(SA_SUICIDE, 2, 0); }
void
R_CleanUp (SA_TYPE saveact, int status, int RunLast)
¶此函式會呼叫在系統終止時發生的任何動作。它需要相當複雜
#include <Rinterface.h> #include <Rembedded.h> /* for Rf_KillAllDevices */ void R_CleanUp (SA_TYPE saveact, int status, int RunLast) { if(saveact == SA_DEFAULT) saveact = SaveAction; if(saveact == SA_SAVEASK) { /* ask what to do and set saveact */ } switch (saveact) { case SA_SAVE: if(runLast) R_dot_Last(); if(R_DirtyImage) R_SaveGlobalEnv(); /* save the console history in R_HistoryFile */ break; case SA_NOSAVE: if(runLast) R_dot_Last(); break; case SA_SUICIDE: default: break; } R_RunExitFinalizers(); /* clean up after the editor e.g. CleanEd() */ R_CleanTempDir(); /* close all the graphics devices */ if(saveact != SA_SUICIDE) Rf_KillAllDevices(); fpu_setup(FALSE); exit(status); }
這些回呼不應在執行中的 R 會話中變更(因此無法從延伸套件呼叫)。
SEXP
R_dataentry (SEXP, SEXP, SEXP, SEXP);
¶SEXP
R_dataviewer (SEXP, SEXP, SEXP, SEXP);
¶SEXP
R_selectlist (SEXP, SEXP, SEXP, SEXP);
¶.External
函式,用於 dataentry
(以及矩陣和資料框上的 edit
)、View
和 select.list
。如果目前未在使用中,這些函式可以變更。
嵌入 R 的應用程式需要不同的符號註冊方式,因為它不是由 R 載入的動態函式庫,就像套件那樣。因此,R 為嵌入式應用程式保留一個特殊的 DllInfo
項目,以便它可以註冊符號以與 .C
、.Call
等一起使用。這個項目可以透過呼叫 getEmbeddingDllInfo
取得,因此典型的用法是
DllInfo *info = R_getEmbeddingDllInfo(); R_registerRoutines(info, cMethods, callMethods, NULL, NULL);
由 cMethods
和 callMethods
定義的原生常式應存在於嵌入式應用程式中。有關一般符號註冊的詳細資訊,請參閱 註冊原生常式。
在 R 與前端介接時,最困難的問題之一就是處理事件迴圈,至少在使用單一執行緒時。R 使用事件和計時器來
locator()
)。
Sys.sleep()
。
具體來說,類 Unix 的 R 命令列版本為以下內容執行獨立的事件迴圈
libcurl
介面。
有一個協定是用於將事件處理常式新增到前兩種類型的事件迴圈,使用在標頭 R_ext/eventloop.h 中宣告的類型和函數,並在檔案 src/unix/sys-std.c 中的註解中說明。可以在特定檔案描述符上新增(或移除)事件的輸入處理常式,或設定輪詢間隔(透過 R_wait_usec
)和一個函數以透過 R_PolledEvents
定期呼叫:輪詢機制由 tcltk 套件使用。
這些設施並非供套件使用,但如果特殊需要,套件應確保在卸載其命名空間時清除並移除其處理常式。請注意,需要標頭 sys/select.h 172:使用者應檢查其可用性,並在包含 R_ext/eventloop.h 之前定義 HAVE_SYS_SELECT_H
。(通常會在處理 eventloop.h 之前,另一個標頭會包含 sys/select.h,但這並不可靠。)
替代前端需要在等待輸入時為其他 R 事件做好準備,並確保它不會在第二種類型的事件期間凍結。tcltk 套件使用將輪詢處理常式新增為 R_timeout_handler
的功能。
嵌入式 R 設計為在主執行緒中執行,所有測試都在該環境中完成。當涉及執行緒時,堆疊檢查機制存在潛在問題。這使用在 Rinterface.h 中宣告的兩個變數(如果定義了 CSTACK_DEFNS
)為
extern uintptr_t R_CStackLimit; /* C stack limit */ extern uintptr_t R_CStackStart; /* Initial stack address */
請注意,uintptr_t
是 C99 的選用型別,R 中已定義其替代品,因此您的程式碼需要適當地定義 HAVE_UINTPTR_T
。為此,請測試 C 標頭 stdint.h 或 C++ 標頭 cstdint 中是否已定義該型別,如果已定義,請在包含 Rinterface.h 之前包含標頭並定義 HAVE_UINTPTR_T
。(對於 C 程式碼,可以只包含 Rconfig.h,可能透過 R.h,而對於 C++11 程式碼,Rinterface.h 會包含標頭 cstdint。)
當呼叫 Rf_initialize_R
時,這些會設定173為適當於主執行緒的值。可以在呼叫 Rf_initialize_R
之後立即設定 R_CStackLimit = (uintptr_t)-1
來停用堆疊檢查,但如果可能的話,最好設定適當的值。(這些是什麼以及如何判定它們是作業系統特定的,而堆疊大小限制可能因次要執行緒而異。如果您有堆疊大小的選擇,建議至少 10Mb。)
您可能也想考慮如何處理訊號:R 為多個訊號設定訊號處理常式,包括 SIGINT
、SIGSEGV
、SIGPIPE
、SIGUSR1
和 SIGUSR2
,但這些都可以透過將 Rinterface.h 中宣告的變數 R_SignalHandlers
設定為 0
來抑制。
請注意,這些變數不能由 R 套件變更:套件不應呼叫 R 內部函式,而這些函式會在次要執行緒上使用堆疊檢查機制。
所有 Windows 介面都會直接或間接呼叫 DLL R.dll 中的 R 進入點。較簡單的應用程式可能會發現透過 (D)COM via 間接路徑會比較容易。
(D)COM 是一種標準的 Windows 機制,用於 Windows 應用程式之間的通訊。一個應用程式 (此處為 R) 以 COM 伺服器的形式執行,提供服務給客戶端,此處為呼叫應用程式的前端。服務會在「類型程式庫」中描述,且 (或多或少) 與語言無關,因此呼叫應用程式可以用 C 或 C++ 或 Visual Basic 或 Perl 或 Python 等語言撰寫。 (D)COM 中的「D」是指「分散式」,因為客戶端和伺服器可以在不同的機器上執行。
R 的基本發行版並非 (D)COM 伺服器,但目前有兩個附加元件可以直接與 R 介接,並提供 (D)COM 伺服器
StatConnector
,由 Thomas Baier 撰寫,可 via https://www.autstat.com/ 取得,它與 R 套件搭配使用,支援將資料傳輸至 R 和從 R 傳輸資料,以及遠端執行 R 指令,以及嵌入 R 圖形視窗。
最近的版本有使用限制。
R
DLL 主要以 C 語言撰寫,並具有 _cdecl
進入點。直接呼叫它會很棘手,除非是從 C 程式碼(或小心一點的 C++)呼叫。
有一個類 Unix 介面呼叫版本
int Rf_initEmbeddedR(int ac, char **av); void Rf_endEmbeddedR(int fatal);
這是 R.dll 中的進入點。可以在來源的 tests/Embedding 目錄中找到其使用範例(以及適當的 Makefile.win)。您可能需要確保 R_HOME/bin 在您的 PATH
中,才能找到 R DLL。
在目錄 src/gnuwin32/front-ends 中提供了直接呼叫 R.dll 的範例,包括一個簡單的命令列前端 rtest.c,其程式碼為
#define Win32 #include <windows.h> #include <stdio.h> #include <Rversion.h> #define LibExtern __declspec(dllimport) extern #include <Rembedded.h> #include <R_ext/RStartup.h> /* for askok and askyesnocancel */ #include <graphapp.h> /* for signal-handling code */ #include <psignal.h> /* simple input, simple output */ /* This version blocks all events: a real one needs to call ProcessEvents frequently. See rterm.c and ../system.c for one approach using a separate thread for input. */ int myReadConsole(const char *prompt, unsigned char *buf, int len, int addtohistory) { fputs(prompt, stdout); fflush(stdout); if(fgets((char *)buf, len, stdin)) return 1; else return 0; } void myWriteConsole(const char *buf, int len) { printf("%s", buf); } void myCallBack(void) { /* called during i/o, eval, graphics in ProcessEvents */ } void myBusy(int which) { /* set a busy cursor ... if which = 1, unset if which = 0 */ } static void my_onintr(int sig) { UserBreak = 1; } int main (int argc, char **argv) { structRstart rp; Rstart Rp = &rp; char Rversion[25], *RHome, *RUser; sprintf(Rversion, "%s.%s", R_MAJOR, R_MINOR); if(strcmp(getDLLVersion(), Rversion) != 0) { fprintf(stderr, "Error: R.DLL version does not match\n"); exit(1); } R_setStartTime(); R_DefParamsEx(Rp, RSTART_VERSION); if((RHome = get_R_HOME()) == NULL) { fprintf(stderr, "R_HOME must be set in the environment or Registry\n"); exit(1); } Rp->rhome = RHome; RUser = getRUser(); Rp->home = RUser; Rp->CharacterMode = LinkDLL; Rp->EmitEmbeddedUTF8 = FALSE; Rp->ReadConsole = myReadConsole; Rp->WriteConsole = myWriteConsole; Rp->CallBack = myCallBack; Rp->ShowMessage = askok; Rp->YesNoCancel = askyesnocancel; Rp->Busy = myBusy; Rp->R_Quiet = TRUE; /* Default is FALSE */ Rp->R_Interactive = FALSE; /* Default is TRUE */ Rp->RestoreAction = SA_RESTORE; Rp->SaveAction = SA_NOSAVE; R_SetParams(Rp); freeRUser(RUser); free_R_HOME(RHome); R_set_command_line_arguments(argc, argv); FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE)); signal(SIGBREAK, my_onintr); GA_initapp(0, 0); readconsolecfg(); setup_Rmainloop(); #ifdef SIMPLE_CASE run_Rmainloop(); #else R_ReplDLLinit(); while(R_ReplDLLdo1() > 0) { /* add user actions here if desired */ } /* only get here on EOF (not q()) */ #endif Rf_endEmbeddedR(0); return 0; }
概念為
HKEY_LOCAL_MACHINE\Software\R-core\R\InstallPath
,否則會在 HKEY_CURRENT_USER\Software\R-core\R\InstallPath
,如果在安裝期間選取(預設為選取)。
Rstart
結構定義啟動條件和回呼。 R_DefParams
設定預設值,而 R_SetParams
設定更新後的數值。 R_DefParamsEx
會取得一個額外的引數,也就是提供的 Rstart
結構的版本號碼(RSTART_VERSION
指的是目前的版本),並在 R 不支援該版本時傳回非零狀態。
R_set_command_line_arguments
使用的命令列引數,供 R 函數 commandArgs()
使用。
一個基本的議題是需要讓 GUI「保持運作」,而這個範例並未做到這一點。R 回呼 R_ProcessEvents
需要頻繁呼叫,以確保 R 視窗中的 Windows 事件得到迅速處理。相反地,R 需要允許 GUI 程式碼(在同一個程序中執行)根據需要更新自身,提供了兩種方法來允許這一點
R_ProcessEvents
呼叫由 Rp->callback
註冊的回呼。其中一個版本用於在 Windows 中為 tcltk 執行套件 Tcl/Tk,因為程式碼是
void R_ProcessEvents(void) { while (peekevent()) doevent(); /* Windows events for GraphApp */ if (UserBreak) { UserBreak = FALSE; onintr(); } R_CallBackHook(); if(R_tcldo) R_tcldo(); }
#ifdef SIMPLE_CASE
。
可能不需要考慮任何 R GraphApp 視窗,儘管這些視窗包括分頁器、windows()
圖形裝置、R 資料和腳本編輯器以及各種快顯視窗,例如 choose.file()
和 select.list()
。可以取代所有這些視窗,但讓 GraphApp 處理其中大部分視窗似乎比較容易。
可以在單一執行緒中於 GUI 中執行 R(如 RGui.exe 所示),但通常使用多個執行緒會比較容易174。
請注意,R 自身的 front end 使用 10Mb 的堆疊大小,而 MinGW 可執行檔預設為 2Mb,Visual C++ 可執行檔則為 1Mb。後者的堆疊大小對於許多 R 應用程式來說太小,因此通用的 front end 應使用較大的堆疊大小。
嵌入 R 4.2.0 及更新版本的應用程式應使用 UCRT 作為 C 執行時期,並在清單中選擇 UTF-8 作為主動程式碼頁,就像所有與 R 一起運送的前端一樣。這將允許嵌入式 R 在最近的 Windows 系統上使用 UTF-8 作為其原生編碼。
嵌入 R 的應用程式和使用 system
呼叫來呼叫 R(例如 Rscript.exe
、Rterm.exe
或 R.exe
)的應用程式都需要能夠找到 R bin 目錄。最簡單的方法是要求使用者設定環境變數 R_HOME
並使用它,但天真的使用者可能會困惑於如何執行此操作或使用什麼值。
Windows 安裝程式很長一段時間以來都允許將 R_HOME
的值記錄在 Windows 登錄檔中:這是可選的,但預設為選取。記錄位置多年來已經改變,以允許多個版本的 R 同時安裝,並允許在同一台機器上安裝 32 位元和 64 位元版本的 R。
基本的登錄檔位置是 Software\R-core\R
。對於管理員安裝,這在 HKEY_LOCAL_MACHINE
下,在 64 位元作業系統上 HKEY_LOCAL_MACHINE\Software\R-core\R
預設會重新導向到 32 位元應用程式,因此 32 位元應用程式將看到最後一個 32 位元安裝的資訊,而 64 位元應用程式則看到最後一個 64 位元安裝的資訊。對於個人安裝,資訊在 HKEY_CURRENT_USER\Software\R-core\R
下,32 位元和 64 位元應用程式都可以看到,因此會記錄最後一個安裝的架構。為了避免這個問題,有位置 Software\R-core\R32
和 Software\R-core\R64
,它們總是參考一個架構。
當 R 已安裝且未停用記錄時,會在該位置為金鑰 InstallPath
和 Current Version
寫入兩個字串值,且在解除安裝 R 時會移除這些金鑰。為允許保留其他已安裝版本的資訊,還有一個名為類似 3.0.0
或 3.0.0 patched
或 3.1.0 Pre-release
的金鑰,其 InstallPath
值。
因此,用於搜尋 R_HOME
的全面演算法類似於
HKEY_CURRENT_USER\Software
通常會還原至較早版本。對 HKEY_CURRENT_USER
和 HKEY_LOCAL_MACHINE
中的一個或兩個執行下列動作。
Software\R-core\R32
或 Software\R-core\R64
中尋找,如果不存在或架構不重要,請在 Software\R-core\R
中尋找。
InstallPath
存在,則這是 R_HOME
(使用反斜線記錄)。如果不存在,請尋找版本特定金鑰,例如 2.11.0 alpha
,選取最新版本(這本身是一個複雜的演算法,因為 2.11.0 patched > 2.11.0 > 2.11.0 alpha > 2.8.1
),並使用其 InstallPath
值。
跳至: | .
\
A B C D E F G I L M N O P Q R S T U V W |
---|
跳至: | .
\
A B C D E F G I L M N O P Q R S T U V W |
---|
跳至: | .
\
A B C D E F G H I L M N O P R S T U V W Z |
---|
跳至: | .
\
A B C D E F G H I L M N O P R S T U V W Z |
---|
儘管這是一個持續的錯誤用法。它似乎源自 S,其 R 套件的類比正式稱為函式庫區段,後來稱為章節,但幾乎總是稱為函式庫。
這似乎通常用於「標記」格式的檔案。請注意,大多數 R 使用者不知道這一點,也不知道如何檢視此類檔案:macOS 和 Windows 等平台在其檔案關聯中沒有設定預設檢視器。CRAN 套件網頁會將此類檔案呈現在HTML中:所使用的轉換器預期檔案會以 UTF-8 編碼。
目前,頂層檔案 .Rbuildignore 和 .Rinstignore,以及 vignettes/.install_extras。
可能會出現誤判,但到目前為止只看過少數幾個。
至少如果這是在與套件編碼相符的區域設定中完成的。
且 CRAN 所要求,因此由 R CMD check --as-cran
檢查。
沒有 src/Makefile* 檔案。
但 R CMD check --as-cran
會檢查開源套件。
重複定義可能會觸發警告:請參閱 使用者定義巨集。
如果沒有 BugReports
欄位,bug.report
會嘗試從 Contact
欄位中提取電子郵件地址。
CRAN 將它們擴充為例如 GPL-2 | GPL-3
。
即使一個包裝在 \donttest
中。
這包括所有由 library
和 require
呼叫直接呼叫的套件,以及透過 data(theirdata, package = "somepkg")
呼叫取得的資料:R CMD check
會對所有這些發出警告。但有一些較為微妙的用法,它可能無法偵測:例如,如果套件 A 使用套件 B,並使用套件 B 中使用套件 C 的功能,而套件 B 建議或增強套件 C,則套件 C 需要在套件 A 的「建議」清單中。包含檔案中未宣告的用法也不會被報告,也不會報告在「增強」下所列套件的無條件用法。R CMD check --as-cran
將偵測到更多較為微妙的用法。
副檔名 .S 和 .s 來自最初為 S(-PLUS) 編寫的程式碼,但通常用於組譯器程式碼。副檔名 .q 用於 S,它曾經暫時稱為 QPE。
但它們應該使用 DESCRIPTION 檔案中宣告的編碼。
對於實作「C」地區設定的作業系統而言,這項資訊是正確的:Windows 的「C」地區設定概念使用 WinAnsi 字元集。
更精確地說,它們可以包含英文英數字元和符號「$ - _ . + ! ' ( ) , ; = &」。
這兩個可能都不支援特定平台。它們主要用於 macOS,但很遺憾的是,macOS SDK 的近期版本已移除許多對 Objective C v1.0 和 Objective C++ 的支援。
Intel Fortran 編譯器不接受這項資訊。
使用 .hpp 無法保證可移植性。
還有「__APPLE_CC__」,但這表示編譯器具有 Apple 特定的功能,而不是作業系統,儘管基於歷史原因,它是由 LLVM clang
定義的。它用於 Rinlinedfuns.h。
POSIX 術語,GNU make 稱之為「建立變數」。
產生此類檔案的最佳方式是從成功執行 R CMD check
的 .Rout 複製。如果您想另外產生檔案,請使用選項 --vanilla --no-echo 執行 R,並將環境變數 LANGUAGE=en
設為取得英文訊息。請小心不要使用選項 --timings 的輸出(並請注意 --as-cran 會設定該選項)。
例如 https://www.rfc-editor.org/rfc/rfc4180。
建議有大小寫問題的人使用 .rda,因為常見錯誤是將 abc.RData 稱為 abc.Rdata!
對於所有測試過的 CRAN 套件,gz
或 bzip2
都可以大幅減少安裝大小。
「BWidget」仍然在 Windows 上,但「Tktable」不在 R 4.0.0 中。
腳本應僅假設符合 POSIX 的 /bin/sh
– 請參閱 https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html。特別是,不得使用 bash
延伸模組,而且並非所有 R 平臺都有 bash
命令,更別提 /bin/bash 中的命令。所有已知的與 R 搭配使用的殼層程式都支援反引號,但並非所有都支援 ‘$(cmd)’。然而,實際世界的殼層程式並未完全符合 POSIX,因此需要解決遺漏和特殊情況,而 Autoconf 會為您執行此項工作。算術擴充是一個已知問題:請參閱 https://gnu.dev.org.tw/software/autoconf/manual/autoconf.html#Portable-Shell 以了解此問題和其他問題。某些檢查可由 checkbashisms
Perl 腳本在 https://sourceforge.net/projects/checkbaskisms/files 執行,此腳本也存在於大多數 Linux 發行版中,套件名稱為 ‘devscripts’ 或 ‘devscripts-checkbashisms’:較新版本可從 Debian 來源中擷取,例如 https://deb.debian.org/debian/pool/main/d/devscripts/ 中最新的 tar.xz,且 Perl 的最新版本需要此檔案。
https://gnu.dev.org.tw/software/autoconf-archive/ax_blas.html。如果您包含該檔案庫中的巨集,您需要安排將它們包含在套件來源中,以便 autoreconf
使用。
但它在用於產生 CRAN 二進制套件的機器上可用:然而由於 Apple 未提供其系統函式庫的 .pc 檔案,例如 expat
、libcurl
、libxml2
、sqlite3
和「zlib」,它很可能找不到這些資訊。一些替代品可從 https://github.com/R-macos/recipes/tree/master/stubs/pkgconfig-darwin 取得,並安裝在 CRAN 套件建構器上。
檢查 pkg-config
的版本並不明智,因為它有時會連結到 pkgconf
,一個具有不同版本系列的獨立專案。
但並非所有專案在僅安裝靜態函式庫時都能正確執行,因此通常需要依序嘗試 pkg-config --libs
和 pkg-config --static --libs
。
十年前,Autoconf 使用 configure.in:這仍然被接受,但應重新命名,而 autoreconf
(由 R CMD check --as-cran
使用)會報告為此類。
對於使用 autoconf
2.70 或更新版本的人,還有 AC_CONFIG_MACRO_DIRS
,它允許指定多個目錄。
在 POSIX 術語中:GNU make
稱這些為「make 變數」。
至少在類 Unix 上:當建置 Rblas.dll 時,Windows 建置目前將此類相依性解析為靜態 Fortran 函式庫。
https://www.openmp.org/、https://en.wikipedia.org/wiki/OpenMP、https://hpc-tutorials.llnl.gov/openmp/
有一些脆弱的解決方法:請參閱 https://mac.r-project.org/openmp/。
LLVM clang
3.8.0 及後續版本的預設建置支援 OpenMP,但 libomp
執行時期函式庫可能尚未安裝。
在大部分實作中,_OPENMP
巨集具有可對應到 OpenMP 版本的日期值:例如,值 201307
是版本 4.0 (2013 年 7 月) 的日期。然而,這可能用於表示部分支援的最新版本,而非完全實作的版本。
Windows 預設值,而非 MinGW-w64 預設值。
撰寫本文時,使用 GCC、Intel 和 Clang 編譯器。計數可能包括執行主要程序的執行緒。
小心不要將 nthreads
宣告為 const int
:Oracle 編譯器要求它為「左值」。
一些 Windows 工具鏈反而有「_REENTRANCE」的拼寫錯誤。
少數作業系統 (AIX、Windows) 不需要此類程式碼的特殊旗標,但大多數作業系統需要,儘管編譯器在未要求時通常會產生 PIC 程式碼。
Intel 編譯器預設不會,但使用沒有 src/Makefile 的套件時會變通處理。
一些變更連結自 https://isocpp.org/std/standing-documents/sd-6-sg10-feature-test-recommendations:另有其他不建議事項。
值 201103L
、201402L
、201703L
和 202002L
最常分別用於 C++11、C++14、C++17 和 C++20,但有些編譯器設定 1L
。對於 C++23,目前只能假設大於 C++20 的值:例如,g++
12 使用 202100L
,clang++
(LLVM 15,Apple 14) 使用 202101L
。
過去經常用於表示「非 C++98」
請參閱 https://isocpp.org/std/standing-documents/sd-6-sg10-feature-test-recommendations 或 https://en.cppreference.com/w/cpp/experimental/feature_test。假設任何承諾符合 C++14 的編譯器都會提供這些功能,似乎是合理的,例如 g++
4.9.x 有提供,但 4.8.5 沒有。
在使用子架構的系統上,特定於架構的版本(例如 ~/.R/check.Renviron.x64)優先。
適當的 file.exe
是 Windows 工具組的一部分:如果找不到適當的 file
,它會檢查 gfile
:後者可在 https://www.opencsw.org/ 的 OpenCSW Solaris 收藏中取得。原始程式碼存放庫為 http://ftp.astron.com/pub/file/。
對於名稱以「win」或「Win」開頭的子目錄,會產生例外狀況。
在其他大多數平台上,此類執行時期函式庫是動態的,但目前在 Windows 上使用靜態函式庫,因為工具鏈並非作業系統的標準部分。
或如果使用選項 --use-valgrind,或環境變數 _R_CHECK_ALWAYS_LOG_VIGNETTE_OUTPUT_
設為 true 值,或與目標輸出檔案有差異時
為了進行最全面的檢查,這應為 5.8.0 或更新版本:任何 tidy --version
未報告版本號碼的版本都太舊了,包括 macOS 附帶的 2006 年版本。
例如,在 2014 年初,gdata 宣告「Imports: gtools」,而 gtools 宣告「Imports: gdata」。
稱為 CVS 或 .svn 或 .arch-ids 或 .bzr 或 .git(但不是稱為 .git 的檔案)或 .hg。
稱為 .metadata。
這是一個錯誤:GNU make 使用 GNUmakefile。
請參閱 tools:::.hidden_file_exclusions
和 tools:::get_exclude_patterns()
以進一步排除檔案和檔案模式。
並為避免大小寫不敏感檔案系統的問題,請使用所有這些副檔名的字元。
除非在 DESCRIPTION 檔案中使用「BuildVignettes: no」禁止。
只要符合套件授權的條件:許多,包括 CRAN,將遺漏原始程式碼元件視為與開放原始碼授權不相容。
R_HOME/bin
加到 PATH
之前,以便 Makefile 中對 R
或 Rscript
的參照使用目前執行的 R 版本。
請注意,延遲載入的資料集不在套件的命名空間中,因此需要透過 ::
存取,例如 survival::survexp.us
。
它們將以兩個未命名參數呼叫,順序相同。
注意:只有當套件在 R 目錄中包含 R 程式碼時,才會在所有版本的 R 中讀取此內容。
請注意,這是共用物件的基本名稱,並會加上適當的副檔名 (.so 或 .dll)。
這預設為與 exportPattern
相同的模式:使用類似 exportClassPattern("^$")
的內容來覆寫此內容。
如果這樣做,如果類別/方法也已匯入,就會出現關於取代匯入的不明警告。
人們使用 dev.new()
在特定大小開啟裝置:這無法移植,但使用 dev.new(noRStudioGD = TRUE)
有幫助。
Solaris make
不接受 CRLF 結尾的 Makefile;Solaris 會警告,而其他一些 make
會忽略不完整的最後一行。
這顯然在 SunOS 4 中被引入,且在其他地方可用 只要 它被空格包圍。
GNU make、BSD make 和 FreeBSD、NetBSD 中 pmake
的其他變體,以前在 macOS 中,以前在 Solaris 上實作的 AT&T make 和「分散式 Make」(dmake
),Oracle Developer Studio 的一部分,且在其他版本中可用,包括 Apache OpenOffice。
例如,test
選項 -a 和 -e 不可移植,且在 Solaris 10/11 上使用的 AT&T Bourne shell 中不受支援,即使它們在 POSIX 標準中。Solaris 也不支援「$(cmd)」。
從 R 4.0.0 開始,預設為 bash
。
它不在 Bourne shell 中,且不受 Solaris 10 支援。
https://fortranwiki.org/fortran/show/Modernizing+Old+Fortran 可能有助於解釋 gfortran -Wall -pedantic
的部分警告。
這些是可選的,因為對應的類型是,但如果類型是,則必須提供。
或在支援變體 _Exit
和 _exit
的地方。
這和 srandom
在任何情況下都是不可移植的。它們在 POSIX 中,但不在 C99 標準中,且在 Windows 上不可用。
包括從版本 13 開始的 macOS。
在 libselinux 中。
至少是 Linux 和 Windows,但不是 macOS。
除了 download.file()
在非互動式使用中使用的最簡單種類。
GNU 連結器重新排序,因此 -L 選項會先被處理,但 Solaris 的連結器不會。
某些版本的 macOS 沒有。
如果需要 Java 解譯器(不是 透過 rJava),這必須宣告,且其存在必須像任何其他外部指令一樣被測試。
例如,處理「https://」URL 的能力。
在 Windows 上不這麼做是預設值,會被 R 可執行檔覆寫。
這些在「x86_64」上的預設編譯器設定中不需要,但可能在「ix86」上需要。
選取「儲存為」,然後從「Quartz 濾鏡」選單中選取「縮小檔案大小」:這可以用其他方式存取,例如 Automator。
除了可能被用於貨幣符號的一些特殊字元(例如反斜線和井號)之外。
通常在類 Unix 系統上,這是透過告訴 fontconfig
在哪裡尋找適合的字型來選取字形。
它在所有已知的平台上都是如此,並且從 R 4.0.0 開始需要。
Ubuntu 提供 5 年的支援(但人們在 7 年後仍在執行 14.04),而 RHEL 提供 10 年的完整支援,並在延伸支援下提供長達 14 年的支援。
這在 Linux、Solaris 和 FreeBSD 上可以看到,儘管每個都有其他方式來開啟所有延伸,例如定義 _GNU_SOURCE
、__EXTENSIONS__
或 _BSD_SOURCE
:GCC 編譯器預設定義 _GNU_SOURCE
,除非使用嚴格標準,例如 -std=c99。在 macOS 上,除非其中一個巨集被賦予太小的值,否則會宣告延伸。
通常從工具鏈的標頭中取得。
在撰寫「arm64」macOS 時,它同時發出警告,而且不會在 math.h 中提供原型,這會導致編譯錯誤。
也是 C++11 及更高版本的一部分。
通常與 C 編譯器包含的標頭相同,但有些編譯器有某些 C 標頭的包裝器。
雖然這預計會成為 C23 的一部分,但要完全支援它還需要好幾年。
https://stackoverflow.com/questions/32739018/a-replacement-for-stdbind2nd
在系統標頭中允許,但會被忽略。
在使用 macOS 13 SDK,部署目標為 macOS 13 時。
而 DEC
則曾是 DEC Fortran。
請參閱 https://gcc.gnu.org/gcc-10/porting_to.html。
請參閱 https://prereleases.llvm.org/11.0.0/rc2/tools/clang/docs/ReleaseNotes.html#modified-compiler-flags。
原則上這可能取決於作業系統,但已在 Linux 和 macOS 上檢查過。
於 2023 年停用。
在 Fortran 2003 中有一個可攜式的方法可以做到這一點(模組 ieee_arithmetic
中的 ieee_is_nan()
),但 GNU Fortran 的 4.x 版本不支援。一個相當穩健的替代方案是測試 if(my_var /= my_var)
。
例如 \alias
、\keyword
和 \note
區段。
可能會有一些例外:例如 Rd 檔案不允許以點號開頭,而且在不區分大小寫的檔案系統上必須有獨特的名稱。
在目前的區域設定中,並對 LaTeX 特殊字元進行特殊處理,且任何「pkgname-package」主題都移到清單的最上方。
\describe
仍然可以用於更一般的清單,包括當 \item
標籤需要特殊標記時,例如用於元語法變數的 \var
,請參閱 標記文字。
由 R 函式 trimws
定義。
目前僅在 HTML 轉換中呈現不同,以及在「\usage」和「\examples」環境之外的 LaTeX 轉換中。
僅在 \dots
和 \ldots
之間有細微的區別。在程式碼區塊中使用 \ldots
在技術上是不正確的,而 tools::checkRd
會對此發出警告,但另一方面目前的轉換器在程式碼區塊中以相同的方式處理它們,而且除了 LaTeX 中兩者之間的細微區別外,其他地方也是如此。
請參閱檔案 Paren.Rd 中的範例區段以取得範例。
R 2.9.0 新增了對 LaTeX 中 UTF-8 西里爾字元的支援,但在某些作業系統上,這需要將西里爾字元支援新增至 LaTeX,因此環境變數 _R_CYRILLIC_TEX_
可能需要設定為非空值才能啟用此功能。
必須建置 R 才能啟用此功能,但選項 --enable-R-profiling 是預設值。
對於類 Unix 系統,這些是 CPU 時間的區間,而對於 Windows 則是經過時間的區間。
除了下面列出的指令例外:此類名稱的物件可透過明確呼叫 print
來列印。
macOS 支援的是早已過時的版本。
在某些發行版中,例如 valgrind-devel,會個別打包。
在某些數字、邏輯、整數、原始、複雜向量中,以及由 R_alloc
分配的記憶體中。
包括在釋放 R 向量資料區段後使用它們。
例如,在 gfortran
中預設為小型固定大小的陣列。
目前在「x86_64」/「ix86」Linux 和 FreeBSD 上,有些支援 Intel macOS,但不是與 R 一起使用的工具鏈。(有一個更快的變體 HWASAN,僅適用於「aarch64」。)在某些平台上,執行時期函式庫 libasan 需要個別安裝,而對於檢查 C++,你可能也需要 libubsan。
LLVM 專案的一部分,並在 Linux 上以 llvm
RPM 和 .deb
進行散布。Apple 目前未散布。
正如 Ubuntu 所說。
安裝在某些 Linux 系統上,例如 asan_symbolize
,並可從 https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/asan/scripts/asan_symbolize.py 取得:如果可用,它會使用 llvm-symbolizer
。
包括 gcc
7.1 和 clang
4.0.0:對於 gcc
,它是由 -fsanitize=address 暗示的。
例如,Linux 上的 X11/GL 函式庫,在檢查套件 rgl 和使用它的其他函式庫時看到—一個解決方法是設定環境變數 RGL_USE_NULL=true
。
在某些平台上,執行時期函式庫 libubsan 需要個別安裝。
但如果停用內嵌和堆疊指標最佳化,則效果更好。
預設為安全措施:請參閱 man dyld
。
請參閱 https://svn.r-project.org/R-dev-web/trunk/CRAN/QA/Simon/R-build/fixpathR:可以使用「@executable_path」而不是絕對路徑。
可能在進行一些特定於平台的轉換之後,例如加上前導或尾隨底線。
這目前包含在 R.h 中,但未來可能不會包含,因此需要由需要該類型的程式碼包含。
請注意,選項 CBoundsCheck = TRUE
沒有檢查溢位。
嚴格來說,這是特定於作業系統的,但多年來沒有看到任何例外。
對於從名稱空間內部呼叫,搜尋範圍會限制在為該套件載入的 DLL 中。
對於未註冊的進入點,作業系統的 dlsym
常式用於尋找位址。其效能會因作業系統而異,即使在最佳情況下,它也需要搜尋比 .Call
進入點的表格(例如)還要大的符號表格。
由於它是一個標準套件,因此在嘗試在此處重現帳戶之前,需要先將其重新命名。
無論是否使用「LinkingTo」。
因此,需要在 NAMESPACE 檔案中有一個對應的 import
或 importFrom
進入點。
即使在這樣的區塊中包含 C 系統標頭也會導致編譯錯誤。
https://en.wikipedia.org/wiki/Application_binary_interface.
例如,g++
5.1 及更新版本中的「_GLIBCXX_USE_CXX11_ABI」:https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_dual_abi.html。
dyld
在 macOS 上,以及下方的 DYLD_LIBRARY_PATHS
。
也就是說,類似於 1990 年代 S 版本 4 中所定義的:這些沒有持續更新,也不建議用於新專案。
請參閱 R API:C 程式碼的進入點:請注意,這些並非全部都是 API 的一部分。
SEXP 是 Simple EXPression 的縮寫,在類似 LISP 的語言語法中很常見。
如果不需要強制轉換,coerceVector
會將舊物件傳遞,不予變更。
可以使用 defineVar(symbol, duplicate(value), rho)
在環境框架 rho
中指定物件的 副本。
請參閱 字元編碼問題,瞭解為什麼這可能不是必要的。
這只保證顯示目前的介面:可能會變更。
已知的問題已定義 LENGTH
、error
、length
、vector
和 warning
:這些是否重要取決於作業系統和工具鏈,許多問題報告都涉及 clang++
。從 LLVM clang
13.0.0 開始,match
的重新對應會中斷後續包含 omp.h。
在 R 4.2.0 之前的 Windows 上並非如此。
也是 C++11 的一部分。
名稱中的「F77_」具有歷史意義,可追溯到 S 中的使用。
這是 C11 的選用擴充。
https://en.wikipedia.org/wiki/Endianness.
它是由 Intel 編譯器定義的,但也會隱藏未滿足的參照,因此無法與 R 一起使用。AIX 和 Solaris 編譯器不支援它。
這適用於預設 C++ 方言 (目前為 C++11) 的編譯器,而不一定適用於其他方言。
在某些情況下,Fortran 編譯器會接受旗標,但實際上並不會隱藏其符號。
在 macOS 的術語中,這是一個 動態 函式庫,也是在該平台上建置 R 的正常方式。
但這些不是自動化測試程序的一部分,因此測試很少。
至少根據 POSIX 2004 及後續版本。較早的標準規定 sys/time.h:如果定義 HAVE_SYS_TIME_H
,R_ext/eventloop.h 將包含它。
至少在有可用值的平台上,即有 getrlimit
,或在 Linux 上或有支援 KERN_USRSTACK
的 sysctl
,包括 FreeBSD 和 macOS。
1990 年代後期僅使用執行緒的嘗試無法在當時主要的 Windows 版本 Windows 95 中正確執行。