C 不再一種編程語言(cctv5體育節目表)

      網友投稿 697 2025-04-03

      近日,Rust和Swift資深專家Aria Beingessner發布的一篇文章《C 不再是一種編程語言》在Hacker News上引起了熱烈討論。


      Hacker News評論區:https://news.ycombinator.com/item?id=30704642

      Aria和朋友Phantomderp在“對C ABI接口感到非常失望并試圖修復上”達成了高度一致。但在失望的原因上,Aria與朋友各自持不同意見。那具體產生了哪些分歧呢?為什么會提出C不再是一種編程語言的觀點呢?筆者對原文進行了編譯:

      整理 | 于軒

      出品 | 程序人生?(ID:coder _life)

      Phantomderp試圖從原生上改善使用C本身作為編程語言的條件,而Aria則希望改善使用C以外的任何語言條件。

      這時候大家就會產生疑問了,這個問題和C有什么關系?

      Aria表示:如果C真的是一種編程語言,那就和它無關。不幸的是,它并不是。這不是說數十億種實現方式和失敗的層次結構,導致它的定義方式非常糟糕的事實,而是C被提升到一個具有威望和權力的角色,它的統治是絕對和永恒的。C是編程的通用語言,我們都必須學C,因此C不再只是一種編程語言,它成了每一種通用編程語言都需要遵守的協議。

      這實際有點像是關于整個“C是一個不可捉摸的實現定義混亂” 。但僅因為它讓我們不得不使用這個協議,這就變成了一個更大的噩夢。

      外部功能接口

      下面一起來談談技術問題。假如你已經完成了你的新語言Bappyscript的設計,對Bappy Paws/Hooves/Fins有一流的支持。這是一種神奇的語言,將徹底改變cats、sheep、和sharks的編程方式。

      但現在需要讓它真正做一些有用的事情。比如接受用戶的輸入,或者輸出,或者字面上的任何可觀察之類的東西。如果你想讓該語言編寫的程序與主流操作系統兼容,那就需要與操作系統的界面進行交互。聽說Linux上的一切都“只是一個文件”,所以一起在Linux上打開一個文件吧!

      OPEN(2)

      NAME

      open, openat, creat - open and possibly create a file

      SYNOPSIS

      #include?

      int open(const char *pathname, int flags);

      int open(const char *pathname, int flags, mode_t mode);

      int creat(const char *pathname, mode_t mode);

      int openat(int dirfd, const char *pathname, int flags);

      int openat(int dirfd, const char *pathname, int flags, mode_t mode);

      /* Documented separately, in openat2(2): */

      int openat2(int dirfd, const char *pathname,

      const struct open_how *how, size_t size);

      Feature Test Macro Requirements for glibc (see

      feature_test_macros(7)):

      openat():

      Since glibc 2.10:

      _POSIX_C_SOURCE >= 200809L

      Before glibc 2.10:

      _ATFILE_SOURCE

      這是Bappyscript,不是C,那Linux的Bappyscript接口在哪里?

      你說Linux中沒有Bappyscript接口是什么意思?好吧,當然是因為這是一種全新的語言,但你會添加一個,對嗎?那這時你就會發現,你好像必須使用他們給的東西。

      你將需要某種接口,讓語言能夠調用外部的函數,就像外部函數接口FFI。然后你發現Rust也有C FFI,Swift也有,甚至Python也有。

      你會發現,每個人都必須學會C才能與主流的操作系統對話,然后當需要相互對話時,大家突然都用起了C。所以…為什么不直接用C來相互對話呢?

      現在C就變成了一種編程通用語言,不僅是一種編程語言,它還是一種協議了。

      與C對話包括哪些內容?

      很明顯,基本上每種語言都必須學會與C進行對話,而且這種語言絕對是非常明確的。

      "對話 "C是什么意思?它意味著以C頭文件的形式獲得接口類型和功能的描述,并以某種方式:

      匹配這些類型的布局

      用鏈接器做一些事情,將函數的符號解析為指針

      用適當的ABI來調用這些函數(比如把args放在正確的寄存器中)

      那么,這里就有幾個問題:

      你實際上不能寫一個C解析器

      C實際上沒有ABI,甚至沒有定義的類型布局

      實際上無法解析一個C頭文件

      Aria曾斷言解析C基本上是不可能的,但有人說其實有很多工具可以讀取C頭文件,比如rust-bindgen。事實果真如此嗎?其實不然。

      bindgen使用libclang來解析C和C++頭文件。要修改bindgen搜索libclang的方式,請參閱clang-sys文檔。關于bindgen如何使用libclang的更多細節,請參閱bindgen用戶指南。

      任何花費大量時間試圖快速解析C(++)頭文件的人都會很快放棄,然后讓一個C(++)編譯器來做這件事。請記住,有意義地解析C頭文件不僅僅是解析:你還需要解決#includes、typedefs和macros的問題!所以現在不僅要實現所有相關功能,還要實現所有平臺的頭文件解析邏輯,并且還需要想方設法找到DEFINED!

      就拿Swift來說,它在C互操作和資源方面擁有絕對優勢,它是由蘋果開發的一門編程語言,有效取代了Objective-C,成為在其平臺上定義和使用系統API的主要語言。在這樣做的過程中,它比其他任何人都更進一步實現了ABI穩定性和設計概念。

      它也是Aria見過的最支持FFI的語言之一。它可以本地導入(Objective-)C(++)頭文件,并產生一個漂亮的本地Swift接口,其類型在邊界自動 "橋接 "到它們的Swift對等項(由于類型具有相同的ABI,所以通常是透明的)。

      Swift也是由蘋果公司中許多構建和維護Clang和LLVM的人開發。這些人都是C及其衍生品方面的世界頂級專家。Doug Gregor就是其中之一,他曾表達了對C FFI的看法:

      所有這些都是Swift內部使用Clang來處理 C(++) ABI的原因。這樣一來,我們就不會去追著Clang增加的每一個影響ABI的新屬性。

      可以看出,即使是Swift也不想花時間解析C(++)頭文件。那么,如果你絕對不想讓C編譯器在編譯時解析和解決頭文件,你該怎么做呢?

      你需要手工翻譯!int64_t??還是寫i64. long…?什么是long?

      C實際上沒有ABI

      好吧,這沒有什么好驚訝的:C語言中的整數類型,為了 “可移植性”而被設計成搖擺不定的大小,實際上大小也是不穩定的。我們可以認為CHAR_BIT很奇怪,但這也不能幫助我們了解long的大小和對齊方式。

      有人說每個平臺都有標準化的調用約定和ABI,確實有,而且它們通常定義了C中關鍵原語的布局(并且有些不只是用C類型來定義調用約定,這里側眼于AMD64 SysV)。

      還有一個棘手的問題:架構并沒有定義ABI,操作系統也是。我們必須在一個特定的目標三元組上全力以赴,比如 “x86_64-pc-windows-gnu”(不要和 "x86_64-pc-windows-msvc "混淆)。經過測試,一共有176個三元組。

      >?rustc?--print?target-list

      aarch64-apple-darwin

      aarch64-apple-ios

      aarch64-apple-ios-macabi

      aarch64-apple-ios-sim

      aarch64-apple-tvos

      ...

      armv7-unknown-linux-musleabi

      armv7-unknown-linux-musleabihf

      armv7-unknown-linux-uclibceabihf

      ...

      x86_64-uwp-windows-gnu

      x86_64-uwp-windows-msvc

      x86_64-wrs-vxworks

      >_

      這實在是有太多ABI了,因為測試中甚至沒有用到所有不同的調用約定,如stdcall vs fastcall或aapcs vs aapcs-vfp。

      但至少所有這些ABI和調用約定之類的東西,都可以一種方便使用的機器可讀格式獲得。至少主流的C編譯器在特定目標三元組的ABI上達成了一致! 當然有一些奇怪的jank C編譯器,但Clang和GCC不是:

      > abi-checker --tests ui128 --pairs clang_calls_gcc gcc_calls_clang

      ...

      Test ui128::c::clang_calls_gcc::i128_val_in_0_perturbed_small passed

      Test ui128::c::clang_calls_gcc::i128_val_in_1_perturbed_small passed

      Test ui128::c::clang_calls_gcc::i128_val_in_2_perturbed_small passed

      Test ui128::c::clang_calls_gcc::i128_val_in_3_perturbed_small passed

      Test ui128::c::clang_calls_gcc::i128_val_in_0_perturbed_big failed!

      test 57 arg3 field 0 mismatch

      caller: [30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 3A, 3B, 3C, 3D, 3E, 3F]

      callee: [38, 39, 3A, 3B, 3C, 3D, 3E, 3F, 40, 41, 42, 43, 44, 45, 46, 47]

      Test ui128::c::clang_calls_gcc::i128_val_in_1_perturbed_big failed!

      test 58 arg3 field 0 mismatch

      caller: [30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 3A, 3B, 3C, 3D, 3E, 3F]

      callee: [38, 39, 3A, 3B, 3C, 3D, 3E, 3F, 40, 41, 42, 43, 44, 45, 46, 47]

      ...

      392 passed, 60 failed, 0 completely failed, 8 skipped

      上面是Aria在Ubuntu 20.04 x64上運行的FFI abi-checker,她在這個相當重要的、表現良好的平臺上測試了一些非常無聊的情況。結果發現,一些整數參數在兩個由Clang和GCC編譯的靜態庫之間按值傳遞失敗了!

      Aria發現,Clang和GCC甚至不能就Linux x64上_int128的ABI達成一致。

      Aria本來是為了檢查rustc中的錯誤,沒想到會在一個重要的、常用的ABI上發現兩大主流C編譯器的不一致。

      試圖馴服C

      Aria認為,可怕的是對C頭文件進行語義解析,只能由該平臺的C編譯器來完成。即使C編譯器告訴了你類型和如何理解注釋,但實際上你仍然不知道所有內容的大小/對齊/慣例。那如何與這些亂七八糟的東西進行互操作呢?Aria提供了兩種選擇。

      第一個選擇是完全投降,將你的語言與C進行靈魂綁定,這可以是以下任何一種:

      用C(++)編寫你的編譯器/運行時,這樣它就可以用C了

      讓你的 "codegen "直接發出C(++),這樣用戶無論如何都需要一個C編譯器

      將你的編譯器建立在一個成熟的主要C編譯器(Clang或GCC)之上

      但上面這些也只能讓你走這么遠,因為除非你的語言真的暴露了unsigned long long,否則你將繼承C的巨大可移植性混亂。

      這就讓我們想到了第二個選擇:撒謊、欺騙和偷竊。

      如果這一切是無論如何都無法避免的災難,你還不如開始手工翻譯類型和接口定義到你的語言中,基本上就是我們每天在Rust中所做的事情。比如,人們使用rust-bindgen和friends自動化處理一些事,但很多時候,定義會被檢查或手工調整。因為人們不想浪費時間,去嘗試Phantomderp的定制C構建系統可移植地工作。

      在Rust中,Linux x64上的intmax_t是什么?

      pub type intmax_t = i64;

      在Nim中,Linux x64上的long long是什么?

      clonglong {.importc: "long long", nodecl.} = int64

      很多代碼已經完全放棄將C保持在循環中,開始對核心類型的定義進行硬編碼。畢竟,它們顯然只是平臺ABI的一部分!他們要改變intmax_t的大小嗎?這顯然是一個破壞ABI的變化!

      那phantomderp正在研究的又是什么?

      我們討論過為何intmax_t不能被改變,因為如果我們從long long(64位整數)改為_int128_t(128位整數),某個地方的二進制會失控使用錯誤的調用約定/返回約定。但有沒有一種方法,如果代碼選擇了它或其他東西,我們可以為較新的應用程序升級函數調用,而讓舊應用程序保持不變?讓我們編寫一些代碼,測試一下透明別名可以幫助ABI的想法。

      Aria提出了她的疑問:編程語言如何處理這種變化?如何指定與哪個版本的 intmax_t互操作?如果你有一些C頭文件提到intmax_t,它使用的是哪個定義?

      在此討論具有不同ABI的平臺的主要機制是目標三元組。你知道什么是目標三元組嗎?你知道基本上涵蓋了過去20年里所有主流桌面/服務器Linux發行版的 x86_64-unknown-linux-gnu包括什么嗎?現在,雖然表面上可以針對這個目標進行編譯,并得到一個在所有這些平臺上都能“正常工作”的二進制文件,但Aria不相信有些程序會被編譯成intmax_t大于int64_t。

      任何試圖做出這種改變的平臺都會成為一個新的x86_64-unknown-linux-gnu2 目標三元組嗎?如果任何針對x86_64-unknown-linux-gnu編譯的東西都被允許在上面運行,這難道還不夠嗎?

      在不破壞ABI的情況下更改簽名

      "那又怎樣,C永遠不會再有進步嗎?"不!但也是!因為他們提供了糟糕的設計。

      老實說,進行ABI兼容的修改是一種藝術形式。這種藝術的一部分就是準備工作。具體來說,如果你準備好了,做出不破壞ABI的修改就會容易得多。

      正如phantomderp的文章所指出的,像glibc( g 是 x86_64-unknown-linux-gnu 中的 gnu )早就明白了這一點,并使用符號版本化這樣的機制來更新簽名和API,同時為任何針對舊版本編譯的人保留舊版本。

      因此,如果你有 int32_t my_rad_symbol(int32_t)?,你告訴編譯器將其導出為 my_rad_symbol_v1 ,那么任何根據這個頭文件進行編譯的人,都會在他們的代碼中寫上 my_rad_symbol ,但針對 my_rad_symbol_v1 鏈接。

      然后當你決定實際上應該使用int64_t時,你可以把int64_t my_rad_symbol(int64_t)?作為my_rad_symbol_v2 ,但保留舊的定義作為 ?my_rad_symbol_v1。任何針對較新版本頭文件進行編譯的人都會高興地使用v2符號,而針對舊版本進行編譯的人則繼續使用v1!

      但是你仍然有一個兼容性的問題:任何用新頭文件編譯的人都不能與庫的舊版本進行鏈接,庫的V1版本根本沒有V2符號!因此,如果你想獲得熱門的新功能,你就要接受與舊系統的不兼容。

      不過這并不是什么大問題,它只是讓平臺供應商感到難過,因為沒有人能夠立即使用他們花了這么多時間做的東西。你不得不推出一個閃亮的新功能,然后讓大家等待它變得足夠普遍和成熟。但為了人們愿意依賴它并中斷對舊平臺的支持(或者愿意為它實施動態檢查和回退)時,你必須坐等幾年。

      如果你真的想讓人們立即升級,那就要談論向前兼容的問題。這讓舊版本的東西以某種方式與他們沒有概念的新功能一起工作。

      在不破壞ABI的情況下更改類型

      那除了可以改變一個函數的簽名,還可以改變類型布局嗎?Aria表示,這取決于你是如何暴露類型的。

      C真正奇妙的一個特點是,它可以讓你區分一個已知布局的類型和一個未知布局的類型。如果你只在C頭文件中前向聲明一個類型,那么任何與之交互的用戶代碼都不被“允許”知道該類型的布局,并且必須一直在指針后面不透明地處理它。

      所以你可以做一個像MyRadType* make_val()和use_val(MyRadType*)的API,然后使用同樣的符號版本技巧來暴露make_val_v1和 use_val_v1符號,任何時候你想改變這個布局,你就在所有與該類型交互的東西上增加版本。類似地,你在MyRadTypeV1、MyRadTypeV2和一些類型定義中保留了一些,以確保人們使用“正確”的類型。這樣就可以在不同的版本之間改變類型的布局。

      如果多個東西建立在你的庫之上,然后開始用不透明類型相互交談,壞事就會發生:

      lib1: 制作一個API,接受MyRadType*并調用?use_val

      lib2:調用 make_val并將結果傳遞給lib1

      如果lib1和lib2針對庫的不同版本進行了編譯,那么make_val_v1就會被輸入到use_val_v2中!你有兩個選擇來處理這個問題:

      1.說這是被禁止的,責備那些無論如何都要這么做的人,然后傷心

      2.以一種向前兼容的方式設計MyRadType,這樣混合就可以了

      常見的前向兼容技巧包括:

      保留未使用的字段供未來版本使用

      MyRadType的所有版本都有一個共同的前綴,可以讓你“檢查”你所使用的版本

      擁有自定大小的字段,以便舊版本可以“跳過”新的部分

      案例研究:MINIDUMP_HANDLE_DATA

      微軟是這種向前兼容的大師,甚至可以實現在架構之間保持布局兼容。Aria最近正在處理的一個例子是Minidumpapiset.h中的MINIDUMP_HANDLE_DATA_STREAM。

      這個API描述了一個有版本的值列表。該列表以這種類型開始:

      typedef struct _MINIDUMP_HANDLE_DATA_STREAM {

      ULONG32 SizeOfHeader;

      ULONG32 SizeOfDescriptor;

      ULONG32 NumberOfDescriptors;

      ULONG32 Reserved;

      } MINIDUMP_HANDLE_DATA_STREAM, *PMINIDUMP_HANDLE_DATA_STREAM;

      其中:

      SizeOfHeader 是MINIDUMP_HANDLE_DATA_STREAM本身的大小。如果他們需要在最后增加更多的字段,那也沒關系,因為舊版本可以使用這個值來檢測頭的“版本”,也可以跳過任何他們不知道的字段。

      SizeOfDescriptor是數組中每個元素的大小。這讓你知道你有什么 "版本 "的元素,并跳過任何你不知道的字段。

      NumberOfDescriptors?是數組長度

      Reserved是一些額外的內存,無論如何他們決定保留在頭文件中(Minidumpapiset.h非常謹慎,從不在任何地方進行填充,因為填充字節有未指定的值,而且它是一種序列化的二進制文件格式。我希望他們添加這個字段是為了使結構的大小是8的倍數,這樣就不會有任何關于數組元素在標題之后是否需要填充的問題。這是在認真對待兼容性!)

      而事實上,微軟實際上有理由使用這種版本方案,并定義了兩個版本的數組元素:

      typedef struct _MINIDUMP_HANDLE_DESCRIPTOR {

      ULONG64 Handle;

      RVA TypeNameRva;

      RVA ObjectNameRva;

      ULONG32 Attributes;

      ULONG32 GrantedAccess;

      ULONG32 HandleCount;

      ULONG32 PointerCount;

      } MINIDUMP_HANDLE_DESCRIPTOR, *PMINIDUMP_HANDLE_DESCRIPTOR;

      typedef struct _MINIDUMP_HANDLE_DESCRIPTOR_2 {

      ULONG64 Handle;

      RVA TypeNameRva;

      RVA ObjectNameRva;

      ULONG32 Attributes;

      ULONG32 GrantedAccess;

      ULONG32 HandleCount;

      ULONG32 PointerCount;

      RVA ObjectInfoRva;

      ULONG32 Reserved0;

      } MINIDUMP_HANDLE_DESCRIPTOR_2, *PMINIDUMP_HANDLE_DESCRIPTOR_2;

      // The latest MINIDUMP_HANDLE_DESCRIPTOR definition.

      typedef MINIDUMP_HANDLE_DESCRIPTOR_2 MINIDUMP_HANDLE_DESCRIPTOR_N;

      typedef MINIDUMP_HANDLE_DESCRIPTOR_N *PMINIDUMP_HANDLE_DESCRIPTOR_N;

      這些結構的實際細節不是很有趣,除了:

      他們只是通過在末尾添加字段來改變它

      有一個“最新版本”的類型定義

      保留了一些也許再次Padding(填充)(RVA是一個ULONG32)

      這是一個堅不可摧的向前兼容的龐然大物。它們對填充非常小心,它甚至在32位和64位之間有相同的布局 (這實際上是非常重要的,因為你希望一個架構上的minidump處理器能夠處理來自每個架構的minidump)。

      案例研究:jmp_buf

      Aria對這種情況不是很熟悉,但在研究歷史上的glibc中斷時,她在LWN上看到了一篇很棒的文章:《glibc s390 ABI中斷》,她假設它是準確的。

      事實證明,glibc曾經破解過類型的ABI,至少在s390上。根據這篇文章的描述,它是混亂的。

      特別是他們改變了setjmp/longjmp使用的保存狀態類型的布局,即jmp_buf 。現在,他們知道這是一個破壞ABI的變化,所以他們做了負責任的符號版本化的事情。

      但jmp_buf并不是一個不透明的類型,其他東西都在內聯地存儲這個類型的實例,比如Perl的運行時間。不用說,這個相對晦澀的類型已經滲透到許多二進制文件中去了,最終的結論是,Debian的所有東西都需要重新編譯!

      C 不再是一種編程語言(cctv5體育節目表)

      這篇文章甚至討論了將libc版本升級以應對這種情況的可能性:

      在像debian這樣的混合ABI環境中,SO名稱碰撞導致兩個libc被加載并爭奪相同的符號命名空間,而解析(以及因此選擇ABI)則由ELF插值和范圍規則決定。這真是一場噩夢。這可能是一個比告訴大家重建并繼續生活更糟糕的解決方案。

      真的能改變intmax_t嗎?

      在Aria看來,不完全是。就像jmp_buf一樣,它不是一個不透明的類型,這意味著它被內聯到大量的隨機結構中,被認為具有大量其他語言和編譯器的特定表示,并且可能是大量公共接口的一部分。而這些接口并不在libc、Linux,甚至不在發行版維護者的控制之下。

      當然,libc可以適當地使用符號版本技巧來使其API與新的定義兼容,但改變像 intmax_t這樣的基本數據類型的大小,是在一個平臺的大生態系統中尋求混亂。

      Aria希望被證明自己是錯誤的,但據她所知,做出這樣的改變需要一個新的目標三元組,并且不允許任何為舊ABI構建的二進制/庫在這個新三元組上運行。當然有人可以做這些工作,但Aria并不羨慕任何這樣做的發行版。

      即使如此,面臨的還有x64的int問題:這是一個非常基本的類型,而且長期以來一直是這種大小,無數的應用程序可能對它有奇怪的無法察覺的假設。這就是為什么int在x64上是32位的,盡管它應該是64位的:int是32位的時間太長了,以至于完全無望將軟件更新到新的大小,盡管它是一個全新的架構和目標三元組。

      Aria再次希望自己是錯的,但是人們有時犯的錯誤如此嚴重,以至于根本無法挽回。如果C語言是一種獨立的編程語言?當然可以去做。但它不是,它是一個協議,還是我們必須使用的糟糕的協議。

      就算C征服了世界,但也許它再也得不到好東西了。

      Linux Swift

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:企業進銷存庫存表
      下一篇:生產者生態系統(生態系統中生產者的定義)
      相關文章
      亚洲熟妇AV乱码在线观看| 在线a亚洲v天堂网2019无码| 亚洲av一综合av一区| 亚洲人成无码久久电影网站| 毛片亚洲AV无码精品国产午夜| 亚洲中文字幕久久精品无码A| 亚洲avav天堂av在线网爱情| 亚洲成a人片在线观看中文app| 久久久久亚洲精品日久生情 | 久久亚洲精品国产精品黑人| 亚洲精品无码国产| 亚洲大尺度无码专区尤物| 久久亚洲国产成人亚| 亚洲国产精品久久久久| 亚洲成色在线综合网站| 亚洲大片在线观看| 久久久无码精品亚洲日韩按摩 | 国产亚洲女在线线精品| 亚洲国产精品激情在线观看| 国产成人毛片亚洲精品| 中文字幕精品亚洲无线码二区| 国产aⅴ无码专区亚洲av麻豆| 国产亚洲3p无码一区二区| 亚洲VA中文字幕无码一二三区 | 精品亚洲aⅴ在线观看| 亚洲国产精品免费在线观看| 99久久婷婷国产综合亚洲| 亚洲精品一卡2卡3卡四卡乱码| 国产成人精品久久亚洲高清不卡| 亚洲高清偷拍一区二区三区| 亚洲一区无码精品色| 国产aⅴ无码专区亚洲av| 日韩精品一区二区亚洲AV观看| 亚洲国产日韩在线人成下载| 久久亚洲国产最新网站| 国产精品亚洲色图| 亚洲女同成人AⅤ人片在线观看| 亚洲日本乱码在线观看| 亚洲一本综合久久| 77777亚洲午夜久久多喷| 国产精品亚洲综合网站|