WebフォームからSalesforce連携。レコード作成と画像アップロード。
今回やりたいこと
Webフォームでエンドユーザーからデータ入力(テキスト&画像)を受け付ける。
→ Salesforceへ入力データを送信。
テキスト情報からレコードを作成し、アップロードした画像と作成したレコードを紐づける。
※ 本記事は2023/10/04時点での筆者の知る範囲での情報の公開であり、再現性や結果を保証するものではありません。当記事の内容を応用する場合は、ご自身でもDYORの精神で情報収集することを推奨します。
APIの接続方法概要
OAuth認証を使用して接続します。
OAuth認証方法の一覧はこちらにありますので、詳細はそちらをご覧ください。
一般的なOAuth認証
- ユーザがアプリケーションを通してSalesforceのリソースにアクセスしようとすると、アプリケーションはユーザを認証サーバーのログインページに移動します(認証サーバーへのリダイレクト)。
ユーザは認証サーバーへログイン後、アプリケーションがSalesforceのリソースにアクセスすることを認証するかどうかを求められます。 - 1で認証されると、認証サーバーはユーザーを再びアプリケーションのページに案内します(アプリケーションへのリダイレクト)。
その際にアプリケーションは認証サーバーからSalesforceのリソースにアクセスするためのトークンを取得します(認証コードのステップを挟むなど詳細は認証方法によって変わります)。 - ユーザはアプリケーションで任意の操作(データ入力など)を行います。
- アプリケーションはアクセストークンを使用してSalesforcへ接続し、データのやりとり(ユーザの入力データを反映させるなど)を行います。
この仕組みにより、ユーザはアプリケーションに対して自身のログイン情報を直接共有せずに特定のアクセス許可を与えることができます。
また、エンドユーザがSalesforceのユーザであることが前提です。
Salesforceで上記の認証をするにはOAuth 2.0 Web サーバーフローを使うことになります。
その他にユーザ名パスワードフローがありますが、この方法はエンドユーザ情報をアプリケーションが知ることになるので非推奨のようです。
2023年夏からはデフォルト設定では無効になっていました。参考
今回行うOAuth認証、固定のユーザでログイン
今回はWebフォームを入力するエンドユーザーはSalesforceのユーザではない場合を想定するので、事前にSalesforceとの接続に使用するユーザを決めておき、そのユーザ情報をもとにアプリケーション側でSalesforceへアクセスします。
アプリケーションの中をさらに具体的に見ると、エンドユーザーの入力を受け付けるWebフォームをクライアントサイドで実装し、認証サーバーやSalesforceとの接続はサーバーサイド側で扱います。
サーバーサイドで認証関連を行うことで、ユーザ情報をクライアントサイドに公開しないようにします。
設定方法
Salesforceで上記の認証をするには、OAuth 2.0 クライアントログイン情報フローを使用します。
ちなみに今回はこちらから無料アカウントを作成し本番環境で挙動を確かめたので、Sandbox環境だと少し違いが出るかもしれません。
- API用のユーザ(インテグレーションユーザ)を作成します。
- プロファイルの作成。
[Salesforce API Only System Integrations]というプロファイルをコピーし新たにプロファイルを作成します。
作成したプロファイルを編集して、フォームから作成する任意のオブジェクトへの作成権限を有効化します。今回はサンプル(API参照名: Sample__c) というオブジェクトを作ることにします。 [Salesforce API Only System Integrations]はインテグレーションユーザの説明にもあるように、セキュリティの観点からAPIの操作権限を最小化しているものです。 - ユーザを作成し、ライセンスに[Salesforce Integration]を、プロファイルに先ほど作成したものを設定します。
- プロファイルの作成。
- 接続アプリケーション設定をリンク先に従って進めます。
特にOAuth設定の部分では以下がポイントです。
その他の認証方法には、OAuth 2.0 JWT ベアラーフローなどがあります。
画像データの送信方法
画像の扱い方についても先に触れます。
Salesforceで画像を扱うには[ファイル]と[メモ&ファイル]という二つの機能があり、ファイルはLightning Experienceで新しく追加されたものです。
そして、ファイルを操作するには以下のオブジェクトを利用します。
- CotentDocument.
ファイルをアップロードすると自動で作成されるもの。
ファイルそのもののメタデータや属性(例: タイトル、所有者など)を持ちますが、ファイルの実際の内容やバージョン情報は持っていません。 - ContentVersion.
ContentVersionはファイルの実際の内容(バイナリデータ)、そのバージョンの詳細、ファイルのタイプやサイズなどの情報を持っています。 - ContentDocumentLink.
ファイルと他のオブジェクトとのリンク情報を持ちます。
例えばSalesforceの画面上で、オブジェクトAのレコードページの関連リスト「ファイル」にファイルBをアップロードすると、次のステップが実行されます。
- アップロードされたファイルBの情報は
ContentVersion
オブジェクトに保存されます。 - ファイルがアップロードされると、関連する
ContentDocument
が自動的に作成されます。 - そして、このファイルがオブジェクトAのレコードに関連付けられるために、
ContentDocumentLink
レコードが作成されます。
このレコードのContentDocumentId
フィールドは、ファイルBのContentDocument
のIDに設定され、LinkedEntityId
フィールドはオブジェクトAのレコードIDに設定されます。
[このセクションの参考資料]
- Blob(Binary Large OBject)データの送信
- How to Upload a Document in Salesforce Files Using Rest API
- ContentVersionドキュメント
node.jsで簡単な実装
node.jsでサーバーを立ち上げ、実際にSalesforceにアクセスしてみます。
必要な情報
- クライアントキーとクライアントシークレット。
[設定] → [アプリケーションマネージャ]で先ほど作成した接続アプリケーションの[参照]を選択すると、以下のような[コンシューマの詳細を管理]というボタンがあるのでクリックします。 すると、[コンシューマ鍵]と[コンシューマの秘密]という二つの情報が表示されるので控えておきます。 - 環境のドメイン。
[設定] → [私のドメイン]を選択すると私のドメインのURLという情報が表示されるのでこちらも控えます。
コード
npm
もしくはyarn
を使用してモジュールを作成し、server.js
とsalesforceAPI
というファイルを作成します。
server.js
にサーバー処理、salesforceAPI
にsalesforceと連携用の関数を用意します。
必要なライブラリ等はimportを見て適宜インストールしてください。
server.js
app.get(...
の部分では、GETリクエスト時にhtmlを返却する関数を登録しています。
これによりクライアントサイドでWebフォームを形成します。
また、app.post(...
の部分では、WebフォームからPOSTされた情報をsubmitRequestToSalesforce
という関数を使用してsalesforceへ送信する関数を登録しています。
submitRequestToSalesforce
については次のセクションで扱います。
import { submitRequestToSalesforce } from "./salesforceAPI.js"; import express from "express"; // httpサーバーを作成するためのモジュール import multer, { memoryStorage } from "multer"; // multipart/form-dataをパースするためのモジュール(クライアントサイドからのファイルを受け取るために使う) const app = express(); const PORT = 3000; // ファイルをメモリ上に一時的に保存するためのmulterの設定 const upload = multer({ storage: memoryStorage() }); // クライアントサイドからのGETに対してhtmlを返す app.get("/", (req, res) => { res.send(` <form action="/" method="post" enctype="multipart/form-data"> <label for="Name">名前:</label><br> <input type="text" id="Name" name="Name" required><br><br> <label for="file1">ファイル1:</label> <input type="file" name="files"><br><br> <label for="file2">ファイル2:</label> <input type="file" name="files"><br><br> <input type="submit" value="Submit"> </form> `); }); // クライアントサイドからPOSTされた情報をSalesforceへ送信する app.post("/", upload.array("files", 2), async (req, res) => { try { const objectApiName = "Sample__c"; const requestObjectData = { Name: req.body.Name, }; const files = req.files; const success = await submitRequestToSalesforce( objectApiName, requestObjectData, files ); if (success) { res.send("Success"); } else { res.status(500).send("Error occurred while processing your request."); } } catch (error) { console.error(error); res.status(500).send(error.message); } }); app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); });
node server.js
を実行し、http://localhost:3000/にアクセスすると以下のようにフォームが表示されます。
salesforceAPI.js
salesforceAPI.js
では、salesforceとの接続とsalesforceへのデータ送信の二つを行います。
初めに示すコードでは、(salesforceへのデータ送信を扱う)submitRequestToSalesforce
関数の中身は省略しています。
authenticateWithSalesforce
関数では、先ほど設定したクライアントログイン情報フローによる認証(salesforceとの接続)を行なっています。
認証リクエストの返り値のアクセストークンやインスタンスURLを、後ほどsalesforceへのデータ送信に利用します。
ここで、process.env.SF_CLIENT_ID
などの環境変数はdotenvパッケージを使用して.env
ファイルから取得しています。
テストであれば環境変数を使わず直接書き込んでも良いと思います。
# .envファイル SF_CLIENT_ID="事前に控えたコンシューマ鍵" SF_CLIENT_SECRET="事前に控えたコンシューマの秘密" SF_MY_DOMAIN="事前に控えた私のドメイン"
import dotenv from "dotenv"; // 環境変数を扱うためのモジュール import axios from "axios"; // HTTPリクエストを送信するためのモジュール import FormData from "form-data"; // multipart/form-dataにエンコーディングするためのモジュール(Salesforceへのファイル送信に使う) dotenv.config(); let sfAccessToken; // Salesforceから取得したアクセストークン let sfInstanceUrl; // SalesforceのインスタンスURL export const submitRequestToSalesforce = async ( objectApiName, requestObjectData, files ) => { if (!sfAccessToken || !sfInstanceUrl) { await authenticateWithSalesforce(); } ... }; const authenticateWithSalesforce = async () => { // OAuth 2.0 クライアントログイン情報フローを使用: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_client_credentials_flow.htm&type=5 try { const response = await axios.post( `https://${process.env.SF_MY_DOMAIN}/services/oauth2/token`, null, { params: { grant_type: "client_credentials", client_id: process.env.SF_CLIENT_ID, client_secret: process.env.SF_CLIENT_SECRET, }, } ); sfAccessToken = response.data.access_token; sfInstanceUrl = response.data.instance_url; } catch (error) { if (error.response) { console.error("Auth error response:", error.response.data); } else { console.error("Auth request error:", error.message); } } };
次にsubmitRequestToSalesforce
についてです。
事前に整形されたデータがrequestObjectData
、files
として引数で渡ってきます。
初めに(const sfResponse = await axios.post( ...
の部分)アクセストークンやrequestObjectData
を使用してサンプルオブジェクトのレコードを作成します。
返り値からは作成したレコードのIdが取得できます。
次にファイルの数だけループ処理で以下を繰り返します。
ContentVersion
の作成- 1で自動で作成される
ContentDocument
から、レコードとのリンクに使用するidの取得 ContentDocumentLink
を作成して、作成したレコードとファイルを紐づける
export const submitRequestToSalesforce = async ( objectApiName, requestObjectData, files ) => { if (!sfAccessToken || !sfInstanceUrl) { await authenticateWithSalesforce(); } try { // Sampleの作成 const sfResponse = await axios.post( `${sfInstanceUrl}/services/data/v58.0/sobjects/${objectApiName}`, JSON.stringify(requestObjectData), { headers: { Authorization: `Bearer ${sfAccessToken}`, "Content-Type": "application/json", }, } ); const objectId = sfResponse.data.id; for (const file of files) { // ContentVersionの作成 const fileName = file.originalname; const fileContent = file.buffer; let formData = new FormData(); formData.append( "entity_content", JSON.stringify({ ReasonForChange: "Uploaded via form", PathOnClient: fileName, }), { contentType: "application/json", } ); formData.append("VersionData", fileContent, { filename: fileName, contentType: "application/octet-stream", }); const sfResponse2 = await axios.post( `${sfInstanceUrl}/services/data/v58.0/sobjects/ContentVersion`, formData, { headers: { Authorization: `Bearer ${sfAccessToken}`, ...formData.getHeaders(), }, } ); const contentVersionId = sfResponse2.data.id; // ContentVersionに関連するContentDocumentのIDを取得 const versionResponse = await axios.get( `${sfInstanceUrl}/services/data/v58.0/sobjects/ContentVersion/${contentVersionId}`, { headers: { Authorization: `Bearer ${sfAccessToken}`, }, } ); const contentDocumentId = versionResponse.data.ContentDocumentId; // ContentDocumentLinkオブジェクトを作成(ファイルをSample__cオブジェクトに関連付ける) const linkData = { ContentDocumentId: contentDocumentId, LinkedEntityId: objectId, ShareType: "V", }; await axios.post( `${sfInstanceUrl}/services/data/v58.0/sobjects/ContentDocumentLink`, linkData, { headers: { Authorization: `Bearer ${sfAccessToken}`, "Content-Type": "application/json", }, } ); } return true; } catch (error) { if (error.response) { console.error("REST API error response:", error.response.data); } else { console.error("REST API request error:", error.message); } return false; } };
これにより「レコードの作成 → ファイルのアプロード → レコードとファイルの紐付け」が実現できます。
ブラウザからWebフォームを提出すると、
salesforce画面上でレコードが作成されているので(リストを「すべて選択」にしないと見れません)、
中身を確認すると[関連]にファイルも紐づけられています。
[このセクションの参考資料]
まとめ
以上になります!
ちなみに、ここではファイルを扱うのにmulter
・FormData
・axios
等を使用してリクエストを作成しましたが、
別の機会(Next.js
でのアプリ構築)に同じようなリクエスト作成を再現しようとしても400エラー(Multipart message must include a non-binary part)が返ってきてしまいました。
ライブラリによる多少の違いでsalesforceが受け付けてくれないのかもしれません。
その時はこちらに記載されてるリクエストボディを参考にスクラッチでリクエストを作成しました。
メモ
- CURLによる接続方法
- JavaScriptにjsforceというライブラリがあり、簡単に操作できて便利そうです。
ブロックチェーン・トークン・NFTを図で理解というお話。
こんにちわ。42 Tokyoというエンジニア養成機関の 2022 年度アドベントカレンダー 25日目を担当します。
秋山です。
42tokyoを休学している間に, ブロックチェーンを利用したアプリ開発などをインターンで半年ほど経験したので私なりのweb3の理解やトークン・NFTの仕組みを簡単な言葉でまとめます。
はじめに
web3(クリプト)は最近耳にする言葉ではありますが, 実際にどのような世界になるのかは人によって考えが違う, または方向性は同じでも構想の深さが違う物だと思います。
私自身, 思想としても技術としてもまだまだ理解が及びません。
とりあえずは「分散型インターネット・またはその上でアプリケーション・サービスが展開される状態」と認識しています。
いずれにせよブロックチェーンをはじめとする分散型台帳が基盤であることは間違いありません。
まずはその辺りから話し始めます。
間違っている部分があれば, 私の勉強になりますので教えて頂けると嬉しいです。
ブロックチェーン
分散型台帳の代表にはブロックチェーンがあります。
ブロックチェーンは, 「AがBにいくら送金する」などの1つの処理をトランザクションと呼び, それらをブロックという単位にまとめて, ブロック同士が暗号学的なルールに従って連結することで形成されます。
ブロックチェーンデータはノードと呼ばれるコンピュータに保存されます。
各ノードは同じデータを所持しています。
ノードは複数存在しそれぞれが直接通信することでネットワークを形成します。
ブロックチェーンに新しいブロックを追加する際には, あるノードからその他のノードに新ブロックが送信され, 新ブロックがルールに従ったものであるか, 中身のトランザクションは過去の履歴と整合性の取れたものであるかが各ノードによって検証されます。
各ノードの立場は等しく, あるノードによる改ざんは他のノードによって検知されます。あるノードがダウンしても他のノードによって継続されます。
そのため, 誰か(個人や企業)の管理したデータベースと対比して, 一点に権利やリソースが集中していない「分散型台帳」と呼ばれます。
ブロックチェーンはノードを動かし, 新たなブロックを形成する人たちによって成り立ちます。
そのため新しいブロックを追加する作業を行った人には暗号通貨を報酬として渡します。
ユーザがブロックチェーン上で取引を行うにはそのチェーンの暗号通貨が必要になるため, ブロックチェーンが普及するにつれその通貨の価値は上がります。
通貨報酬はブロックを形成する人たちのインセンティブとなります。
なのでブロックチェーンは暗号通貨ありきで存在するものです。
暗号通貨の実体はブロックチェーン上に記録された数値です。
過去の履歴から, どのアドレス(ブロックチェーン上ではユーザをアドレスという数字で区別しています)が数値をいくつ持っているかが算出できるようになっています。
またブロックチェーンにおいて, どのノードがブロックを追加するのかの決め方(コンセンサスアルゴリズムと呼ばれます)にはProof of Work や Proof of Stake があります。
データを改ざんするユーザを生み出さないための仕組みとしてとても重要なものです。
ビットコインが採用しているProof of Workについてはこちらの動画がわかりやすかったです。
Proof of Workにおいてトランザクションがどのように処理されるのかについてはこちらの記事がわかりやすかったです。
ブロックチェーンへのアクセス
ブロックチェーンにトランザクションを提出する(誰かに通貨を送金するなど)には署名という作業が必要で, ブロックの検証にはブロック内の署名が本人のものか検証されます。
そこには秘密鍵と公開鍵と呼ばれるものが必要です。
秘密鍵で署名を行い, 検証には公開鍵が使われます。また, 公開鍵を元にアドレスと呼ばれるユーザの識別子が生成されます。
このようにブロックチェーンへのアクセスには秘密鍵と公開鍵が必要で, その2つを管理するツールをウォレットと呼びます。
ウォレットを使用してブロックチェーンにアクセスすることで, ブロックチェーン上の自分の残高の確認や送金作業が可能になります。
ノードを所持していない場合, トランザクションの提出や閲覧はいずれかのノードとHTTP通信を介して行われます。
スマートコントラクト
ビットコインとブロックチェーンの誕生の後に, ブロックチェーンをアプリに利用できるプラットフォーム「イーサリアム」が誕生しました。
イーサリアムやその他のweb3と呼ばれるブロックチェーンには暗号通貨の送受信のみでなくプログラムをブロックチェーン上に記録することができます。
この仕組みによりあらゆるサービスを分散化されたインターネットで展開できるということになります。
スマートコントラクトの実体はプログラムであり, プラグラミングのご経験のある方はクラスと考えるとわかりやすいです。
トークン
貨幣と銀行のような役目を果たすプログラムです。
コントラクト内には, トークンの総量や, アドレスに対してこのトークンをいくら所持しているのかなどの情報を記録しています。
またトークンの送金や残高照会, 発行などの機能を関数として持っています。
例えばゲームのコントラクトとトークンのコンラクトを使えば, ゲーム内の通貨としてトークンを利用でき, そのトークンはブロックチェーン上の他のコントラクトともやり取りが可能です。
上に示したのは一般的なトークンコントラクトの機能の抜粋であり, コントラクトの仕様は作成者の自由です。
基本的な規格(有名なものでERC20)に沿った機能のみ搭載しても良いですし, ゲーム内の経済を調整するためにトークン発行量を変更可能にしておくこともできます。
しかしブロックチェーン上のデータは閲覧可能なので, 誰かに偏って利益になるような仕組みのコントラクトは利用されづらいでしょう。
ブロックチェーン上に自由に展開されるトークンに対し, ブロックチェーンのエコシステムを回すのに使用されているトークン(イーサリアムならETH)をネイティブトークンや暗号通貨と呼び区別しているみたいです。
トークンは作成者や管理者が総量や供給量をコントロールできる(実装による)点がネイティブトークン(暗号通貨)との違いです。
NFT
Non Fungible Tokenの略です。トークンの一種です。
NFTに対し先ほどお話ししたトークンはFungible Tokenなので, ここではFTと呼びます。
NFTはトークンそれぞれを番号で区別しています。
例えば, FTもNFTも総量が50ある場合を考えます。
FTは50ある各トークンを区別していないため, アドレスAが20, アドレスBが10といった具合に各アドレスの残高を数量で管理するだけです。
数量は貨幣が行う物の価値の「測り」を実現します。
一方NFTは50ある各トークン全てを1~50の番号で区別しているため, アドレスAは1番, アドレスBは5番と23番といった具合に各アドレスの保持するトークンを番号で管理します。
さらに各トークンにはURL情報を紐付けられるようになっており, URLに使われるのは画像のリンクなどです。
画像自体をブロックチェーンに保存するのはコストが高くなってしまい, 通常画像は従来のサーバーなどに保存しておきます。
NFTはコントラクト内で固有の番号をアドレスに割り当てます。
そしてブロックチェーン上に存在するコントラクトには固有のアドレスが割り当てられています。また, ブロックチェーンも種類はありますが同じものはこの世に存在しません。
つまり, NFTはこの世で固有の番号をアドレスに割り当てます。
例えばNFTを権利として扱うと, 何かのデジタルデータの所有権(紐づいているデジタルデータ自体はこの世に1つかわからないがNFTはこの世に1つ)となったり, イベントの参加権となったり, 証明書の役割を果たしたりします。
NFTはトークンでありコントラクトの関数を介して他のアドレスに譲渡可能なので, 権利の取引をするアプリケーションも展開可能です。
特にweb3で生まれるサービスは, ブロックチェーンの性質上, 既存企業のように独占するデータがないため, 別のサービスとの相互作用が前提で動いているものが多いと思います。
あらゆるサービスが同じインフラ(ブロックチェーン)上で動いています。
そんな中NFTはいろんな価値体験に繋げられるかもしれません。
ビットコインとweb3
基本でもありわかりやすいため冒頭はビットコインのブロックチェーンの話も出ましたが, ビットコインとweb3と呼ばれる領域やその通貨たちは別物です。
中央管理体の存在なしに希少性を維持できる史上初のデジタル通貨として, ビットコインはその地位を確立しています。
その技術をもとに, アプリケーションに活かすなどの動きから派生したものがweb3と私は理解しています。
ビットコインにはサービスの展開や独自トークンの発行というweb3特有の考え方はありません。
ビットコインの概念についてはこちらがスラスラ読めてわかりやすかったです。
まとめ
デジタルデータが分散的に管理されていることの重要性は, 現状に不満がない場合は気づきにくいのかもしれません。
例えばSNSでアカウントがバンされる経験をしたり, 中央管理体による支配を実感する生活のもとではまた感じ方が違うはずです。
一方である程度管理されているという状態は楽な気もしています。
暗号通貨で送金ミスをしてしまっても, 仲介しているのはプログラムくらいしかいないので文句を言う相手はいません。自己責任です。
分散型インターネットが普及する場合は, ユーザの責任が多くなり自分で管理することも増えるのでその辺りの知識を広めることと, ブロックチェーンにアクセスするインタフェースの質の向上が大切そうです。
私がこの領域で面白いと思うことは, 技術開発の速さです。
例えば
などがあります。
web3はこれから普及するのかどうかの話に関わらず, 開発とその利用がしばらく進んでいく気がするので, どうなっていくのかまたは自分は何ができるのかこれからが楽しみです。
以上で自分の頭の中のものをムリクリまとめましたが読んでいただきありがとうございました。メリクリです。
レイトレーシングに挑戦しましたというお話。
はじめに
レイトレーシング法を使って空間上の物体を描画するプログラムを作りました.
言語: C
グラフィックツール: X-Window
ライブラリ: MinilibX
その他: ペアで開発
描画する物体など, 空間上の情報はrtファイルというファイルから受け取ります.
- rtファイルの形式例
A 0.2 255,255,255 //環境光, 光の強さ(0~1.0), 色(rgb) L 20,21,-20 0.7 255,255,255 //光源, 位置(xyz座標), 光の強さ, 色 C 20,10,20 -1,-0.5,-1 70 //カメラ, 位置, 向き, 視野角度 pl 0,0,0 0,1,0 0,255,255 //平面, 平面上の点位置, 法線の向き, 色 sp 0,0,0 10 255,0,0 //球体, 中心点位置, 直径, 色 cy 0,0,0 1,0,0 0.5 10000 255,120,120 //円筒, 中心点位置, 中心線向き, 直径, 高さ, 色
レボジトリ
github.com参考資料
丁寧に解説されていて本当に助けられました, ありがとうございました.
knzw.tech
本記事では考え方の概要と,
レイトレースの知識を活用して螺旋状に球体を並べる(本記事トップの画像のような)
通称螺旋丸の作り方も紹介したいと思います.
何か間違っている部分がありましたら, ご指摘頂けると嬉しいです.
概要
下図は球体をレイトレースで表現する際のイメージ図です.
カメラ, 球体, 床, 光源があり,
カメラ前方には最終的な出力画像と対応した仮想的なスクリーンがあります.
画像基板の各ピクセル(に対応するスクリーン上の点)に対して
以下の処理をすることで色を決定し画像を生成します.
また, 半直線や光線のことをレイといいます.
- カメラからスクリーン上のあらゆる点に対してレイ(カメラレイと呼ぶ)を飛ばす
- カメラレイが物体と交差する場合は, その交点から光源に向けてレイ(ライトレイと呼ぶ)を飛ばす
- 各点の色を決定する
点A: カメラレイが物体と交差しないので背景色
点B: カメラレイが球と交差, ライトレイを飛ばす, ライトレイの角度的に明るめの球の色
点C: カメラレイが球と交差, ライトレイを飛ばす, ライトレイの角度的に暗めの球の色
点D: カメラレイが球と交差, ライトレイが球自体に遮られる, 陰の色
点E: カメラレイが床と交差, ライトレイが他の物体(球)に遮られる, 影の色
現実世界では, 光源からの光が物体に反射してカメラに届くことで物体を映せるわけですが,
レイトレーシング法では逆にカメラから光線を追っていきます.
カメラに届く光のみを考えた方が楽だからです(だと思います).
レイをトレース(追跡)してシュミレーションするのでレイトレースと言います.
上記では「ライトレイの角度的に明るめの色」など抽象的な説明をしていましたが,
実際はPhongの反射モデルを用いて色を算出しました.
レイと物体の交差判定
例として, レイと球の交差判定について考察したものを下図に載せます.
図中の上半分は, レイの式①と球体の式②について連立方程式を解いています.
下半分は, 得た式からレイと球の交差する条件について考えています.
他の物体に関しても同じように, レイの式と物体を表す式をもとに交差する条件について考えます.
(カメラから飛ばす)レイの導き方
やること
スクリーン上の点Pにレイを飛ばす場合を考えます.
点Pcam(カメラ位置)から点C(スクリーン中心), 点Cから点Pへのベクトルを追っていくと,
レイを表す式を導き出せます.
図中のdx, dyは平面上でそれぞれx軸正方向, y軸正方向の単位ベクトルです.
目標として表したレイの式について,
既にわかっているベクトルVcam(カメラの向き)以外を求めていきます.
スクリーン上の座標(xs, ys)の求め方
画像上のあるピクセルについてレイを考えているとして,
スクリーン上の座標(xs, ys)を画像上の座標(xi, yi)で表すのが目標です.
下図のように画像とスクリーンの縦横の幅をこちらで決めてしまえば式を導き出すことができます.
画像とスクリーンは全く同じ縦横の幅, 座標で考えると簡単ですが
画像の大きさを変えられなくなってしまうので縦横の相似関係のみ維持するように考えます.
画像を大きく設定するとピクセルの量が増え, 解像度が変化します.
また, スクリーンの大小はカメラからの距離が変化するだけでレイの方向は変わりません.
(次の項目の図を見ると想像しやすいかもしれません).
スクリーンまで距離tの求め方
ここでカメラの視野角θ, カメラの向きベクトルVcam, スクリーンの幅Ws(前項により)は既知です.
三角関数から以下のように求められます.
スクリーン上の単位ベクトルdx, dyの求め方
dx, dyはカメラの向きベクトルVcamに垂直な面において, 互いに垂直に交わるベクトルです.
つまりVcamとdx, dyの3つのベクトルはそれぞれが垂直に交わることになります.
垂直に交わるベクトルは外積なるものを使って求めます.
流れは以下です.
dxを求めるために用意したベクトル(ey)をy軸正方向のベクトルにしたので,
スクリーンの上方向は常にy軸正方向になります(と思います).
今回のカメラはその辺の角度が決まっていないのでこれでいいかな程度で実装を終えています.
※カメラの向きベクトルがeyと同じ場合については考慮しなければいけません
螺旋丸の作り方
螺旋状に変化する球体の中心座標の導き方をご紹介します.
ここまでで, 互いに垂直なレイ・dx・dyベクトルを使って3次元空間上で平面座標を扱えることがわかりました.
また, 平面上で円周上に球体を移動させて, さらに平面自体も移動させると螺旋状に球体が動きます.
下図は左半分が平面上で移動させる球体の様子, 右半分がさらに平面自体も移動させた様子をイメージしたものです.
平面上での球体の位置はrとθを使って表せます.
平面の中心位置は, レイの開始位置とレイの方向ベクトルにtをかけることで表せます.
つまり処理の流れは以下です.
- レイ(螺旋の中心線)の開始位置と方向ベクトル, r・θ・tの初期値, 球体の直径などを決める
- 球体の中心座標を求める
- θとtを一定の変化率で変化させる
- 2と3を繰り返す
処理3でθとtのみではなく, rなど他のパラメータを変えるとまた面白い動きが見れそうです.
プログラムの流れ
おまけで載せさせてください.
さいごに
現実世界の現象をコンピュータ上でシュミレーションできた実感がとても面白かったです!
もちろんその仕組みを知れたことも面白かったです.
今回の知識が何に役に立つかはわかりませんが, 来たるXRの世界を少しでも理解できたらいいなと思います.
一緒に実装してくださったfyutaさんありがとうございました.
哲学者に食事をさせましたというお話。
スレッドとミューテックスを使って食事する哲学者の問題をシュミレーションしました.
シュミレーションルール
- 全ての哲学者が食事をする必要があり, 一人でも一定時間内に食事できない(死亡)とシュミレーションを終了する.
- 各哲学者は互いに会話することができないため, 他の哲学者が死亡することを予測することはできない.
- 各哲学者は以下のいずれかの状態でいる.
- eating(フォークを両手に取っている)
- sleeping(フォークは両方置いている)
- thinking(フォークを取ろうとする)
eating->sleeping->thinking->eating->...の順で状態が変化する.
- 以下の情報を引数で受け取る, 各時間の単位はms.
- number_of_philosophers : 哲学者(またはフォークの)数
- time_to_die : 最後の食事(またはシュミレーションが始まって)から次の食事までにこの時間を過ぎると哲学者は死亡する
- time_to_eat : 食事にかかる時間
- time_to_sleep : 睡眠にかかる時間
- number_of_times_each_philosopher_must_eat : (オプション)、全ての哲学者がこの回数以上食事をするとシュミレーションは終了する, 指定されなければ誰か一人が死んだ時のみ終了する.
- 哲学者は1から順に(時計 or 反時計回りで)番号を振られる.
- 各哲学者の状態が変化する時にはタイムスタンプと共に内容を表示する必要がある.
- timestamp_in_ms X has taken a fork
- timestamp_in_ms X is eating
- timestamp_in_ms X is sleeping
- timestamp_in_ms X is thinking
- timestamp_in_ms X died
- 哲学者はそれぞれスレッドで動かし, ミューテックスを使ってフォークの取り合いを制御する.
サンプルからの実装例
サンプルプログラムを書きながら実装例まで見ていきます.
各関数や用語の説明は省略します.
スレッド作成
以下のコードでスレッドの作成と挙動を実験します.
#include <stdio.h> #include <pthread.h> //スレッドに渡す関数 //引数の中身を出力 void *thread_func(void *thread_data) { int *num; num = (int *)thread_data; printf("thread_func: %d\n", *num); return (thread_data); } //thread_funcとnumを引数にスレッド作成 //numをインクリメント後, 中身を出力 int main(void) { int num; pthread_t thread_id; num = 0; pthread_create(&thread_id, NULL, &thread_func, &num);//スレッド作成 while (num < 50000) num++; printf("main: %d\n", num); pthread_join(thread_id, NULL); return (0); }
実行結果
thread_func: 2358 main: 50000
thread_funcの出力結果は毎度変わります.
つまり, main関数のnumインクリメント中に,
別スレッドのthread_func関数の出力が行われいてることが分かります.
実際-g -fsanitize=thread
というフラグを付けてコンパイルしてから実行するとデータ競合の発生が分かります.
ミューテックス追加
次にミューテックスを利用してデータ競合を防いでみます.
ポイントはnumの操作・出力の前後にミューテックスのロック・アンロックを挟んでいるところです.
#include <stdio.h> #include <pthread.h> pthread_mutex_t mutex; //追加 void *thread_func(void *thread_data) { int *num; num = (int *)thread_data; pthread_mutex_lock(&mutex); //追加 printf("thread_func: %d\n", *num); pthread_mutex_unlock(&mutex); //追加 return (thread_data); } int main(void) { int num; pthread_t thread_id; num = 0; pthread_mutex_init(&mutex, NULL); //追加 pthread_create(&thread_id, NULL, &thread_func, &num); pthread_mutex_lock(&mutex); //追加 while (num < 50000) num++; printf("main: %d\n", num); pthread_mutex_unlock(&mutex); //追加 pthread_join(thread_id, NULL); pthread_mutex_destroy(&mutex); //追加 return (0); }
実行結果
main: 50000 thread_func: 50000
今回は出力が同じになり, データ競合も確認できませんでした.
main関数でのnumインクリメント中は, 別スレッドのthread_func関数の処理はミューテックスにより待機していたことが分かります.
実装例
スレッドとミューテックスの使い方がわかったので実装例を書きます.
例でも実際に書くと長くなってしまうので, 超省略版を載せます.
※実際に書くときは各関数のエラー処理も忘れずに
//philosopher(哲学者)のスレッドが使う関数群 void eat() { pthread_mutex_lock(right_fork); pthread_mutex_lock(left_fork); /* 食事 */ pthread_mutex_unlock(right_fork); pthread_mutex_unlock(left_fork); } void sleep() { /* 睡眠 */ } void think() { /* 思考 */ } //philosopherのスレッドに渡す関数 void *philo(void *) { while (1) { eat(); sleep(); think(); } } //philosopherのスレッドを(死んでいないか)監視するmonitorスレッドに渡す関数 void *monitor(void *) { if (is_died) { /* 死亡 */ } } int main(void) { while (number_of_philo) { pthread_create(philo); pthread_create(monitor); } }
まとめ
今回はざっくりとしたお話になってしまいましたが,
スレッドという概念に触れられてとても勉強になりました!!
Go言語初めの一歩踏んでみましたというお話。
はじめに
こんにちは, 42tokyo Advent Calendar 2021の22日目を担当する, 在校生のrakiyamaです。
この記事では, Go言語の学習で私が躓いたことについてまとめました.
環境 : Mac
現在のGoの最新バージョン : 1.17
Goのインストールはbrew install go
で行いました(その他インストール方法参考).
本記事で載せている参考リンクは英語の記事もありますが(気合い(DeepL)で読むと意外と読めます),
探せば日本語版もあるかもしれません.
基本用語
ダウンロード
意味: 対象物を持ってくる.
例: 利用したいプログラムのソースコードを自分のパソコンにコピーする.インストール
意味: 対象物を実行できるようにする(この過程でダウンロードやコンパイルを含むこともある?, 目的はあくまで実行できるようにすること).
例: バイナリファイル(実行形式のプログラム)を適切なディレクトリ(/binなど)に配置する.バージョン
Goの話でバージョンという言葉が出たら以下を指すことが多いみたいです.- Go言語自体のバージョン
- レポジトリやモジュールのバージョン
※レポジトリとモジュールがよく分からなければひとまずレポジトリは忘れて, モジュールを"プログラムの一つのまとまり"として先に進みましょう.
仕様に変更が加わってGo1.10からGo1.11になりましたよ〜っていう感じで更新を識別するための表記のことですね.
GOPATH
パッケージ
GOPATHとモジュール
Go言語ではファイルの管理方法が定められていて, 以下の二つがあります.
- GOPATHなるものを基準にする方法(GO1.10まで)
- モジュールなるものを基準にする方法(GO1.11から徐々に標準となった)
Go言語で開発する際のルールと捉えています.
私はこの管理方法がバージョンによって変わることを知らなかったので, 学習を進める中でめちゃんこ混乱しました.
仮にここではGOPATHに従う方法をGOPATHモード, モジュールに従う方法をモジュールモード(公式ではmodule-aware modeとか書いてあります) として話をします.
GOPATHモード
GOPATHモードでは, 以下のようなディレクトリ構造をGOPATH配下に作成し, ファイルを管理します.
このディレクトリをルートディレクトリとして作業を進めるため, ワークスペースとも呼ぶみたいです.
GOPATH/ bin/ バイナリファイル pkg/ パッケージのオブジェクトファイル src/ ソースコード (全てのソースコードはここに)
開発を進める際のルートディレクトリは常にGOPATHであり, ソースコードはsrcディレクトリの中で管理します.
つまりGOPATHの中でのみ開発を進めるという特徴があります.
「ワークスペースのsrc内にファイルを作成しましょう」みたいな話が参考サイト等で出てきたら,
GOPATHモードが前提の話だと思います.
実際どのようにプログラムを作成するのかは以下リンクが分かりやすいです.
go.dev
モジュールモード
ここで新たにモジュールという概念が出てきます.
- モジュール
- 1つ以上のパッケージをまとめたもの.
実体は, パッケージ + バージョン管理情報.- バージョン管理とは
依存関係にあるモジュールのバージョンや
使用しているGoのバージョンの情報を
(ファイルに明示的に記述しておくなどして)管理しておくこと.
- バージョン管理とは
- 例として, 先ほどのパッケージをモジュールでまとめたイメージを載せます.
ポイントはバージョン管理によりモジュールの依存関係がまとめられていることです. 仮に作ったstringmanipモジュールのバージョンを0.0.0とすると,
stringmanipモジュール0.0.0のビルドの再現性がバージョン管理によって確保されています.
GOPATHモードではこのバージョン管理の仕組みが標準で用意されていなく苦労してたみたいですが, どのように対処していたのか詳しくはわかりません.
- 1つ以上のパッケージをまとめたもの.
モジュールモードでは, ファイルをパッケージにまとめ,
さらにパッケージをモジュールとしてまとめてプログラムを作成します.
ルートディレクトリにgo.modというバージョン管理ファイルを用意すれば,
そのディレクトリ自体はどこに位置しても大丈夫です(goコマンドが使えるところならおそらく).
要はGOPATHモードに比べて,
バージョン管理の仕組みが用意され, 開発に関しても任意のディレクトリで行えるようになったので,
便利になったということです.
ちなみにGOPATHは引き続きインストール先などで利用されています.
参考リンク GOPATH に(可能な限り)依存しない Go 開発環境(Go 1.15 版)
モジュールのディレクトリ構造
ここでは先ほどのモジュールのイメージを元に実際のディレクトリ構造を載せます.
とてもざっくり説明なので, 詳しくは以下のリンクを参考にしてください.
go.dev
自作パッケージを用意して, main.go内で使う
↓ディレクトリ構造
. ├── convertstrings //自作パッケージconvertstringsを含んだconvertstringsディレクトリ │ ├── lower.go │ └── upper.go └── main.go
↓バージョン管理ファイル(go.mod)作成
//ターミナル $> go mod init stringmanip(モジュール名) $> tree . ├── convertstrings │ ├── lower.go │ └── upper.go ├── go.mod └── main.go
↓自作パッケージ(convertstrings)のimport
//main.go package main import "stringmanip/convertstrings" //使いたいパッケージのディレクトリパスを指定 //go.modのある場所をモジュール(stringmanip)全体のルートディレクトリとして考える func main () { convertstrings.Upper("string") }
外部パッケージも使う
↓外部パッケージのimport
//main.go package main import "stringmanip/convertstrings" import "github.com/example/language/translate" //リモートレポジトリから使いたいパッケージまでのディレクトリパス指定 func main () { convertstrings.Upper("string") translate.Spanish("string") }
↓go.modの編集, go.modのあるディレクトリで以下を実行
//ターミナル $> go mod tidy
main.go内でパッケージ名とそのディレクトリ名が同じで分かりにくですが,
importパスで指定しているのはディレクトリで
convertstrings.Upper()
のように使用する際に先頭に付けているのはパッケージ名です.
モード操作
現在のモードの確認
$> go env | grep GO111MODULE GO111MODULE="on" #常にモジュールモード GO111MODULE="off" #常にGOPATHモード GO111MODULE="auto" #go.modファイル(バージョン管理情報をまとめたファイル)がカレントor親ディレクトリにある時のみモジュールモード
モードの変更
$> go env -w GO111MODULE=auto # autoに変更
Goについて参考になった情報
困ったらこれ
Documentation - The Go Programming Language
一つ一つのトピックがシンプルにまとめられていて非常に分かりやすいです.コードの書き方
- Go の最初の手順 - Learn | Microsoft Docs
私はこちらを参考にコードの書き方を学びました.
日本語で基本的文法などはわかりやすいですが,
GOPATHとモジュールが混ざっているのでその辺ややこしいです. - A Tour of Go
- Hello, World - Learn Go with tests
- Go の最初の手順 - Learn | Microsoft Docs
その他
Go のモジュール管理【バージョン 1.17 改訂版】
Go言語でハマったことメモ(ポインターのアロー演算子がない) - 新しいことにはウェルカム
golangではスタックとヒープを気にする必要が無い
まとめ
ファイルやディレクトリ構成はHow to Write Go Code - The Go Programming Languageを参考にモジュールで考えれば良いんだなーということが伝われば幸いです!!
どこか間違った点があればご指摘頂けると嬉しいです。
明日はrsudoさんがイケイケ開発環境構築について記事を書いてくれる予定です!お楽しみに!
シェルの再実装しましたというお話。
何を作ったか
bashの機能(制限あり)を備えたプログラムを作りました。
言語 : C
実行ファイル名 : minishell
できること
- パイプライン、クォート、リダイレクト、ヒアドキュメント、環境変数
minishell$ cat << EOF | more minishell$ echo "current path is $PWD" > file minishell$ echo '$USER (not expanded)' >> file minishell$ 0<file cat 1>file minishell$ ./put_something_to_fd42 42>file
- ビルトイン関数もいくつか実装
echo , cd , pwd , export , unset , env , exit - シグナルハンドル
ctrl-C ctrl-D ctrl-\ - 終了ステータス
echo $?
で最後のコマンドの終了ステータスを参照できます。
開発の流れ
42tokyoの学生rsudoさんと以下のツールを使って開発しました。
初めは以下の資料を見る、bashを触る、とりあえずコードを書いてみるを繰り返しました。
Bash Reference Manual
オープンソースアプリケーションのアーキテクチャ
ある程度把握できたところで、データ構造を決定しコードを書いていきました。
プログラムの流れ
構文解析などはrsudoさんの記事が参考になるかと思います。テストをする上でのTipsも載っています。
細かな実装の話
ビルトイン以外のコマンド(catなど)実行に使うexecve関数・fork関数や、
入出力処理(レダイレクションやパイプラインの処理)で使うdup系関数・pipe関数の使い方はパイプを実装してみたというお話 - rakiyama0229のブログで書いています。
親プロセスで処理するか、子プロセスで処理するか
- ビルトイン以外のコマンドはサブシェル(子プロセス)で処理
cat file
execve()を使用して実行->成功すると実行プロセスが終了するので子プロセスで処理します。 - コマンド行にパイプが存在するなら、パイプ区切りでそれぞれ子プロセスで処理
echo aaa | cat -e | cat -n
shellが行っているようにマルチプロセスで実行するためです。
コード例:
while(コマンドの数だけ) { pipe(pipefd); pid = fork(); if (pid == 0) /* 前のpipefd-(入力)-> コマンド実行 -(出力)-> 新しいpipefd */ } while(プロセスの数だけ) wait(NULl);
- 単一のビルトインコマンドはminishellのプロセス(親プロセス)で処理
export ENV=val
cd ..
echo rakiyama > file
などなど
exportやcdなど結果を親プロセスに反映したいためです。
ヒアドキュメント
コマンド行にヒアドキュメントがあれば、(リダイレクトやコマンド実行処理に入る前に)全てのヒアドキュメントに対する入力内容を受け取ります。
cat << EOF | cat << EOF | cat << EOF
ヒアドキュメントの内容はPIPE(pipe()で生成するPIPE)に書き込み、その後のリダイレクト処理ではPIPEのFD(ファイルディスクリプタ)を参照するようにします。
子プロセスで入力・書き込み処理をした理由は、
ヒアドキュメントを受け取っている最中のシグナルハンドラは親プロセスと違ったのでシグナルハンドラを変えた別のプロセスで処理するようにしたかったからです。
リダイレクション
dup2()を使い、FDが指定されていれば(3>file
)そのFDで複製、指定がなければ(>file
)デフォルト値(0か1)で複製します。
out_fd = open("file name", O_WRONLY | O_CREAT | O_TRUNC, 0666); dup2(out_fd, FD);
- 親でリダイレクト処理する際の注意点
親でリダイレクト処理をする=標準入出力FDを書き換えると、その後入出力先が変わってしまうので
次の入力を受け取るまでに標準入出力FDをデフォルト値に戻す必要があります。
dup()でFD=0~2をバックアップしておき、一連の処理が終了したら、バックアップFD(標準入出力を指してる)を使って0~2を元の標準入出力に戻します。
一連の処理中に指定されたFD(3>file
の3)とバックアップFDが被ることが判明すれば、再度バックアップFDを取り直す必要があります。 - その他注意点
テスト
minishellの挙動を見る際に、以下の二つのスクリプトを別ターミナルで回していました。
- リーク検出(nopさんのコードを参考にしました)
psコマンドでminishellプロセスを見つけ出して、1秒毎にリークを監視します。
リークが検出されれば何かしら表示されるはずです。
#!/bin/bash echo ------------ps-------------- ps | tee ps_current_process.txt | sed '/grep/d' | GREP_COLOR='0;35' egrep --color=auto '.*minishel.*|$' echo ------------ps-------------- pid=`cat ps_current_process.txt | grep minishell | awk '{print $1}'` echo -e "\033[35mPID->${pid}\033[m" rm ps_current_process.txt while [ 1 ] do leaks $pid 1>/dev/null if [ $? -ne 0 ]; then leaks $pid fi sleep 1 done
#!/bin/bash echo ------------ps-------------- ps | tee ps_current_process.txt | sed '/grep/d' | GREP_COLOR='0;35' egrep --color=auto '.*minishel.*|$' echo ------------ps-------------- pid=`cat ps_current_process.txt | grep minishell | awk '{print $1}'` echo -e "\033[35mPID->${pid}\033[m" sleep 5 rm ps_current_process.txt while [ 1 ] do lsof -p $pid sleep 1 done
まとめ
学べたこと
反省点
チーム開発では、初期段階からお互いが何をしていてどこまで進んでいるのかを把握することが完成への近道ということ・またその意見交換の難しさを体感して学べました。
特に開発初期段階はとにかく手を動かしてみないとわからないことが多かったので、ついつい意見交換より個人的プログラミングに集中してしまって、作業が一旦落ち着いてから意見交換を挟むと膨大な量を説明することになり、相手を疲れさせてしまいました。
どれだけ個人の作業が途中段階でも、一定の期間でお互いを把握することの方が大切だと思いました。
初めはこのプロジェクトが終わる気がしませんでしたが、チームで話し合うと少しずつ前に進み面白かったです。
最後まで一緒に取り組めたrsudoさんには感謝です。
その他参考文献
チーム開発におけるプルリクの作法 - Qiita
各関数のman
printfを実装してみたというお話。
何を作ったか
C言語標準ライブラリのprintf関数を実装。
プロトタイプ宣言 int ft_printf(const char *format, ...);
第一引数 format に%dなど含んだ文字列、その後に引数を受けて、
指定された形式で文字列を標準出力へ出力します。
- 解釈できるformatのルール
%[フラグ][最小フィールド幅].[精度][変換指定子]
- 対応フラグ : '-0'
- 対応変換指定子 : 'cspdiuxX%'
実装する前に
出力はwrite関数を使い、出力した文字数を返り値として返すためにft_printfプログラム内でwrite関数の返り値を受け取るようにします。
可変長引数を扱うので、stdarg系の関数を使います。
プログラムの流れ
- おおまかな流れは、
formatを一文字目から順に見ていき
'%'があれば%以降の文字列解析->第二引数以降を受け取り->指定された形式で文字を出力していく
%がなければ文字をそのまま出力する
例 : ft_printf("%dtokyo", 42)->write("42"), wirte("t"), wirte("o"), write("k"), write("y"), write("o") - 解析結果などの情報は逐次構造体にまとめて処理を進めていく
- 図での流れと、実行例
実行例[ft_printf("%-10.5d", -42)]
出力[-00042 ]
- start.c
- %が指定されているので解析開始、情報を構造体storeに格納
flag = HYPHEN (フラグ)
width = 10 (最小フィールド幅)
prec = 5 (精度)
spec = SPEC_D (変換指定子)
- %が指定されているので解析開始、情報を構造体storeに格納
- prepare_to_put.c
- 引数を受け取る->-42
接頭語を"-"、出力する本体を"42"と考える(こう分けると今後の処理が楽)
ちなみに%pならprefix="0x"、必要ない時はprefix=""にしとく - 出力する文字列の情報を構造体に格納
body = 2 (本体の文字数 : "42"の文字数)
zero = 3 (0の文字数 : 精度の5から"42"の文字数2を引いた数)
prefix = "-"
blank = 4 (空白の文字数 : フィールド幅の10からbody・zero・prefixの文字数を引いた数)
- 引数を受け取る->-42
- put.c
put_flag_hyphen()によりprefix, zero, body, blankの順番で文字を出力していく。まとめ
そもそもprintfで変換指定子以外使ったことがなかったので、
フラグの指定などあらゆる使い方を試してprintfの機能を理解するところから始めました。
enumと関数ポインタ配列の存在を42tokyoの知人に教えてもらい今回初めて使ってみました。
enumを使いだすと、配列の利点(任意のアドレスにランダムアクセスできたり)を使えるようになり、そこから関数ポインタ配列を使ってみることになり、コードがシンプルになっていきました。
コードがまとまっていくと、プログラムの流れもまとめやすくなり、最終的に「変換指定子別で前処理、フラグで出力方法を管理する」方向性が見えてきました。
- start.c